@meet_patel_03/authflow-react 0.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 ADDED
@@ -0,0 +1,606 @@
1
+ # @meet_patel_03/authflow-react
2
+
3
+ React SDK for AuthFlow — hooks and provider for the Authorization Code + PKCE flow. Includes state management, automatic token refresh, and session persistence.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @meet_patel_03/authflow-react @meet_patel_03/authflow-js
9
+ ```
10
+
11
+ Requirements:
12
+
13
+ - React 18+
14
+ - `@meet_patel_03/authflow-js` (peer dependency)
15
+
16
+ ## Quick start
17
+
18
+ ```tsx
19
+ // main.tsx — wrap your app with AuthFlowProvider
20
+ import { AuthFlowProvider } from "@meet_patel_03/authflow-react";
21
+ import { App } from "./App";
22
+
23
+ export default function Root() {
24
+ return (
25
+ <AuthFlowProvider
26
+ config={{
27
+ domain: "https://your-authflow-domain.com",
28
+ clientId: "af_your_client_id",
29
+ redirectUri: "https://your-app.com/callback",
30
+ scope: "openid profile email", // optional
31
+ }}>
32
+ <App />
33
+ </AuthFlowProvider>
34
+ );
35
+ }
36
+
37
+ // components/navbar.tsx — use hooks in any component
38
+ import { useAuthFlow } from "@meet_patel_03/authflow-react";
39
+
40
+ export function Navbar() {
41
+ const { user, isAuthenticated, isLoading, loginWithRedirect, logout } =
42
+ useAuthFlow();
43
+
44
+ if (isLoading) {
45
+ return <div>Loading authentication state...</div>;
46
+ }
47
+
48
+ if (!isAuthenticated) {
49
+ return (
50
+ <nav>
51
+ <button onClick={() => loginWithRedirect({ screen_hint: "login" })}>
52
+ Login
53
+ </button>
54
+ <button onClick={() => loginWithRedirect({ screen_hint: "signup" })}>
55
+ Sign Up
56
+ </button>
57
+ </nav>
58
+ );
59
+ }
60
+
61
+ return (
62
+ <nav>
63
+ <span>Hello {user?.name || user?.email}</span>
64
+ <button onClick={() => logout({ returnTo: window.location.origin })}>
65
+ Logout
66
+ </button>
67
+ </nav>
68
+ );
69
+ }
70
+
71
+ // components/protected-route.tsx — protect routes
72
+ import { useAuthFlow } from "@meet_patel_03/authflow-react";
73
+ import { ReactNode } from "react";
74
+ import { Navigate } from "react-router";
75
+
76
+ export function ProtectedRoute({ children }: { children: ReactNode }) {
77
+ const { isAuthenticated, isLoading } = useAuthFlow();
78
+
79
+ if (isLoading) return <div>Loading...</div>;
80
+ if (!isAuthenticated) return <Navigate to="/login" />;
81
+
82
+ return <>{children}</>;
83
+ }
84
+
85
+ // pages/callback.tsx — handle OAuth redirect
86
+ import { useAuthCallback } from "@meet_patel_03/authflow-react";
87
+ import { useNavigate } from "react-router";
88
+
89
+ export function CallbackPage() {
90
+ const navigate = useNavigate();
91
+ const { status, error } = useAuthCallback({
92
+ onSuccess: () => {
93
+ // Redirect to dashboard after successful login
94
+ navigate("/dashboard", { replace: true });
95
+ },
96
+ onError: (err) => {
97
+ console.error("Login failed:", err);
98
+ navigate("/login?error=" + encodeURIComponent(err), { replace: true });
99
+ },
100
+ });
101
+
102
+ if (status === "loading") {
103
+ return <div>Completing login...</div>;
104
+ }
105
+
106
+ if (status === "error") {
107
+ return (
108
+ <div>
109
+ <p>Login failed: {error}</p>
110
+ <a href="/login">Back to login</a>
111
+ </div>
112
+ );
113
+ }
114
+
115
+ // Success — navigation happens via onSuccess callback
116
+ return null;
117
+ }
118
+
119
+ // pages/dashboard.tsx — access user and tokens
120
+ import { useAuthFlow } from "@meet_patel_03/authflow-react";
121
+
122
+ export function DashboardPage() {
123
+ const { user, getAccessToken } = useAuthFlow();
124
+
125
+ const fetchProtectedApi = async () => {
126
+ const token = await getAccessToken();
127
+ if (!token) {
128
+ console.error("Not authenticated");
129
+ return;
130
+ }
131
+
132
+ const res = await fetch("/api/me", {
133
+ headers: { Authorization: `Bearer ${token}` },
134
+ });
135
+ const data = await res.json();
136
+ console.log("API response:", data);
137
+ };
138
+
139
+ return (
140
+ <div>
141
+ <h1>Welcome, {user?.email}</h1>
142
+ <p>Email verified: {user?.email_verified ? "Yes" : "No"}</p>
143
+ <button onClick={fetchProtectedApi}>Call API</button>
144
+ </div>
145
+ );
146
+ }
147
+ ```
148
+
149
+ ## Configuration
150
+
151
+ The `<AuthFlowProvider>` accepts the same configuration as `AuthFlowClient` from `@authflow/js`:
152
+
153
+ | Option | Required | Default | Description |
154
+ | ------------- | -------- | ------------------------------------ | --------------------------------------------------------------- |
155
+ | `domain` | Yes | — | Your AuthFlow backend URL (e.g. `https://auth.yourcompany.com`) |
156
+ | `clientId` | Yes | — | Application `client_id` from the AuthFlow dashboard |
157
+ | `redirectUri` | No | `window.location.origin + /callback` | OAuth2 redirect URI. Must match Allowed Callback URLs |
158
+ | `scope` | No | `openid profile email` | Space-separated OAuth2 scopes to request |
159
+ | `storageKey` | No | `authflow` | localStorage key prefix for token persistence |
160
+
161
+ ## API
162
+
163
+ ### `<AuthFlowProvider config={...}>`
164
+
165
+ Wraps your app and provides authentication context. Initialize this **once** at the root level.
166
+
167
+ **Props:**
168
+
169
+ - `config` — `AuthFlowConfig` object (see Configuration above)
170
+ - `children` — React elements to wrap
171
+
172
+ **Behavior:**
173
+
174
+ - Creates an internal `AuthFlowClient` instance (reused across re-renders)
175
+ - On mount, restores authentication state from localStorage
176
+ - Silently refreshes expired access tokens
177
+ - Provides context values to all child components
178
+
179
+ ```tsx
180
+ import { AuthFlowProvider } from "@meet_patel_03/authflow-react";
181
+
182
+ export function App() {
183
+ return (
184
+ <AuthFlowProvider
185
+ config={{
186
+ domain: "https://auth.yourcompany.com",
187
+ clientId: "af_your_client_id",
188
+ redirectUri: "https://your-app.com/callback",
189
+ }}>
190
+ <Routes>
191
+ <Route
192
+ path="/callback"
193
+ element={<CallbackPage />}
194
+ />
195
+ <Route
196
+ path="/dashboard"
197
+ element={
198
+ <ProtectedRoute>
199
+ <DashboardPage />
200
+ </ProtectedRoute>
201
+ }
202
+ />
203
+ {/* ... */}
204
+ </Routes>
205
+ </AuthFlowProvider>
206
+ );
207
+ }
208
+ ```
209
+
210
+ ### `useAuthFlow()`
211
+
212
+ Primary hook — provides access to authentication state and actions. Must be used inside `<AuthFlowProvider>`.
213
+
214
+ **Returns:**
215
+
216
+ ```ts
217
+ {
218
+ client: AuthFlowClient, // Underlying SDK instance for advanced use
219
+ user: AuthFlowUser | null, // Current user or null if not authenticated
220
+ isLoading: boolean, // True while initial auth state is loading
221
+ isAuthenticated: boolean, // True if user is logged in with valid token
222
+ loginWithRedirect(options?), // Redirect to login page
223
+ handleRedirectCallback(), // Exchange code for tokens (call in /callback)
224
+ getAccessToken(), // Get valid access token (auto-refreshes)
225
+ logout(options?), // Logout and clear session
226
+ }
227
+ ```
228
+
229
+ **Throws:** If used outside `<AuthFlowProvider>`.
230
+
231
+ ```ts
232
+ import { useAuthFlow } from "@meet_patel_03/authflow-react";
233
+
234
+ function MyComponent() {
235
+ const { user, isLoading, isAuthenticated, loginWithRedirect } = useAuthFlow();
236
+
237
+ if (isLoading) return <div>Checking authentication...</div>;
238
+
239
+ if (!isAuthenticated) {
240
+ return <button onClick={() => loginWithRedirect()}>Login</button>;
241
+ }
242
+
243
+ return <div>Welcome, {user?.email}</div>;
244
+ }
245
+ ```
246
+
247
+ #### `loginWithRedirect(options?)`
248
+
249
+ Redirects the user to the AuthFlow Universal Login page.
250
+
251
+ **Parameters:**
252
+
253
+ - `options.screen_hint` (optional) — `"login"` (default) or `"signup"` to show registration page
254
+
255
+ ```ts
256
+ const { loginWithRedirect } = useAuthFlow();
257
+
258
+ // Login page
259
+ <button onClick={() => loginWithRedirect()}>Login</button>
260
+
261
+ // Signup page
262
+ <button onClick={() => loginWithRedirect({ screen_hint: "signup" })}>Sign Up</button>
263
+ ```
264
+
265
+ #### `handleRedirectCallback()`
266
+
267
+ Completes the OAuth2 redirect. **Call this once in your `/callback` route.**
268
+
269
+ **Returns:** `Promise<TokenSet>` — resolves with access, refresh, and ID tokens
270
+
271
+ **Throws:** `AuthFlowError` if the code exchange fails
272
+
273
+ ```ts
274
+ const { handleRedirectCallback } = useAuthFlow();
275
+
276
+ try {
277
+ const tokens = await handleRedirectCallback();
278
+ console.log("Login successful!");
279
+ } catch (err) {
280
+ console.error("Token exchange failed:", err);
281
+ }
282
+ ```
283
+
284
+ #### `getAccessToken()`
285
+
286
+ Returns a valid access token, automatically refreshing if near expiry.
287
+
288
+ **Returns:** `Promise<string | null>` — valid token or null if not authenticated
289
+
290
+ **Use cases:**
291
+
292
+ - Before making API calls
293
+ - Getting the token to send in Authorization header
294
+
295
+ ```ts
296
+ const { getAccessToken } = useAuthFlow();
297
+
298
+ const fetchUserProfile = async () => {
299
+ const token = await getAccessToken();
300
+ if (!token) {
301
+ console.log("Not authenticated");
302
+ return;
303
+ }
304
+
305
+ const res = await fetch("/api/profile", {
306
+ headers: { Authorization: `Bearer ${token}` },
307
+ });
308
+ const profile = await res.json();
309
+ console.log(profile);
310
+ };
311
+ ```
312
+
313
+ #### `logout(options?)`
314
+
315
+ Revokes the server-side session, clears local tokens, and redirects.
316
+
317
+ **Parameters:**
318
+
319
+ - `options.returnTo` (optional) — URL to redirect to after logout (defaults to `window.location.origin`)
320
+
321
+ ```ts
322
+ const { logout } = useAuthFlow();
323
+
324
+ <button onClick={() => logout({ returnTo: "https://your-app.com" })}>
325
+ Logout
326
+ </button>
327
+ ```
328
+
329
+ ### `useUser()`
330
+
331
+ Convenience hook — returns just the current user without needing the full `useAuthFlow()` context.
332
+
333
+ **Returns:** `AuthFlowUser | null`
334
+
335
+ ```ts
336
+ import { useUser } from "@meet_patel_03/authflow-react";
337
+
338
+ function UserCard() {
339
+ const user = useUser();
340
+
341
+ if (!user) return <div>Not logged in</div>;
342
+
343
+ return (
344
+ <div>
345
+ <p>Name: {user.name}</p>
346
+ <p>Email: {user.email}</p>
347
+ <p>Verified: {user.email_verified ? "Yes" : "No"}</p>
348
+ </div>
349
+ );
350
+ }
351
+ ```
352
+
353
+ ### `useAuthCallback({ onSuccess?, onError? })`
354
+
355
+ Handles the OAuth2 redirect callback. Drop this in your `/callback` route component.
356
+
357
+ **Parameters:**
358
+
359
+ - `onSuccess` (optional) — Callback when token exchange succeeds (typically redirects to dashboard)
360
+ - `onError` (optional) — Callback when exchange fails, receives error message
361
+
362
+ **Returns:**
363
+
364
+ ```ts
365
+ {
366
+ status: "loading" | "success" | "error", // Current state
367
+ error: string | null, // Error message if status === "error"
368
+ }
369
+ ```
370
+
371
+ **Important:** This hook guards against React 18 StrictMode double-execution, so the token exchange runs exactly once even in development.
372
+
373
+ ```ts
374
+ import { useAuthCallback } from "@meet_patel_03/authflow-react";
375
+ import { useNavigate } from "react-router-dom";
376
+
377
+ function CallbackPage() {
378
+ const navigate = useNavigate();
379
+ const { status, error } = useAuthCallback({
380
+ onSuccess: () => {
381
+ console.log("Login successful!");
382
+ navigate("/dashboard", { replace: true });
383
+ },
384
+ onError: (err) => {
385
+ console.error("Login error:", err);
386
+ navigate("/login?error=" + encodeURIComponent(err), { replace: true });
387
+ },
388
+ });
389
+
390
+ if (status === "loading") {
391
+ return <div>Completing login...</div>;
392
+ }
393
+
394
+ if (status === "error") {
395
+ return (
396
+ <div>
397
+ <p>Login failed: {error}</p>
398
+ <a href="/login">Try again</a>
399
+ </div>
400
+ );
401
+ }
402
+
403
+ return null;
404
+ }
405
+ ```
406
+
407
+ ### `withAuthRequired(Component)`
408
+
409
+ Higher-Order Component that protects a route from unauthenticated access.
410
+
411
+ **Behavior:**
412
+
413
+ - Returns `null` while `isLoading` is true
414
+ - Redirects to `/login` if not authenticated
415
+ - Renders the component if authenticated
416
+
417
+ **Parameters:**
418
+
419
+ - `Component` — React component to protect
420
+
421
+ **Returns:** Wrapped component
422
+
423
+ ```ts
424
+ import { withAuthRequired } from "@meet_patel_03/authflow-react";
425
+
426
+ function DashboardPage() {
427
+ const { user } = useAuthFlow();
428
+ return <h1>Welcome, {user?.email}</h1>;
429
+ }
430
+
431
+ // Protect the route
432
+ export default withAuthRequired(DashboardPage);
433
+
434
+ // In router:
435
+ <Route path="/dashboard" element={<ProtectedDashboard />} />
436
+ ```
437
+
438
+ ## Error Handling
439
+
440
+ Use `AuthFlowError` from `@meet_patel_03/authflow-js` for detailed error handling:
441
+
442
+ ```ts
443
+ import { useAuthCallback } from "@meet_patel_03/authflow-react";
444
+ import { AuthFlowError } from "@meet_patel_03/authflow-js";
445
+
446
+ function CallbackPage() {
447
+ const { status, error } = useAuthCallback({
448
+ onError: (err) => {
449
+ console.error("Callback error:", err);
450
+ // Network errors, CSRF, invalid state, etc.
451
+ },
452
+ });
453
+ }
454
+ ```
455
+
456
+ Common error scenarios:
457
+
458
+ - **"state_mismatch"** — CSRF token validation failed; user may have been redirected from a different browser tab
459
+ - **"missing_code"** — OAuth provider didn't return an authorization code
460
+ - **"invalid_grant"** — Authorization code expired, already used, or invalid
461
+ - Network errors if the backend is unreachable
462
+
463
+ ## Types
464
+
465
+ The SDK exports all types from `@meet_patel_03/authflow-js`:
466
+
467
+ - **`AuthFlowUser`** — User claims (sub, email, name, email_verified, custom fields)
468
+ - **`AuthFlowConfig`** — Provider configuration
469
+ - **`TokenSet`** — OAuth2 tokens (access_token, refresh_token, id_token, expires_in, etc.)
470
+ - **`AuthFlowError`** — OAuth2 errors with `error` and `errorDescription` properties
471
+ - **`AuthFlowProviderProps`** — Props for `<AuthFlowProvider>`
472
+ - **`AuthFlowContextValue`** — Return type of `useAuthFlow()`
473
+ - **`CallbackStatus`** — Type of `status` from `useAuthCallback()` ("loading" | "success" | "error")
474
+
475
+ ```ts
476
+ import type {
477
+ AuthFlowUser,
478
+ AuthFlowConfig,
479
+ TokenSet,
480
+ AuthFlowError,
481
+ } from "@meet_patel_03/authflow-react";
482
+ ```
483
+
484
+ ## Common Patterns
485
+
486
+ ### Protected Route Component
487
+
488
+ ```tsx
489
+ import { useAuthFlow } from "@meet_patel_03/authflow-react";
490
+ import { Navigate, useLocation } from "react-router-dom";
491
+
492
+ export function ProtectedRoute({ children }: { children: React.ReactNode }) {
493
+ const { isAuthenticated, isLoading } = useAuthFlow();
494
+ const location = useLocation();
495
+
496
+ if (isLoading) return <div>Loading...</div>;
497
+
498
+ if (!isAuthenticated) {
499
+ return (
500
+ <Navigate
501
+ to="/login"
502
+ state={{ from: location }}
503
+ replace
504
+ />
505
+ );
506
+ }
507
+
508
+ return <>{children}</>;
509
+ }
510
+
511
+ // Usage:
512
+ <Route
513
+ path="/dashboard"
514
+ element={
515
+ <ProtectedRoute>
516
+ <DashboardPage />
517
+ </ProtectedRoute>
518
+ }
519
+ />;
520
+ ```
521
+
522
+ ### API Client with Auto-Refresh
523
+
524
+ ```ts
525
+ import { useAuthFlow } from "@meet_patel_03/authflow-react";
526
+
527
+ export function useApi() {
528
+ const { getAccessToken } = useAuthFlow();
529
+
530
+ return {
531
+ async get(url: string) {
532
+ const token = await getAccessToken();
533
+ if (!token) throw new Error("Not authenticated");
534
+
535
+ return fetch(url, {
536
+ headers: { Authorization: `Bearer ${token}` },
537
+ }).then((r) => r.json());
538
+ },
539
+
540
+ async post(url: string, data: unknown) {
541
+ const token = await getAccessToken();
542
+ if (!token) throw new Error("Not authenticated");
543
+
544
+ return fetch(url, {
545
+ method: "POST",
546
+ headers: {
547
+ "Content-Type": "application/json",
548
+ Authorization: `Bearer ${token}`,
549
+ },
550
+ body: JSON.stringify(data),
551
+ }).then((r) => r.json());
552
+ },
553
+ };
554
+ }
555
+
556
+ // Usage in components:
557
+ function MyComponent() {
558
+ const api = useApi();
559
+
560
+ const deleteAccount = async () => {
561
+ await api.post("/api/account/delete", {});
562
+ };
563
+ }
564
+ ```
565
+
566
+ ### Conditional Navigation
567
+
568
+ ```tsx
569
+ import { useAuthFlow } from "@meet_patel_03/authflow-react";
570
+ import { useNavigate } from "react-router-dom";
571
+
572
+ function LoginPrompt() {
573
+ const { isAuthenticated, loginWithRedirect } = useAuthFlow();
574
+ const navigate = useNavigate();
575
+
576
+ const handleClick = async () => {
577
+ if (!isAuthenticated) {
578
+ await loginWithRedirect();
579
+ } else {
580
+ navigate("/dashboard");
581
+ }
582
+ };
583
+
584
+ return (
585
+ <button onClick={handleClick}>
586
+ {isAuthenticated ? "Go to Dashboard" : "Login"}
587
+ </button>
588
+ );
589
+ }
590
+ ```
591
+
592
+ ### Loading States
593
+
594
+ ```tsx
595
+ import { useAuthFlow } from "@meet_patel_03/authflow-react";
596
+
597
+ function RootLayout() {
598
+ const { isLoading } = useAuthFlow();
599
+
600
+ if (isLoading) {
601
+ return <div>Initializing authentication...</div>;
602
+ }
603
+
604
+ return <main>{/* Your routes */}</main>;
605
+ }
606
+ ```
@@ -0,0 +1,84 @@
1
+ import React from 'react';
2
+ import { AuthFlowClient, AuthFlowUser, TokenSet, AuthFlowConfig } from '@meet_patel_03/authflow-js';
3
+ export { AuthFlowConfig, AuthFlowError, AuthFlowUser, TokenSet } from '@meet_patel_03/authflow-js';
4
+
5
+ interface AuthFlowContextValue {
6
+ /** The underlying AuthFlowClient instance — use this for advanced operations */
7
+ client: AuthFlowClient;
8
+ /** Current authenticated user, null if not logged in */
9
+ user: AuthFlowUser | null;
10
+ /** True once the initial auth state has been resolved */
11
+ isLoading: boolean;
12
+ /** True if the user is logged in and the access token is not expired */
13
+ isAuthenticated: boolean;
14
+ /** Redirect to the AuthFlow Universal Login page */
15
+ loginWithRedirect: (options?: {
16
+ screen_hint?: "login" | "signup";
17
+ }) => Promise<void>;
18
+ /**
19
+ * Call in your /callback route.
20
+ * Exchanges the authorization code for tokens, then resolves with the TokenSet.
21
+ */
22
+ handleRedirectCallback: () => Promise<TokenSet>;
23
+ /**
24
+ * Returns a valid access token, auto-refreshing if near expiry.
25
+ * Returns null if not authenticated.
26
+ */
27
+ getAccessToken: () => Promise<string | null>;
28
+ /** Revokes server-side session, clears tokens, and redirects to returnTo */
29
+ logout: (options?: {
30
+ returnTo?: string;
31
+ }) => Promise<void>;
32
+ }
33
+ interface AuthFlowProviderProps {
34
+ /** AuthFlow configuration — same shape as AuthFlowClient constructor */
35
+ config: AuthFlowConfig;
36
+ children: React.ReactNode;
37
+ }
38
+ declare const AuthFlowProvider: React.FC<AuthFlowProviderProps>;
39
+ /**
40
+ * Primary hook — use this in any component to access auth state and actions.
41
+ *
42
+ * @throws if used outside of <AuthFlowProvider>
43
+ */
44
+ declare const useAuthFlow: () => AuthFlowContextValue;
45
+ /** Convenience hook — returns just the current user */
46
+ declare const useUser: () => AuthFlowUser | null;
47
+ /**
48
+ * HOC that redirects to login if the user is not authenticated.
49
+ * Renders null while auth state is loading.
50
+ *
51
+ * @example
52
+ * const ProtectedDashboard = withAuthRequired(Dashboard);
53
+ */
54
+ declare function withAuthRequired<P extends object>(Component: React.ComponentType<P>, options?: {
55
+ returnTo?: string;
56
+ }): React.FC<P>;
57
+
58
+ type CallbackStatus = "loading" | "success" | "error";
59
+ interface UseAuthCallbackResult {
60
+ status: CallbackStatus;
61
+ error: string | null;
62
+ }
63
+ /**
64
+ * Drop this in your /callback route component.
65
+ * Handles the token exchange and calls onSuccess or onError when done.
66
+ *
67
+ * @example
68
+ * function CallbackPage() {
69
+ * const { status, error } = useAuthCallback({
70
+ * onSuccess: () => navigate("/dashboard"),
71
+ * onError: (err) => navigate("/login?error=" + err),
72
+ * });
73
+ *
74
+ * if (status === "loading") return <Spinner />;
75
+ * if (status === "error") return <div>Login failed: {error}</div>;
76
+ * return null;
77
+ * }
78
+ */
79
+ declare const useAuthCallback: (options?: {
80
+ onSuccess?: () => void;
81
+ onError?: (error: string) => void;
82
+ }) => UseAuthCallbackResult;
83
+
84
+ export { type AuthFlowContextValue, AuthFlowProvider, type AuthFlowProviderProps, type CallbackStatus, type UseAuthCallbackResult, useAuthCallback, useAuthFlow, useUser, withAuthRequired };
@@ -0,0 +1,84 @@
1
+ import React from 'react';
2
+ import { AuthFlowClient, AuthFlowUser, TokenSet, AuthFlowConfig } from '@meet_patel_03/authflow-js';
3
+ export { AuthFlowConfig, AuthFlowError, AuthFlowUser, TokenSet } from '@meet_patel_03/authflow-js';
4
+
5
+ interface AuthFlowContextValue {
6
+ /** The underlying AuthFlowClient instance — use this for advanced operations */
7
+ client: AuthFlowClient;
8
+ /** Current authenticated user, null if not logged in */
9
+ user: AuthFlowUser | null;
10
+ /** True once the initial auth state has been resolved */
11
+ isLoading: boolean;
12
+ /** True if the user is logged in and the access token is not expired */
13
+ isAuthenticated: boolean;
14
+ /** Redirect to the AuthFlow Universal Login page */
15
+ loginWithRedirect: (options?: {
16
+ screen_hint?: "login" | "signup";
17
+ }) => Promise<void>;
18
+ /**
19
+ * Call in your /callback route.
20
+ * Exchanges the authorization code for tokens, then resolves with the TokenSet.
21
+ */
22
+ handleRedirectCallback: () => Promise<TokenSet>;
23
+ /**
24
+ * Returns a valid access token, auto-refreshing if near expiry.
25
+ * Returns null if not authenticated.
26
+ */
27
+ getAccessToken: () => Promise<string | null>;
28
+ /** Revokes server-side session, clears tokens, and redirects to returnTo */
29
+ logout: (options?: {
30
+ returnTo?: string;
31
+ }) => Promise<void>;
32
+ }
33
+ interface AuthFlowProviderProps {
34
+ /** AuthFlow configuration — same shape as AuthFlowClient constructor */
35
+ config: AuthFlowConfig;
36
+ children: React.ReactNode;
37
+ }
38
+ declare const AuthFlowProvider: React.FC<AuthFlowProviderProps>;
39
+ /**
40
+ * Primary hook — use this in any component to access auth state and actions.
41
+ *
42
+ * @throws if used outside of <AuthFlowProvider>
43
+ */
44
+ declare const useAuthFlow: () => AuthFlowContextValue;
45
+ /** Convenience hook — returns just the current user */
46
+ declare const useUser: () => AuthFlowUser | null;
47
+ /**
48
+ * HOC that redirects to login if the user is not authenticated.
49
+ * Renders null while auth state is loading.
50
+ *
51
+ * @example
52
+ * const ProtectedDashboard = withAuthRequired(Dashboard);
53
+ */
54
+ declare function withAuthRequired<P extends object>(Component: React.ComponentType<P>, options?: {
55
+ returnTo?: string;
56
+ }): React.FC<P>;
57
+
58
+ type CallbackStatus = "loading" | "success" | "error";
59
+ interface UseAuthCallbackResult {
60
+ status: CallbackStatus;
61
+ error: string | null;
62
+ }
63
+ /**
64
+ * Drop this in your /callback route component.
65
+ * Handles the token exchange and calls onSuccess or onError when done.
66
+ *
67
+ * @example
68
+ * function CallbackPage() {
69
+ * const { status, error } = useAuthCallback({
70
+ * onSuccess: () => navigate("/dashboard"),
71
+ * onError: (err) => navigate("/login?error=" + err),
72
+ * });
73
+ *
74
+ * if (status === "loading") return <Spinner />;
75
+ * if (status === "error") return <div>Login failed: {error}</div>;
76
+ * return null;
77
+ * }
78
+ */
79
+ declare const useAuthCallback: (options?: {
80
+ onSuccess?: () => void;
81
+ onError?: (error: string) => void;
82
+ }) => UseAuthCallbackResult;
83
+
84
+ export { type AuthFlowContextValue, AuthFlowProvider, type AuthFlowProviderProps, type CallbackStatus, type UseAuthCallbackResult, useAuthCallback, useAuthFlow, useUser, withAuthRequired };
package/dist/index.js ADDED
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ AuthFlowProvider: () => AuthFlowProvider,
24
+ useAuthCallback: () => useAuthCallback,
25
+ useAuthFlow: () => useAuthFlow,
26
+ useUser: () => useUser,
27
+ withAuthRequired: () => withAuthRequired
28
+ });
29
+ module.exports = __toCommonJS(index_exports);
30
+
31
+ // src/AuthFlowContext.tsx
32
+ var import_react = require("react");
33
+ var import_authflow_js = require("@meet_patel_03/authflow-js");
34
+ var import_jsx_runtime = require("react/jsx-runtime");
35
+ var AuthFlowContext = (0, import_react.createContext)(null);
36
+ var AuthFlowProvider = ({
37
+ config,
38
+ children
39
+ }) => {
40
+ const clientRef = (0, import_react.useRef)(null);
41
+ if (!clientRef.current) {
42
+ clientRef.current = new import_authflow_js.AuthFlowClient(config);
43
+ }
44
+ const client = clientRef.current;
45
+ const [user, setUser] = (0, import_react.useState)(null);
46
+ const [isLoading, setIsLoading] = (0, import_react.useState)(true);
47
+ const [isAuthenticated, setIsAuthenticated] = (0, import_react.useState)(false);
48
+ (0, import_react.useEffect)(() => {
49
+ const initAuth = async () => {
50
+ try {
51
+ const token = await client.getAccessToken();
52
+ if (token) {
53
+ const currentUser = client.getUser();
54
+ setUser(currentUser);
55
+ setIsAuthenticated(true);
56
+ }
57
+ } catch {
58
+ } finally {
59
+ setIsLoading(false);
60
+ }
61
+ };
62
+ initAuth();
63
+ }, [client]);
64
+ const loginWithRedirect = (0, import_react.useCallback)(
65
+ (options) => client.loginWithRedirect(options),
66
+ [client]
67
+ );
68
+ const handleRedirectCallback = (0, import_react.useCallback)(async () => {
69
+ const tokens = await client.handleRedirectCallback();
70
+ setUser(client.getUser());
71
+ setIsAuthenticated(true);
72
+ return tokens;
73
+ }, [client]);
74
+ const getAccessToken = (0, import_react.useCallback)(() => client.getAccessToken(), [client]);
75
+ const logout = (0, import_react.useCallback)(
76
+ async (options) => {
77
+ await client.logout(options);
78
+ setUser(null);
79
+ setIsAuthenticated(false);
80
+ },
81
+ [client]
82
+ );
83
+ const value = {
84
+ client,
85
+ user,
86
+ isLoading,
87
+ isAuthenticated,
88
+ loginWithRedirect,
89
+ handleRedirectCallback,
90
+ getAccessToken,
91
+ logout
92
+ };
93
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AuthFlowContext.Provider, { value, children });
94
+ };
95
+ var useAuthFlow = () => {
96
+ const ctx = (0, import_react.useContext)(AuthFlowContext);
97
+ if (!ctx) {
98
+ throw new Error(
99
+ "useAuthFlow must be used inside <AuthFlowProvider>. Wrap your app: <AuthFlowProvider config={...}><App /></AuthFlowProvider>"
100
+ );
101
+ }
102
+ return ctx;
103
+ };
104
+ var useUser = () => useAuthFlow().user;
105
+ function withAuthRequired(Component, options = {}) {
106
+ const displayName = Component.displayName ?? Component.name ?? "Component";
107
+ const WithAuthRequired = (props) => {
108
+ const { isAuthenticated, isLoading, loginWithRedirect } = useAuthFlow();
109
+ (0, import_react.useEffect)(() => {
110
+ if (!isLoading && !isAuthenticated) {
111
+ loginWithRedirect();
112
+ }
113
+ }, [isLoading, isAuthenticated, loginWithRedirect]);
114
+ if (isLoading || !isAuthenticated) return null;
115
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Component, { ...props });
116
+ };
117
+ WithAuthRequired.displayName = `withAuthRequired(${displayName})`;
118
+ return WithAuthRequired;
119
+ }
120
+
121
+ // src/useAuthCallback.ts
122
+ var import_react2 = require("react");
123
+ var import_authflow_js2 = require("@meet_patel_03/authflow-js");
124
+ var useAuthCallback = (options = {}) => {
125
+ const { handleRedirectCallback } = useAuthFlow();
126
+ const [status, setStatus] = (0, import_react2.useState)("loading");
127
+ const [error, setError] = (0, import_react2.useState)(null);
128
+ const hasRun = (0, import_react2.useRef)(false);
129
+ (0, import_react2.useEffect)(() => {
130
+ if (hasRun.current) return;
131
+ hasRun.current = true;
132
+ handleRedirectCallback().then(() => {
133
+ setStatus("success");
134
+ options.onSuccess?.();
135
+ }).catch((err) => {
136
+ const message = err instanceof import_authflow_js2.AuthFlowError ? err.errorDescription ?? err.error : err instanceof Error ? err.message : "Unknown error during login callback.";
137
+ setStatus("error");
138
+ setError(message);
139
+ options.onError?.(message);
140
+ });
141
+ }, []);
142
+ return { status, error };
143
+ };
144
+ // Annotate the CommonJS export names for ESM import in node:
145
+ 0 && (module.exports = {
146
+ AuthFlowProvider,
147
+ useAuthCallback,
148
+ useAuthFlow,
149
+ useUser,
150
+ withAuthRequired
151
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,127 @@
1
+ // src/AuthFlowContext.tsx
2
+ import {
3
+ createContext,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useRef,
8
+ useState
9
+ } from "react";
10
+ import { AuthFlowClient } from "@meet_patel_03/authflow-js";
11
+ import { jsx } from "react/jsx-runtime";
12
+ var AuthFlowContext = createContext(null);
13
+ var AuthFlowProvider = ({
14
+ config,
15
+ children
16
+ }) => {
17
+ const clientRef = useRef(null);
18
+ if (!clientRef.current) {
19
+ clientRef.current = new AuthFlowClient(config);
20
+ }
21
+ const client = clientRef.current;
22
+ const [user, setUser] = useState(null);
23
+ const [isLoading, setIsLoading] = useState(true);
24
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
25
+ useEffect(() => {
26
+ const initAuth = async () => {
27
+ try {
28
+ const token = await client.getAccessToken();
29
+ if (token) {
30
+ const currentUser = client.getUser();
31
+ setUser(currentUser);
32
+ setIsAuthenticated(true);
33
+ }
34
+ } catch {
35
+ } finally {
36
+ setIsLoading(false);
37
+ }
38
+ };
39
+ initAuth();
40
+ }, [client]);
41
+ const loginWithRedirect = useCallback(
42
+ (options) => client.loginWithRedirect(options),
43
+ [client]
44
+ );
45
+ const handleRedirectCallback = useCallback(async () => {
46
+ const tokens = await client.handleRedirectCallback();
47
+ setUser(client.getUser());
48
+ setIsAuthenticated(true);
49
+ return tokens;
50
+ }, [client]);
51
+ const getAccessToken = useCallback(() => client.getAccessToken(), [client]);
52
+ const logout = useCallback(
53
+ async (options) => {
54
+ await client.logout(options);
55
+ setUser(null);
56
+ setIsAuthenticated(false);
57
+ },
58
+ [client]
59
+ );
60
+ const value = {
61
+ client,
62
+ user,
63
+ isLoading,
64
+ isAuthenticated,
65
+ loginWithRedirect,
66
+ handleRedirectCallback,
67
+ getAccessToken,
68
+ logout
69
+ };
70
+ return /* @__PURE__ */ jsx(AuthFlowContext.Provider, { value, children });
71
+ };
72
+ var useAuthFlow = () => {
73
+ const ctx = useContext(AuthFlowContext);
74
+ if (!ctx) {
75
+ throw new Error(
76
+ "useAuthFlow must be used inside <AuthFlowProvider>. Wrap your app: <AuthFlowProvider config={...}><App /></AuthFlowProvider>"
77
+ );
78
+ }
79
+ return ctx;
80
+ };
81
+ var useUser = () => useAuthFlow().user;
82
+ function withAuthRequired(Component, options = {}) {
83
+ const displayName = Component.displayName ?? Component.name ?? "Component";
84
+ const WithAuthRequired = (props) => {
85
+ const { isAuthenticated, isLoading, loginWithRedirect } = useAuthFlow();
86
+ useEffect(() => {
87
+ if (!isLoading && !isAuthenticated) {
88
+ loginWithRedirect();
89
+ }
90
+ }, [isLoading, isAuthenticated, loginWithRedirect]);
91
+ if (isLoading || !isAuthenticated) return null;
92
+ return /* @__PURE__ */ jsx(Component, { ...props });
93
+ };
94
+ WithAuthRequired.displayName = `withAuthRequired(${displayName})`;
95
+ return WithAuthRequired;
96
+ }
97
+
98
+ // src/useAuthCallback.ts
99
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
100
+ import { AuthFlowError } from "@meet_patel_03/authflow-js";
101
+ var useAuthCallback = (options = {}) => {
102
+ const { handleRedirectCallback } = useAuthFlow();
103
+ const [status, setStatus] = useState2("loading");
104
+ const [error, setError] = useState2(null);
105
+ const hasRun = useRef2(false);
106
+ useEffect2(() => {
107
+ if (hasRun.current) return;
108
+ hasRun.current = true;
109
+ handleRedirectCallback().then(() => {
110
+ setStatus("success");
111
+ options.onSuccess?.();
112
+ }).catch((err) => {
113
+ const message = err instanceof AuthFlowError ? err.errorDescription ?? err.error : err instanceof Error ? err.message : "Unknown error during login callback.";
114
+ setStatus("error");
115
+ setError(message);
116
+ options.onError?.(message);
117
+ });
118
+ }, []);
119
+ return { status, error };
120
+ };
121
+ export {
122
+ AuthFlowProvider,
123
+ useAuthCallback,
124
+ useAuthFlow,
125
+ useUser,
126
+ withAuthRequired
127
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@meet_patel_03/authflow-react",
3
+ "version": "0.1.0",
4
+ "description": "AuthFlow React SDK — hooks and provider for the Authorization Code + PKCE flow",
5
+ "main": "./dist/index.cjs",
6
+ "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "require": "./dist/index.cjs",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean --external react",
20
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch --external react",
21
+ "test": "vitest run"
22
+ },
23
+ "peerDependencies": {
24
+ "react": ">=18.0.0",
25
+ "@meet_patel_03/authflow-js": "^0.1.0"
26
+ },
27
+ "devDependencies": {
28
+ "@testing-library/react": "^14.0.0",
29
+ "@types/react": "^18.0.0",
30
+ "jsdom": "^24.0.0",
31
+ "tsup": "^8.0.0",
32
+ "typescript": "^5.0.0",
33
+ "vitest": "^1.0.0",
34
+ "@meet_patel_03/authflow-js": "file:../authflow-js"
35
+ },
36
+ "keywords": [
37
+ "authflow",
38
+ "oauth2",
39
+ "oidc",
40
+ "react",
41
+ "hooks",
42
+ "auth"
43
+ ],
44
+ "license": "MIT",
45
+ "sideEffects": false,
46
+ "author": "Meet Patel (https://github.com/Meet-Patel-12)"
47
+ }