@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.md CHANGED
@@ -1,133 +1,783 @@
1
+ **English** | [한국어](./README.ko.md)
2
+
1
3
  # @authon/react
2
4
 
3
- React SDK for [Authon](https://authon.dev) — Provider, hooks, and pre-built components.
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
- ## Quick Start
17
+ ## Setup
18
+
19
+ Wrap your app with `<AuthonProvider>` at the root level:
16
20
 
17
21
  ```tsx
18
- import {
19
- AuthonProvider,
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
- <Header />
31
- <Main />
28
+ <Router />
32
29
  </AuthonProvider>
33
30
  );
34
31
  }
32
+ ```
35
33
 
36
- function Header() {
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
- function Main() {
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
- ## API Reference
38
+ ## Components
63
39
 
64
40
  ### `<AuthonProvider>`
65
41
 
66
- Wraps your app and provides auth context.
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: { primaryColorStart: '#7c3aed' },
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
- ### Hooks
64
+ | Prop | Type | Description |
65
+ |------|------|-------------|
66
+ | `publishableKey` | `string` | Your project's publishable key |
67
+ | `config` | `AuthonConfig` (optional) | Additional client configuration |
83
68
 
84
- #### `useAuthon()`
69
+ ---
85
70
 
86
- Returns the full auth context:
71
+ ### `<SignIn>`
87
72
 
88
- ```ts
89
- const {
90
- isSignedIn, // boolean
91
- isLoading, // boolean
92
- user, // AuthonUser | null
93
- signOut, // () => Promise<void>
94
- openSignIn, // () => Promise<void>
95
- openSignUp, // () => Promise<void>
96
- getToken, // () => string | null
97
- client, // Authon instance
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
- #### `useUser()`
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
- Shorthand for user data:
90
+ ---
104
91
 
105
- ```ts
106
- const { user, isLoading } = useUser();
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
- ### Components
106
+ | Prop | Type | Default | Description |
107
+ |------|------|---------|-------------|
108
+ | `mode` | `'popup' \| 'embedded'` | `'popup'` | Display mode |
109
+
110
+ ---
110
111
 
111
- | Component | Props | Description |
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
- ### `<Protect>` Example
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>You need admin access.</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)