@authon/react 0.2.1 → 0.3.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.ko.md +227 -0
- package/README.md +720 -70
- package/dist/index.cjs +2008 -157
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +174 -9
- package/dist/index.d.ts +174 -9
- package/dist/index.js +1994 -157
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,133 +1,783 @@
|
|
|
1
|
+
**English** | [한국어](./README.ko.md)
|
|
2
|
+
|
|
1
3
|
# @authon/react
|
|
2
4
|
|
|
3
|
-
React
|
|
5
|
+
React components and hooks for [Authon](https://authon.dev) authentication.
|
|
4
6
|
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
|
-
npm install @authon/react
|
|
10
|
+
npm install @authon/react @authon/js
|
|
9
11
|
# or
|
|
10
|
-
pnpm add @authon/react
|
|
12
|
+
pnpm add @authon/react @authon/js
|
|
11
13
|
```
|
|
12
14
|
|
|
13
15
|
Requires `react >= 18.0.0`.
|
|
14
16
|
|
|
15
|
-
##
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
Wrap your app with `<AuthonProvider>` at the root level:
|
|
16
20
|
|
|
17
21
|
```tsx
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
SignedIn,
|
|
21
|
-
SignedOut,
|
|
22
|
-
UserButton,
|
|
23
|
-
useUser,
|
|
24
|
-
useAuthon,
|
|
25
|
-
} from '@authon/react';
|
|
22
|
+
// src/main.tsx
|
|
23
|
+
import { AuthonProvider } from '@authon/react';
|
|
26
24
|
|
|
27
25
|
function App() {
|
|
28
26
|
return (
|
|
29
27
|
<AuthonProvider publishableKey="pk_live_...">
|
|
30
|
-
<
|
|
31
|
-
<Main />
|
|
28
|
+
<Router />
|
|
32
29
|
</AuthonProvider>
|
|
33
30
|
);
|
|
34
31
|
}
|
|
32
|
+
```
|
|
35
33
|
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<nav>
|
|
39
|
-
<SignedIn>
|
|
40
|
-
<UserButton />
|
|
41
|
-
</SignedIn>
|
|
42
|
-
<SignedOut>
|
|
43
|
-
<SignInButton />
|
|
44
|
-
</SignedOut>
|
|
45
|
-
</nav>
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function SignInButton() {
|
|
50
|
-
const { openSignIn } = useAuthon();
|
|
51
|
-
return <button onClick={() => openSignIn()}>Sign In</button>;
|
|
52
|
-
}
|
|
34
|
+
Get your publishable key from the [Authon Dashboard](https://authon.dev/dashboard).
|
|
53
35
|
|
|
54
|
-
|
|
55
|
-
const { user, isLoading } = useUser();
|
|
56
|
-
if (isLoading) return <p>Loading...</p>;
|
|
57
|
-
if (!user) return <p>Please sign in.</p>;
|
|
58
|
-
return <h1>Welcome, {user.displayName}</h1>;
|
|
59
|
-
}
|
|
60
|
-
```
|
|
36
|
+
---
|
|
61
37
|
|
|
62
|
-
##
|
|
38
|
+
## Components
|
|
63
39
|
|
|
64
40
|
### `<AuthonProvider>`
|
|
65
41
|
|
|
66
|
-
|
|
42
|
+
Initializes the Authon client and provides auth context to your entire component tree. Must wrap all other Authon components and hooks.
|
|
67
43
|
|
|
68
44
|
```tsx
|
|
45
|
+
import { AuthonProvider } from '@authon/react';
|
|
46
|
+
|
|
69
47
|
<AuthonProvider
|
|
70
48
|
publishableKey="pk_live_..."
|
|
71
49
|
config={{
|
|
72
50
|
apiUrl: 'https://api.authon.dev',
|
|
73
51
|
theme: 'auto',
|
|
74
52
|
locale: 'en',
|
|
75
|
-
appearance: {
|
|
53
|
+
appearance: {
|
|
54
|
+
primaryColorStart: '#7c3aed',
|
|
55
|
+
primaryColorEnd: '#4f46e5',
|
|
56
|
+
borderRadius: 12,
|
|
57
|
+
},
|
|
76
58
|
}}
|
|
77
59
|
>
|
|
78
60
|
{children}
|
|
79
61
|
</AuthonProvider>
|
|
80
62
|
```
|
|
81
63
|
|
|
82
|
-
|
|
64
|
+
| Prop | Type | Description |
|
|
65
|
+
|------|------|-------------|
|
|
66
|
+
| `publishableKey` | `string` | Your project's publishable key |
|
|
67
|
+
| `config` | `AuthonConfig` (optional) | Additional client configuration |
|
|
83
68
|
|
|
84
|
-
|
|
69
|
+
---
|
|
85
70
|
|
|
86
|
-
|
|
71
|
+
### `<SignIn>`
|
|
87
72
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
} = useAuthon();
|
|
73
|
+
Opens the sign-in modal or renders an embedded sign-in form.
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import { SignIn } from '@authon/react';
|
|
77
|
+
|
|
78
|
+
// Popup mode (default) — opens the modal immediately on mount
|
|
79
|
+
<SignIn mode="popup" />
|
|
80
|
+
|
|
81
|
+
// Embedded mode — renders a container div for the hosted form
|
|
82
|
+
<SignIn mode="embedded" />
|
|
99
83
|
```
|
|
100
84
|
|
|
101
|
-
|
|
85
|
+
| Prop | Type | Default | Description |
|
|
86
|
+
|------|------|---------|-------------|
|
|
87
|
+
| `mode` | `'popup' \| 'embedded'` | `'popup'` | Display mode |
|
|
88
|
+
| `redirectUrl` | `string` (optional) | — | URL to redirect after sign-in |
|
|
102
89
|
|
|
103
|
-
|
|
90
|
+
---
|
|
104
91
|
|
|
105
|
-
|
|
106
|
-
|
|
92
|
+
### `<SignUp>`
|
|
93
|
+
|
|
94
|
+
Opens the sign-up modal or renders an embedded sign-up form.
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { SignUp } from '@authon/react';
|
|
98
|
+
|
|
99
|
+
// Popup mode (default)
|
|
100
|
+
<SignUp mode="popup" />
|
|
101
|
+
|
|
102
|
+
// Embedded mode
|
|
103
|
+
<SignUp mode="embedded" />
|
|
107
104
|
```
|
|
108
105
|
|
|
109
|
-
|
|
106
|
+
| Prop | Type | Default | Description |
|
|
107
|
+
|------|------|---------|-------------|
|
|
108
|
+
| `mode` | `'popup' \| 'embedded'` | `'popup'` | Display mode |
|
|
109
|
+
|
|
110
|
+
---
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
|-----------|-------|-------------|
|
|
113
|
-
| `<SignedIn>` | `children` | Renders children only when signed in |
|
|
114
|
-
| `<SignedOut>` | `children` | Renders children only when signed out |
|
|
115
|
-
| `<UserButton>` | none | Avatar dropdown with sign-out action |
|
|
116
|
-
| `<SignIn>` | `mode?` | Opens sign-in modal or renders embedded form |
|
|
117
|
-
| `<SignUp>` | `mode?` | Opens sign-up modal or renders embedded form |
|
|
118
|
-
| `<Protect>` | `fallback?`, `condition?` | Guards content, optionally with a custom condition |
|
|
112
|
+
### `<UserButton>`
|
|
119
113
|
|
|
120
|
-
|
|
114
|
+
Displays a user avatar button. When clicked, opens a dropdown with the user's name, email, and a sign-out option. When the user is signed out, renders a "Sign In" button instead.
|
|
121
115
|
|
|
122
116
|
```tsx
|
|
117
|
+
import { UserButton } from '@authon/react';
|
|
118
|
+
|
|
119
|
+
function Navbar() {
|
|
120
|
+
return (
|
|
121
|
+
<nav>
|
|
122
|
+
<UserButton />
|
|
123
|
+
</nav>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
No props required.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### `<SignedIn>`
|
|
133
|
+
|
|
134
|
+
Renders `children` only when a user is signed in. Renders nothing while auth is loading.
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
import { SignedIn } from '@authon/react';
|
|
138
|
+
|
|
139
|
+
<SignedIn>
|
|
140
|
+
<Dashboard />
|
|
141
|
+
</SignedIn>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
### `<SignedOut>`
|
|
147
|
+
|
|
148
|
+
Renders `children` only when no user is signed in. Renders nothing while auth is loading.
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import { SignedOut } from '@authon/react';
|
|
152
|
+
|
|
153
|
+
<SignedOut>
|
|
154
|
+
<a href="/sign-in">Sign In</a>
|
|
155
|
+
</SignedOut>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### `<Protect>`
|
|
161
|
+
|
|
162
|
+
Guards content based on authentication status and an optional custom condition. Useful for role-based access control.
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
import { Protect } from '@authon/react';
|
|
166
|
+
|
|
167
|
+
// Require sign-in only
|
|
168
|
+
<Protect fallback={<p>Please sign in to continue.</p>}>
|
|
169
|
+
<PrivatePage />
|
|
170
|
+
</Protect>
|
|
171
|
+
|
|
172
|
+
// Require a specific role
|
|
123
173
|
<Protect
|
|
124
|
-
fallback={<p>
|
|
174
|
+
fallback={<p>Admin access required.</p>}
|
|
125
175
|
condition={(user) => user.publicMetadata?.role === 'admin'}
|
|
126
176
|
>
|
|
127
177
|
<AdminPanel />
|
|
128
178
|
</Protect>
|
|
129
179
|
```
|
|
130
180
|
|
|
181
|
+
| Prop | Type | Description |
|
|
182
|
+
|------|------|-------------|
|
|
183
|
+
| `children` | `ReactNode` | Content to render when access is granted |
|
|
184
|
+
| `fallback` | `ReactNode` (optional) | Content to render when access is denied |
|
|
185
|
+
| `condition` | `(user: AuthonUser) => boolean` (optional) | Additional check beyond sign-in status |
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### `<SocialButton>`
|
|
190
|
+
|
|
191
|
+
A single OAuth provider button with built-in styles, icons, and loading state.
|
|
192
|
+
|
|
193
|
+
```tsx
|
|
194
|
+
import { SocialButton } from '@authon/react';
|
|
195
|
+
|
|
196
|
+
function SocialLogin() {
|
|
197
|
+
const { client } = useAuthon();
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
|
201
|
+
<SocialButton
|
|
202
|
+
provider="google"
|
|
203
|
+
onClick={async (provider) => {
|
|
204
|
+
await client!.signInWithOAuth(provider);
|
|
205
|
+
}}
|
|
206
|
+
/>
|
|
207
|
+
<SocialButton
|
|
208
|
+
provider="github"
|
|
209
|
+
onClick={async (provider) => {
|
|
210
|
+
await client!.signInWithOAuth(provider);
|
|
211
|
+
}}
|
|
212
|
+
compact
|
|
213
|
+
size={48}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
| Prop | Type | Default | Description |
|
|
221
|
+
|------|------|---------|-------------|
|
|
222
|
+
| `provider` | `OAuthProviderType` | — | OAuth provider identifier |
|
|
223
|
+
| `onClick` | `(provider: OAuthProviderType) => void \| Promise<void>` | — | Click handler |
|
|
224
|
+
| `loading` | `boolean` | `false` | Show spinner |
|
|
225
|
+
| `disabled` | `boolean` | `false` | Disable button |
|
|
226
|
+
| `label` | `string` (optional) | `"Continue with {Provider}"` | Override button label |
|
|
227
|
+
| `compact` | `boolean` | `false` | Icon-only square button |
|
|
228
|
+
| `className` | `string` (optional) | — | Custom class |
|
|
229
|
+
| `style` | `CSSProperties` (optional) | — | Custom inline style |
|
|
230
|
+
| `iconSize` | `number` (optional) | `20` (`24` in compact) | Icon size in px |
|
|
231
|
+
| `borderRadius` | `number` | `10` | Border radius in px |
|
|
232
|
+
| `height` | `number` | `48` | Button height in px (full mode) |
|
|
233
|
+
| `size` | `number` | `48` | Button size in px (compact mode) |
|
|
234
|
+
|
|
235
|
+
Supported `OAuthProviderType` values: `'google' | 'apple' | 'kakao' | 'naver' | 'facebook' | 'github' | 'discord' | 'x' | 'line' | 'microsoft'`
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
### `<SocialButtons>`
|
|
240
|
+
|
|
241
|
+
Automatically fetches your project's enabled OAuth providers and renders a list of `<SocialButton>` components. Handles OAuth sign-in flow internally.
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
import { SocialButtons } from '@authon/react';
|
|
245
|
+
|
|
246
|
+
// Vertical list (default)
|
|
247
|
+
<SocialButtons
|
|
248
|
+
onSuccess={() => console.log('Signed in!')}
|
|
249
|
+
onError={(error) => console.error(error)}
|
|
250
|
+
/>
|
|
251
|
+
|
|
252
|
+
// Compact icon row
|
|
253
|
+
<SocialButtons
|
|
254
|
+
compact
|
|
255
|
+
gap={12}
|
|
256
|
+
labels={{ google: 'Sign in with Google', kakao: '카카오로 로그인' }}
|
|
257
|
+
buttonProps={{ borderRadius: 8 }}
|
|
258
|
+
/>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
| Prop | Type | Default | Description |
|
|
262
|
+
|------|------|---------|-------------|
|
|
263
|
+
| `onSuccess` | `() => void` (optional) | — | Called after successful OAuth sign-in |
|
|
264
|
+
| `onError` | `(error: Error) => void` (optional) | — | Called on OAuth error |
|
|
265
|
+
| `className` | `string` (optional) | — | Container class |
|
|
266
|
+
| `style` | `CSSProperties` (optional) | — | Container style |
|
|
267
|
+
| `gap` | `number` (optional) | `10` (`12` compact) | Gap between buttons in px |
|
|
268
|
+
| `compact` | `boolean` | `false` | Render icon-only buttons in a row |
|
|
269
|
+
| `labels` | `Partial<Record<OAuthProviderType, string>>` (optional) | — | Custom label per provider |
|
|
270
|
+
| `buttonProps` | `Partial<SocialButtonProps>` (optional) | — | Props forwarded to each `<SocialButton>` |
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Hooks
|
|
275
|
+
|
|
276
|
+
### `useAuthon()`
|
|
277
|
+
|
|
278
|
+
Returns the full auth context. Throws if used outside `<AuthonProvider>`.
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
import { useAuthon } from '@authon/react';
|
|
282
|
+
|
|
283
|
+
function ProfileButton() {
|
|
284
|
+
const { isSignedIn, isLoading, user, signOut, openSignIn, openSignUp, getToken, client } = useAuthon();
|
|
285
|
+
|
|
286
|
+
if (isLoading) return <span>Loading...</span>;
|
|
287
|
+
|
|
288
|
+
if (!isSignedIn) {
|
|
289
|
+
return <button onClick={() => openSignIn()}>Sign In</button>;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<div>
|
|
294
|
+
<span>Hello, {user?.displayName}</span>
|
|
295
|
+
<button onClick={() => signOut()}>Sign Out</button>
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Return type:**
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
interface AuthonContextValue {
|
|
305
|
+
isSignedIn: boolean;
|
|
306
|
+
isLoading: boolean;
|
|
307
|
+
user: AuthonUser | null;
|
|
308
|
+
signOut: () => Promise<void>;
|
|
309
|
+
openSignIn: () => Promise<void>;
|
|
310
|
+
openSignUp: () => Promise<void>;
|
|
311
|
+
getToken: () => string | null;
|
|
312
|
+
client: Authon | null;
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
### `useUser()`
|
|
319
|
+
|
|
320
|
+
Shorthand hook that returns only the current user and loading state.
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
import { useUser } from '@authon/react';
|
|
324
|
+
|
|
325
|
+
function WelcomeBanner() {
|
|
326
|
+
const { user, isLoading } = useUser();
|
|
327
|
+
|
|
328
|
+
if (isLoading) return <p>Loading...</p>;
|
|
329
|
+
if (!user) return null;
|
|
330
|
+
|
|
331
|
+
return <h2>Welcome back, {user.displayName ?? user.email}!</h2>;
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
**Return type:**
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
{
|
|
339
|
+
user: AuthonUser | null;
|
|
340
|
+
isLoading: boolean;
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
### `useAuthonMfa()`
|
|
347
|
+
|
|
348
|
+
Manages TOTP-based multi-factor authentication (Google Authenticator, Authy, etc.).
|
|
349
|
+
|
|
350
|
+
```tsx
|
|
351
|
+
import { useAuthonMfa } from '@authon/react';
|
|
352
|
+
import { useState } from 'react';
|
|
353
|
+
|
|
354
|
+
function MfaSetupPage() {
|
|
355
|
+
const { setupMfa, verifyMfaSetup, disableMfa, getMfaStatus, regenerateBackupCodes, isLoading, error } = useAuthonMfa();
|
|
356
|
+
const [qrSvg, setQrSvg] = useState('');
|
|
357
|
+
const [backupCodes, setBackupCodes] = useState<string[]>([]);
|
|
358
|
+
const [code, setCode] = useState('');
|
|
359
|
+
|
|
360
|
+
const handleSetup = async () => {
|
|
361
|
+
const result = await setupMfa();
|
|
362
|
+
if (result) {
|
|
363
|
+
setQrSvg(result.qrCodeSvg);
|
|
364
|
+
setBackupCodes(result.backupCodes);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const handleVerify = async () => {
|
|
369
|
+
const success = await verifyMfaSetup(code);
|
|
370
|
+
if (success) alert('MFA enabled successfully!');
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
const handleCheckStatus = async () => {
|
|
374
|
+
const status = await getMfaStatus();
|
|
375
|
+
console.log('MFA enabled:', status?.enabled, 'Backup codes left:', status?.backupCodesRemaining);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<div>
|
|
380
|
+
<button onClick={handleSetup} disabled={isLoading}>
|
|
381
|
+
Enable MFA
|
|
382
|
+
</button>
|
|
383
|
+
|
|
384
|
+
{qrSvg && (
|
|
385
|
+
<>
|
|
386
|
+
<div dangerouslySetInnerHTML={{ __html: qrSvg }} />
|
|
387
|
+
<p>Backup codes: {backupCodes.join(', ')}</p>
|
|
388
|
+
<input
|
|
389
|
+
value={code}
|
|
390
|
+
onChange={(e) => setCode(e.target.value)}
|
|
391
|
+
placeholder="Enter 6-digit code"
|
|
392
|
+
/>
|
|
393
|
+
<button onClick={handleVerify} disabled={isLoading}>
|
|
394
|
+
Verify & Enable
|
|
395
|
+
</button>
|
|
396
|
+
</>
|
|
397
|
+
)}
|
|
398
|
+
|
|
399
|
+
{error && <p style={{ color: 'red' }}>{error.message}</p>}
|
|
400
|
+
</div>
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**MFA sign-in flow (verifying TOTP after password):**
|
|
406
|
+
|
|
407
|
+
```tsx
|
|
408
|
+
import { useAuthon, useAuthonMfa } from '@authon/react';
|
|
409
|
+
import { AuthonMfaRequiredError } from '@authon/js';
|
|
410
|
+
|
|
411
|
+
function LoginForm() {
|
|
412
|
+
const { client } = useAuthon();
|
|
413
|
+
const { verifyMfa, isLoading } = useAuthonMfa();
|
|
414
|
+
const [mfaToken, setMfaToken] = useState('');
|
|
415
|
+
const [mfaStep, setMfaStep] = useState(false);
|
|
416
|
+
|
|
417
|
+
const handleSignIn = async (email: string, password: string) => {
|
|
418
|
+
try {
|
|
419
|
+
await client!.signInWithEmail(email, password);
|
|
420
|
+
// signed in — no MFA required
|
|
421
|
+
} catch (err) {
|
|
422
|
+
if (err instanceof AuthonMfaRequiredError) {
|
|
423
|
+
setMfaToken(err.mfaToken);
|
|
424
|
+
setMfaStep(true);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const handleMfa = async (code: string) => {
|
|
430
|
+
const success = await verifyMfa(mfaToken, code);
|
|
431
|
+
if (success) console.log('Signed in with MFA!');
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// ...
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**Return type:**
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
interface UseAuthonMfaReturn {
|
|
442
|
+
setupMfa: () => Promise<(MfaSetupResponse & { qrCodeSvg: string }) | null>;
|
|
443
|
+
verifyMfaSetup: (code: string) => Promise<boolean>;
|
|
444
|
+
verifyMfa: (mfaToken: string, code: string) => Promise<boolean>;
|
|
445
|
+
disableMfa: (code: string) => Promise<boolean>;
|
|
446
|
+
getMfaStatus: () => Promise<MfaStatus | null>;
|
|
447
|
+
regenerateBackupCodes: (code: string) => Promise<string[] | null>;
|
|
448
|
+
isLoading: boolean;
|
|
449
|
+
error: Error | null;
|
|
450
|
+
}
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
### `useAuthonPasskeys()`
|
|
456
|
+
|
|
457
|
+
Manages WebAuthn passkey registration and authentication.
|
|
458
|
+
|
|
459
|
+
```tsx
|
|
460
|
+
import { useAuthonPasskeys } from '@authon/react';
|
|
461
|
+
|
|
462
|
+
function PasskeySettings() {
|
|
463
|
+
const {
|
|
464
|
+
registerPasskey,
|
|
465
|
+
authenticateWithPasskey,
|
|
466
|
+
listPasskeys,
|
|
467
|
+
renamePasskey,
|
|
468
|
+
revokePasskey,
|
|
469
|
+
isLoading,
|
|
470
|
+
error,
|
|
471
|
+
} = useAuthonPasskeys();
|
|
472
|
+
|
|
473
|
+
const handleRegister = async () => {
|
|
474
|
+
const passkey = await registerPasskey('My MacBook');
|
|
475
|
+
if (passkey) {
|
|
476
|
+
console.log('Passkey registered:', passkey.id);
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const handleList = async () => {
|
|
481
|
+
const passkeys = await listPasskeys();
|
|
482
|
+
console.log('Registered passkeys:', passkeys);
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const handleRevoke = async (id: string) => {
|
|
486
|
+
const success = await revokePasskey(id);
|
|
487
|
+
if (success) console.log('Passkey revoked');
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
return (
|
|
491
|
+
<div>
|
|
492
|
+
<button onClick={handleRegister} disabled={isLoading}>
|
|
493
|
+
Add Passkey
|
|
494
|
+
</button>
|
|
495
|
+
<button onClick={handleList} disabled={isLoading}>
|
|
496
|
+
List Passkeys
|
|
497
|
+
</button>
|
|
498
|
+
{error && <p style={{ color: 'red' }}>{error.message}</p>}
|
|
499
|
+
</div>
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**Return type:**
|
|
505
|
+
|
|
506
|
+
```ts
|
|
507
|
+
interface UseAuthonPasskeysReturn {
|
|
508
|
+
registerPasskey: (name?: string) => Promise<PasskeyCredential | null>;
|
|
509
|
+
authenticateWithPasskey: (email?: string) => Promise<boolean>;
|
|
510
|
+
listPasskeys: () => Promise<PasskeyCredential[] | null>;
|
|
511
|
+
renamePasskey: (id: string, name: string) => Promise<PasskeyCredential | null>;
|
|
512
|
+
revokePasskey: (id: string) => Promise<boolean>;
|
|
513
|
+
isLoading: boolean;
|
|
514
|
+
error: Error | null;
|
|
515
|
+
}
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
---
|
|
519
|
+
|
|
520
|
+
### `useAuthonPasswordless()`
|
|
521
|
+
|
|
522
|
+
Handles magic link and email OTP (one-time password) authentication flows.
|
|
523
|
+
|
|
524
|
+
```tsx
|
|
525
|
+
import { useAuthonPasswordless } from '@authon/react';
|
|
526
|
+
import { useState } from 'react';
|
|
527
|
+
|
|
528
|
+
function PasswordlessLogin() {
|
|
529
|
+
const { sendMagicLink, sendEmailOtp, verifyPasswordless, isLoading, error } = useAuthonPasswordless();
|
|
530
|
+
const [email, setEmail] = useState('');
|
|
531
|
+
const [otpSent, setOtpSent] = useState(false);
|
|
532
|
+
const [code, setCode] = useState('');
|
|
533
|
+
|
|
534
|
+
const handleSendOtp = async () => {
|
|
535
|
+
const success = await sendEmailOtp(email);
|
|
536
|
+
if (success) setOtpSent(true);
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
const handleVerifyOtp = async () => {
|
|
540
|
+
const success = await verifyPasswordless({ email, code });
|
|
541
|
+
if (success) console.log('Signed in!');
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
const handleMagicLink = async () => {
|
|
545
|
+
const success = await sendMagicLink(email);
|
|
546
|
+
if (success) alert('Check your email for a sign-in link!');
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// Verify magic link token from URL
|
|
550
|
+
const handleTokenVerify = async (token: string) => {
|
|
551
|
+
const success = await verifyPasswordless({ token });
|
|
552
|
+
if (success) console.log('Signed in via magic link!');
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
return (
|
|
556
|
+
<div>
|
|
557
|
+
<input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
|
|
558
|
+
<button onClick={handleMagicLink} disabled={isLoading}>Send Magic Link</button>
|
|
559
|
+
<button onClick={handleSendOtp} disabled={isLoading}>Send OTP</button>
|
|
560
|
+
|
|
561
|
+
{otpSent && (
|
|
562
|
+
<>
|
|
563
|
+
<input value={code} onChange={(e) => setCode(e.target.value)} placeholder="Enter code" />
|
|
564
|
+
<button onClick={handleVerifyOtp} disabled={isLoading}>Verify</button>
|
|
565
|
+
</>
|
|
566
|
+
)}
|
|
567
|
+
|
|
568
|
+
{error && <p style={{ color: 'red' }}>{error.message}</p>}
|
|
569
|
+
</div>
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
**Return type:**
|
|
575
|
+
|
|
576
|
+
```ts
|
|
577
|
+
interface UseAuthonPasswordlessReturn {
|
|
578
|
+
sendMagicLink: (email: string) => Promise<boolean>;
|
|
579
|
+
sendEmailOtp: (email: string) => Promise<boolean>;
|
|
580
|
+
verifyPasswordless: (opts: { token?: string; email?: string; code?: string }) => Promise<boolean>;
|
|
581
|
+
isLoading: boolean;
|
|
582
|
+
error: Error | null;
|
|
583
|
+
}
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
588
|
+
### `useAuthonWeb3()`
|
|
589
|
+
|
|
590
|
+
Handles Web3 wallet authentication (Sign-In with Ethereum / Solana).
|
|
591
|
+
|
|
592
|
+
```tsx
|
|
593
|
+
import { useAuthonWeb3 } from '@authon/react';
|
|
594
|
+
|
|
595
|
+
function Web3Login() {
|
|
596
|
+
const { getNonce, verify, listWallets, linkWallet, unlinkWallet, isLoading, error } = useAuthonWeb3();
|
|
597
|
+
|
|
598
|
+
const handleSignIn = async () => {
|
|
599
|
+
const address = '0xYourAddress';
|
|
600
|
+
|
|
601
|
+
// 1. Get a nonce to sign
|
|
602
|
+
const nonceResponse = await getNonce(address, 'evm', 'metamask', 1);
|
|
603
|
+
if (!nonceResponse) return;
|
|
604
|
+
|
|
605
|
+
// 2. Sign the message with your wallet (e.g. MetaMask)
|
|
606
|
+
const signature = await window.ethereum.request({
|
|
607
|
+
method: 'personal_sign',
|
|
608
|
+
params: [nonceResponse.message, address],
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
// 3. Verify the signature to sign in
|
|
612
|
+
const success = await verify(nonceResponse.message, signature, address, 'evm', 'metamask');
|
|
613
|
+
if (success) console.log('Signed in with wallet!');
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const handleLinkWallet = async () => {
|
|
617
|
+
const address = '0xYourAddress';
|
|
618
|
+
const nonceResponse = await getNonce(address, 'evm', 'metamask');
|
|
619
|
+
if (!nonceResponse) return;
|
|
620
|
+
|
|
621
|
+
const signature = await window.ethereum.request({
|
|
622
|
+
method: 'personal_sign',
|
|
623
|
+
params: [nonceResponse.message, address],
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
const wallet = await linkWallet({
|
|
627
|
+
address,
|
|
628
|
+
chain: 'evm',
|
|
629
|
+
walletType: 'metamask',
|
|
630
|
+
message: nonceResponse.message,
|
|
631
|
+
signature,
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
if (wallet) console.log('Wallet linked:', wallet.id);
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const handleListWallets = async () => {
|
|
638
|
+
const wallets = await listWallets();
|
|
639
|
+
console.log('Linked wallets:', wallets);
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
return (
|
|
643
|
+
<div>
|
|
644
|
+
<button onClick={handleSignIn} disabled={isLoading}>Sign In with MetaMask</button>
|
|
645
|
+
<button onClick={handleLinkWallet} disabled={isLoading}>Link Wallet</button>
|
|
646
|
+
<button onClick={handleListWallets} disabled={isLoading}>List Wallets</button>
|
|
647
|
+
{error && <p style={{ color: 'red' }}>{error.message}</p>}
|
|
648
|
+
</div>
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
**Return type:**
|
|
654
|
+
|
|
655
|
+
```ts
|
|
656
|
+
interface UseAuthonWeb3Return {
|
|
657
|
+
getNonce: (
|
|
658
|
+
address: string,
|
|
659
|
+
chain: Web3Chain,
|
|
660
|
+
walletType: Web3WalletType,
|
|
661
|
+
chainId?: number,
|
|
662
|
+
) => Promise<Web3NonceResponse | null>;
|
|
663
|
+
verify: (
|
|
664
|
+
message: string,
|
|
665
|
+
signature: string,
|
|
666
|
+
address: string,
|
|
667
|
+
chain: Web3Chain,
|
|
668
|
+
walletType: Web3WalletType,
|
|
669
|
+
) => Promise<boolean>;
|
|
670
|
+
listWallets: () => Promise<Web3Wallet[] | null>;
|
|
671
|
+
linkWallet: (params: LinkWalletParams) => Promise<Web3Wallet | null>;
|
|
672
|
+
unlinkWallet: (walletId: string) => Promise<boolean>;
|
|
673
|
+
isLoading: boolean;
|
|
674
|
+
error: Error | null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
interface LinkWalletParams {
|
|
678
|
+
address: string;
|
|
679
|
+
chain: Web3Chain;
|
|
680
|
+
walletType: Web3WalletType;
|
|
681
|
+
chainId?: number;
|
|
682
|
+
message: string;
|
|
683
|
+
signature: string;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
type Web3Chain = 'evm' | 'solana';
|
|
687
|
+
type Web3WalletType = 'metamask' | 'pexus' | 'walletconnect' | 'coinbase' | 'phantom' | 'trust' | 'other';
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
### `useAuthonSessions()`
|
|
693
|
+
|
|
694
|
+
Lists and revokes active user sessions.
|
|
695
|
+
|
|
696
|
+
```tsx
|
|
697
|
+
import { useAuthonSessions } from '@authon/react';
|
|
698
|
+
import { useEffect, useState } from 'react';
|
|
699
|
+
import type { SessionInfo } from '@authon/shared';
|
|
700
|
+
|
|
701
|
+
function SessionManager() {
|
|
702
|
+
const { listSessions, revokeSession, isLoading, error } = useAuthonSessions();
|
|
703
|
+
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
|
704
|
+
|
|
705
|
+
useEffect(() => {
|
|
706
|
+
listSessions().then((s) => {
|
|
707
|
+
if (s) setSessions(s);
|
|
708
|
+
});
|
|
709
|
+
}, []);
|
|
710
|
+
|
|
711
|
+
const handleRevoke = async (sessionId: string) => {
|
|
712
|
+
const success = await revokeSession(sessionId);
|
|
713
|
+
if (success) {
|
|
714
|
+
setSessions((prev) => prev.filter((s) => s.id !== sessionId));
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
if (isLoading) return <p>Loading sessions...</p>;
|
|
719
|
+
|
|
720
|
+
return (
|
|
721
|
+
<ul>
|
|
722
|
+
{sessions.map((session) => (
|
|
723
|
+
<li key={session.id}>
|
|
724
|
+
<span>{session.userAgent ?? 'Unknown device'}</span>
|
|
725
|
+
<span>{session.ipAddress}</span>
|
|
726
|
+
<button onClick={() => handleRevoke(session.id)}>Revoke</button>
|
|
727
|
+
</li>
|
|
728
|
+
))}
|
|
729
|
+
{error && <p style={{ color: 'red' }}>{error.message}</p>}
|
|
730
|
+
</ul>
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
**Return type:**
|
|
736
|
+
|
|
737
|
+
```ts
|
|
738
|
+
interface UseAuthonSessionsReturn {
|
|
739
|
+
listSessions: () => Promise<SessionInfo[] | null>;
|
|
740
|
+
revokeSession: (sessionId: string) => Promise<boolean>;
|
|
741
|
+
isLoading: boolean;
|
|
742
|
+
error: Error | null;
|
|
743
|
+
}
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
---
|
|
747
|
+
|
|
748
|
+
## TypeScript Types
|
|
749
|
+
|
|
750
|
+
Key types exported from `@authon/react` and `@authon/shared`:
|
|
751
|
+
|
|
752
|
+
```ts
|
|
753
|
+
import type {
|
|
754
|
+
AuthonContextValue,
|
|
755
|
+
SocialButtonProps,
|
|
756
|
+
SocialButtonsProps,
|
|
757
|
+
UseAuthonMfaReturn,
|
|
758
|
+
UseAuthonPasskeysReturn,
|
|
759
|
+
UseAuthonPasswordlessReturn,
|
|
760
|
+
UseAuthonWeb3Return,
|
|
761
|
+
LinkWalletParams,
|
|
762
|
+
UseAuthonSessionsReturn,
|
|
763
|
+
} from '@authon/react';
|
|
764
|
+
|
|
765
|
+
import type {
|
|
766
|
+
AuthonUser,
|
|
767
|
+
SessionInfo,
|
|
768
|
+
PasskeyCredential,
|
|
769
|
+
Web3Wallet,
|
|
770
|
+
Web3NonceResponse,
|
|
771
|
+
MfaSetupResponse,
|
|
772
|
+
MfaStatus,
|
|
773
|
+
OAuthProviderType,
|
|
774
|
+
Web3Chain,
|
|
775
|
+
Web3WalletType,
|
|
776
|
+
} from '@authon/shared';
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
---
|
|
780
|
+
|
|
131
781
|
## Documentation
|
|
132
782
|
|
|
133
783
|
[authon.dev/docs](https://authon.dev/docs)
|