@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 +606 -0
- package/dist/index.d.mts +84 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +151 -0
- package/dist/index.mjs +127 -0
- package/package.json +47 -0
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
|
+
```
|
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|