@forward-software/react-auth 2.0.5 → 2.1.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/README.md CHANGED
@@ -147,6 +147,65 @@ The `createAuth` function wraps your `AuthClient` implementation with an `Enhanc
147
147
  - `subscribe(() => { })`, subscribe to AuthClient state changes
148
148
  - `getSnapshot()`, returns the current state of the AuthClient
149
149
 
150
+ ---
151
+
152
+ ### Use multiple AuthClients
153
+
154
+ When your app needs to support multiple authentication providers simultaneously (e.g. username/password alongside Google Sign-In), use `createMultiAuth`:
155
+
156
+ ```ts
157
+ import { createMultiAuth } from '@forward-software/react-auth';
158
+
159
+ export const { AuthProvider, authClients, useAuth } = createMultiAuth({
160
+ credentials: credentialsAuthClient,
161
+ google: googleAuthClient,
162
+ });
163
+ ```
164
+
165
+ The `createMultiAuth` function accepts a map of `{ id: AuthClient }` pairs and returns:
166
+
167
+ - `AuthProvider`, the context Provider component that initialises **all** clients and provides access to them
168
+ - `authClients`, a map of enhanced authentication clients keyed by the IDs you provided
169
+ - `useAuth`, a hook that accepts a client ID and returns the corresponding enhanced auth client
170
+
171
+ #### AuthProvider
172
+
173
+ The same `LoadingComponent` and `ErrorComponent` props are supported. `LoadingComponent` is shown until **all** clients finish initializing. `ErrorComponent` is shown if **any** client's initialization fails.
174
+
175
+ ```tsx
176
+ <AuthProvider
177
+ LoadingComponent={<Spinner />}
178
+ ErrorComponent={<ErrorPage />}
179
+ >
180
+ <App />
181
+ </AuthProvider>
182
+ ```
183
+
184
+ #### useAuth
185
+
186
+ The `useAuth` hook is generic — the return type is automatically narrowed to the exact `EnhancedAuthClient` type for the key you provide:
187
+
188
+ ```tsx
189
+ function MyComponent() {
190
+ // Each call is fully typed based on the key
191
+ const credentialsClient = useAuth('credentials');
192
+ const googleClient = useAuth('google');
193
+
194
+ return (
195
+ <>
196
+ <button onClick={() => credentialsClient.login({ username, password })}>
197
+ Sign in with credentials
198
+ </button>
199
+ <button onClick={() => googleClient.login()}>
200
+ Sign in with Google
201
+ </button>
202
+ </>
203
+ );
204
+ }
205
+ ```
206
+
207
+ Each client provides the same `EnhancedAuthClient` interface described above.
208
+
150
209
  ## Examples
151
210
 
152
211
  The [`examples`](https://github.com/forwardsoftware/react-auth/tree/main/examples) folder in the repository contains some examples of how you can integrate this library in your React app.
package/dist/auth.d.ts CHANGED
@@ -217,10 +217,31 @@ export type AuthProviderProps = PropsWithChildren<{
217
217
  */
218
218
  LoadingComponent?: React.ReactNode;
219
219
  }>;
220
+ /**
221
+ * Creates an authentication context and provider supporting multiple auth clients.
222
+ * Each client is identified by a string key.
223
+ *
224
+ * @template M - A map of client IDs to AuthClient implementations.
225
+ * @template E - The type of error expected during authentication flows. Defaults to `Error`.
226
+ * @param {M} authClientsMap - A map of auth client IDs to their instances.
227
+ * @returns An object containing:
228
+ * - `AuthProvider`: A React component to wrap the application or parts of it.
229
+ * - `authClients`: The map of enhanced authentication clients.
230
+ * - `useAuth`: A hook that accepts a client ID and returns the corresponding enhanced auth client.
231
+ */
232
+ export declare function createMultiAuth<M extends Record<string, AuthClient>, E extends Error = Error>(authClientsMap: M): {
233
+ AuthProvider: React.FC<AuthProviderProps>;
234
+ authClients: { [K in keyof M]: EnhancedAuthClient<M[K], E>; };
235
+ useAuth: <K_1 extends keyof M>(id: K_1) => EnhancedAuthClient<M[K_1], E>;
236
+ };
220
237
  /**
221
238
  * Creates an authentication context and provider for a React application.
222
239
  * It wraps the provided `authClient` with enhanced state management and event handling.
223
240
  *
241
+ * This is a convenience wrapper around `createMultiAuth` for the common single-provider case.
242
+ * Internally it registers the client under the key `'default'` and re-exports a
243
+ * `useAuthClient` hook that delegates to `useAuth('default')`.
244
+ *
224
245
  * @template AC - The type of the base `AuthClient` implementation.
225
246
  * @template E - The type of error expected during authentication flows. Defaults to `Error`.
226
247
  * @param {AC} authClient - The base authentication client instance to use.
package/dist/auth.js CHANGED
@@ -9,7 +9,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  };
10
10
  import { jsx as _jsx } from "react/jsx-runtime";
11
11
  import { createContext, useContext, useEffect, useState } from 'react';
12
- import { useSyncExternalStore } from 'use-sync-external-store/shim';
13
12
  import { createEventEmitter, Deferred } from "./utils";
14
13
  class AuthClientEnhancements {
15
14
  constructor(authClient) {
@@ -170,46 +169,78 @@ class AuthClientEnhancements {
170
169
  }
171
170
  }
172
171
  export function wrapAuthClient(authClient) {
173
- Object.setPrototypeOf(AuthClientEnhancements.prototype, authClient);
174
- return new AuthClientEnhancements(authClient);
172
+ const perInstanceProto = Object.create(authClient);
173
+ Object.getOwnPropertyNames(AuthClientEnhancements.prototype)
174
+ .filter((name) => name !== 'constructor')
175
+ .forEach((name) => {
176
+ const descriptor = Object.getOwnPropertyDescriptor(AuthClientEnhancements.prototype, name);
177
+ if (descriptor) {
178
+ Object.defineProperty(perInstanceProto, name, descriptor);
179
+ }
180
+ });
181
+ const instance = new AuthClientEnhancements(authClient);
182
+ Object.setPrototypeOf(instance, perInstanceProto);
183
+ return instance;
175
184
  }
176
- export function createAuth(authClient) {
177
- const authContext = createContext(null);
178
- const enhancedAuthClient = wrapAuthClient(authClient);
185
+ export function createMultiAuth(authClientsMap) {
186
+ const enhancedClientsMap = Object.keys(authClientsMap).reduce((acc, id) => {
187
+ acc[id] = wrapAuthClient(authClientsMap[id]);
188
+ return acc;
189
+ }, Object.create(null));
190
+ const clientsList = Object.keys(enhancedClientsMap).map((id) => enhancedClientsMap[id]);
191
+ const multiAuthContext = createContext(null);
179
192
  const AuthProvider = ({ children, ErrorComponent, LoadingComponent }) => {
180
- const [isInitFailed, setInitFailed] = useState(false);
181
- const { isAuthenticated, isInitialized } = useSyncExternalStore(enhancedAuthClient.subscribe, enhancedAuthClient.getSnapshot);
193
+ const [initState, setInitState] = useState({
194
+ allInitialized: clientsList.length === 0,
195
+ failed: false,
196
+ });
182
197
  useEffect(() => {
183
- function initAuthClient() {
198
+ function initAllClients() {
184
199
  return __awaiter(this, void 0, void 0, function* () {
185
- const initSuccess = yield enhancedAuthClient.init();
186
- setInitFailed(!initSuccess);
200
+ const results = yield Promise.all(clientsList.map((client) => client.init().catch(() => false)));
201
+ setInitState({ allInitialized: true, failed: results.some((r) => !r) });
187
202
  });
188
203
  }
189
- initAuthClient();
204
+ initAllClients();
190
205
  }, []);
191
- if (!!ErrorComponent && isInitFailed) {
206
+ if (!!ErrorComponent && initState.failed) {
192
207
  return ErrorComponent;
193
208
  }
194
- if (!!LoadingComponent && !isInitialized) {
209
+ if (!!LoadingComponent && !initState.allInitialized) {
195
210
  return LoadingComponent;
196
211
  }
197
- return (_jsx(authContext.Provider, { value: {
198
- authClient: enhancedAuthClient,
199
- isAuthenticated,
200
- isInitialized,
201
- }, children: children }));
212
+ return (_jsx(multiAuthContext.Provider, { value: enhancedClientsMap, children: children }));
202
213
  };
203
- const useAuthClient = function () {
204
- const ctx = useContext(authContext);
214
+ const useAuth = function (id) {
215
+ const ctx = useContext(multiAuthContext);
205
216
  if (!ctx) {
217
+ throw new Error('useAuth hook should be used inside AuthProvider');
218
+ }
219
+ const client = ctx[id];
220
+ if (!client) {
221
+ throw new Error(`useAuth: no auth client registered for id "${String(id)}"`);
222
+ }
223
+ return client;
224
+ };
225
+ return {
226
+ AuthProvider,
227
+ authClients: enhancedClientsMap,
228
+ useAuth,
229
+ };
230
+ }
231
+ export function createAuth(authClient) {
232
+ const { AuthProvider, authClients, useAuth } = createMultiAuth({ default: authClient });
233
+ const useAuthClient = function () {
234
+ try {
235
+ return useAuth('default');
236
+ }
237
+ catch (_a) {
206
238
  throw new Error('useAuthClient hook should be used inside AuthProvider');
207
239
  }
208
- return ctx.authClient;
209
240
  };
210
241
  return {
211
242
  AuthProvider,
212
- authClient: enhancedAuthClient,
243
+ authClient: authClients.default,
213
244
  useAuthClient,
214
245
  };
215
246
  }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { createAuth } from "./auth";
2
- export type { AuthClient } from "./auth";
1
+ export { createAuth, createMultiAuth } from "./auth";
2
+ export type { AuthClient, EnhancedAuthClient } from "./auth";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { createAuth } from "./auth";
1
+ export { createAuth, createMultiAuth } from "./auth";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@forward-software/react-auth",
3
3
  "description": "Simplify your Auth flow when working with React apps",
4
- "version": "2.0.5",
4
+ "version": "2.1.0",
5
5
  "author": "ForWarD Software (https://forwardsoftware.solutions/)",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -40,7 +40,6 @@
40
40
  "@types/node": "^25.2.3",
41
41
  "@types/react": "catalog:",
42
42
  "@types/react-dom": "catalog:",
43
- "@types/use-sync-external-store": "^1.5.0",
44
43
  "@vitejs/plugin-react": "catalog:",
45
44
  "jsdom": "catalog:",
46
45
  "react": "catalog:",
@@ -48,9 +47,7 @@
48
47
  "vite": "catalog:",
49
48
  "vitest": "catalog:"
50
49
  },
51
- "dependencies": {
52
- "use-sync-external-store": "^1.6.0"
53
- },
50
+ "dependencies": {},
54
51
  "peerDependencies": {
55
52
  "react": ">=16.8"
56
53
  }
package/src/auth.tsx CHANGED
@@ -1,6 +1,5 @@
1
1
  import React, { createContext, useContext, useEffect, useState } from 'react';
2
2
  import type { PropsWithChildren } from 'react';
3
- import { useSyncExternalStore } from 'use-sync-external-store/shim';
4
3
 
5
4
  import { createEventEmitter, Deferred, EventReceiver } from "./utils";
6
5
  import type { EventKey } from "./utils";
@@ -433,27 +432,24 @@ export type EnhancedAuthClient<AC extends AuthClient, E extends Error> = AC & Au
433
432
  * @returns {EnhancedAuthClient<AC, E>} - An enhanced authentication client with additional features
434
433
  */
435
434
  export function wrapAuthClient<AC extends AuthClient, E extends Error = Error>(authClient: AC): EnhancedAuthClient<AC, E> {
436
- Object.setPrototypeOf(AuthClientEnhancements.prototype, authClient);
437
-
438
- return new AuthClientEnhancements<AC, E>(authClient) as unknown as EnhancedAuthClient<AC, E>;
439
- }
435
+ // Build a per-instance prototype so that wrapping multiple clients does not cause
436
+ // one client's methods to bleed into another.
437
+ // Chain: instance → perInstanceProto (enhancement methods) authClient (raw AC methods)
438
+ const perInstanceProto = Object.create(authClient);
439
+ Object.getOwnPropertyNames(AuthClientEnhancements.prototype)
440
+ .filter((name) => name !== 'constructor')
441
+ .forEach((name) => {
442
+ const descriptor = Object.getOwnPropertyDescriptor(AuthClientEnhancements.prototype, name);
443
+ if (descriptor) {
444
+ Object.defineProperty(perInstanceProto, name, descriptor);
445
+ }
446
+ });
440
447
 
441
- /**
442
- * Represents the current state of the authentication provider
443
- */
444
- type AuthProviderState = {
445
- isAuthenticated: boolean;
446
- isInitialized: boolean;
447
- };
448
+ const instance = new AuthClientEnhancements<AC, E>(authClient);
449
+ Object.setPrototypeOf(instance, perInstanceProto);
448
450
 
449
- /**
450
- * The authentication context containing both the state and the enhanced auth client
451
- * @template AC - The AuthClient implementation type
452
- * @template E - The error type used throughout the authentication flow
453
- */
454
- type AuthContext<AC extends AuthClient, E extends Error> = AuthProviderState & {
455
- authClient: EnhancedAuthClient<AC, E>;
456
- };
451
+ return instance as unknown as EnhancedAuthClient<AC, E>;
452
+ }
457
453
 
458
454
  /**
459
455
  * Props that can be passed to AuthProvider
@@ -471,76 +467,130 @@ export type AuthProviderProps = PropsWithChildren<{
471
467
  }>;
472
468
 
473
469
  /**
474
- * Creates an authentication context and provider for a React application.
475
- * It wraps the provided `authClient` with enhanced state management and event handling.
470
+ * Creates an authentication context and provider supporting multiple auth clients.
471
+ * Each client is identified by a string key.
476
472
  *
477
- * @template AC - The type of the base `AuthClient` implementation.
473
+ * @template M - A map of client IDs to AuthClient implementations.
478
474
  * @template E - The type of error expected during authentication flows. Defaults to `Error`.
479
- * @param {AC} authClient - The base authentication client instance to use.
475
+ * @param {M} authClientsMap - A map of auth client IDs to their instances.
480
476
  * @returns An object containing:
481
477
  * - `AuthProvider`: A React component to wrap the application or parts of it.
482
- * - `authClient`: The enhanced authentication client instance.
483
- * - `useAuthClient`: A hook to access the enhanced `authClient` within the `AuthProvider`.
478
+ * - `authClients`: The map of enhanced authentication clients.
479
+ * - `useAuth`: A hook that accepts a client ID and returns the corresponding enhanced auth client.
484
480
  */
485
- export function createAuth<AC extends AuthClient, E extends Error = Error>(authClient: AC) {
486
- // Create a React context containing an AuthClient instance.
487
- const authContext = createContext<AuthContext<AC, E> | null>(null);
488
-
489
- const enhancedAuthClient = wrapAuthClient<AC, E>(authClient);
490
-
491
- // Create the React Context Provider for the AuthClient instance.
481
+ export function createMultiAuth<M extends Record<string, AuthClient>, E extends Error = Error>(
482
+ authClientsMap: M,
483
+ ) {
484
+ type EnhancedMap = { [K in keyof M]: EnhancedAuthClient<M[K], E> };
485
+
486
+ const enhancedClientsMap = (Object.keys(authClientsMap) as (keyof M)[]).reduce(
487
+ (acc, id) => {
488
+ acc[id] = wrapAuthClient<M[typeof id], E>(authClientsMap[id]);
489
+ return acc;
490
+ },
491
+ Object.create(null) as EnhancedMap,
492
+ );
493
+
494
+ const clientsList = (Object.keys(enhancedClientsMap) as (keyof M)[]).map(
495
+ (id) => enhancedClientsMap[id],
496
+ );
497
+
498
+ const multiAuthContext = createContext<EnhancedMap | null>(null);
499
+
500
+ // Create the React Context Provider for all AuthClient instances.
492
501
  const AuthProvider: React.FC<AuthProviderProps> = ({ children, ErrorComponent, LoadingComponent }) => {
493
- const [isInitFailed, setInitFailed] = useState(false);
494
- const { isAuthenticated, isInitialized } = useSyncExternalStore(enhancedAuthClient.subscribe, enhancedAuthClient.getSnapshot);
502
+ const [initState, setInitState] = useState<{ allInitialized: boolean; failed: boolean }>({
503
+ allInitialized: clientsList.length === 0,
504
+ failed: false,
505
+ });
495
506
 
496
507
  useEffect(() => {
497
- async function initAuthClient() {
498
- // Call init function
499
- const initSuccess = await enhancedAuthClient.init();
500
- setInitFailed(!initSuccess);
508
+ async function initAllClients() {
509
+ // Each client's init() is wrapped with .catch() so that a rejection from one client
510
+ // (e.g. an onPostInit error) does not short-circuit the others; all clients always
511
+ // get the chance to finish initializing before we update the provider state.
512
+ const results = await Promise.all(
513
+ clientsList.map((client) => client.init().catch((): boolean => false)),
514
+ );
515
+ setInitState({ allInitialized: true, failed: results.some((r) => !r) });
501
516
  }
502
517
 
503
- // Init AuthClient
504
- initAuthClient();
518
+ initAllClients();
505
519
  }, []);
506
520
 
507
- if (!!ErrorComponent && isInitFailed) {
521
+ if (!!ErrorComponent && initState.failed) {
508
522
  return ErrorComponent;
509
523
  }
510
524
 
511
- if (!!LoadingComponent && !isInitialized) {
525
+ if (!!LoadingComponent && !initState.allInitialized) {
512
526
  return LoadingComponent;
513
527
  }
514
528
 
515
529
  return (
516
- <authContext.Provider
517
- value={{
518
- authClient: enhancedAuthClient,
519
- isAuthenticated,
520
- isInitialized,
521
- }}
522
- >
530
+ <multiAuthContext.Provider value={enhancedClientsMap}>
523
531
  {children}
524
- </authContext.Provider>
532
+ </multiAuthContext.Provider>
525
533
  );
526
534
  };
527
535
 
536
+ /**
537
+ * Hook to access a specific authentication client by its ID within the AuthProvider.
538
+ * @throws Error if used outside of an AuthProvider
539
+ * @throws Error if the provided id is not registered in the clients map
540
+ */
541
+ const useAuth = function <K extends keyof M>(id: K): EnhancedAuthClient<M[K], E> {
542
+ const ctx = useContext(multiAuthContext);
543
+ if (!ctx) {
544
+ throw new Error('useAuth hook should be used inside AuthProvider');
545
+ }
546
+ const client = ctx[id];
547
+ if (!client) {
548
+ throw new Error(`useAuth: no auth client registered for id "${String(id)}"`);
549
+ }
550
+ return client;
551
+ };
552
+
553
+ return {
554
+ AuthProvider,
555
+ authClients: enhancedClientsMap,
556
+ useAuth,
557
+ };
558
+ }
559
+
560
+ /**
561
+ * Creates an authentication context and provider for a React application.
562
+ * It wraps the provided `authClient` with enhanced state management and event handling.
563
+ *
564
+ * This is a convenience wrapper around `createMultiAuth` for the common single-provider case.
565
+ * Internally it registers the client under the key `'default'` and re-exports a
566
+ * `useAuthClient` hook that delegates to `useAuth('default')`.
567
+ *
568
+ * @template AC - The type of the base `AuthClient` implementation.
569
+ * @template E - The type of error expected during authentication flows. Defaults to `Error`.
570
+ * @param {AC} authClient - The base authentication client instance to use.
571
+ * @returns An object containing:
572
+ * - `AuthProvider`: A React component to wrap the application or parts of it.
573
+ * - `authClient`: The enhanced authentication client instance.
574
+ * - `useAuthClient`: A hook to access the enhanced `authClient` within the `AuthProvider`.
575
+ */
576
+ export function createAuth<AC extends AuthClient, E extends Error = Error>(authClient: AC) {
577
+ const { AuthProvider, authClients, useAuth } = createMultiAuth<{ default: AC }, E>({ default: authClient });
578
+
528
579
  /**
529
580
  * Hook to access the authentication client within the AuthProvider
530
581
  * @throws Error if used outside of an AuthProvider
531
582
  */
532
583
  const useAuthClient = function (): EnhancedAuthClient<AC, E> {
533
- const ctx = useContext(authContext);
534
- if (!ctx) {
584
+ try {
585
+ return useAuth('default');
586
+ } catch {
535
587
  throw new Error('useAuthClient hook should be used inside AuthProvider');
536
588
  }
537
-
538
- return ctx.authClient;
539
589
  };
540
590
 
541
591
  return {
542
592
  AuthProvider,
543
- authClient: enhancedAuthClient,
593
+ authClient: authClients.default,
544
594
  useAuthClient,
545
595
  };
546
596
  }
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { createAuth } from "./auth";
2
- export type { AuthClient } from "./auth";
1
+ export { createAuth, createMultiAuth } from "./auth";
2
+ export type { AuthClient, EnhancedAuthClient } from "./auth";