@authon/react 0.3.0 → 0.3.2

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
@@ -2,786 +2,217 @@
2
2
 
3
3
  # @authon/react
4
4
 
5
- React components and hooks for [Authon](https://authon.dev) authentication.
5
+ > Drop-in React authentication with hooks and components — Auth0 alternative, open-source auth
6
6
 
7
- ## Install
8
-
9
- ```bash
10
- npm install @authon/react @authon/js
11
- # or
12
- pnpm add @authon/react @authon/js
13
- ```
14
-
15
- Requires `react >= 18.0.0`.
16
-
17
- ## Setup
18
-
19
- Wrap your app with `<AuthonProvider>` at the root level:
20
-
21
- ```tsx
22
- // src/main.tsx
23
- import { AuthonProvider } from '@authon/react';
24
-
25
- function App() {
26
- return (
27
- <AuthonProvider publishableKey="pk_live_...">
28
- <Router />
29
- </AuthonProvider>
30
- );
31
- }
32
- ```
33
-
34
- Get your publishable key from the [Authon Dashboard](https://authon.dev/dashboard).
35
-
36
- ---
7
+ [![npm version](https://img.shields.io/npm/v/@authon/react?color=6d28d9)](https://www.npmjs.com/package/@authon/react)
8
+ [![License](https://img.shields.io/badge/license-MIT-blue)](../../LICENSE)
37
9
 
38
- ## Components
10
+ ## Prerequisites
39
11
 
40
- ### `<AuthonProvider>`
12
+ Before installing the SDK, create an Authon project and get your API keys:
41
13
 
42
- Initializes the Authon client and provides auth context to your entire component tree. Must wrap all other Authon components and hooks.
43
-
44
- ```tsx
45
- import { AuthonProvider } from '@authon/react';
46
-
47
- <AuthonProvider
48
- publishableKey="pk_live_..."
49
- config={{
50
- apiUrl: 'https://api.authon.dev',
51
- theme: 'auto',
52
- locale: 'en',
53
- appearance: {
54
- primaryColorStart: '#7c3aed',
55
- primaryColorEnd: '#4f46e5',
56
- borderRadius: 12,
57
- },
58
- }}
59
- >
60
- {children}
61
- </AuthonProvider>
62
- ```
14
+ 1. **Create a project** at [Authon Dashboard](https://authon.dev/dashboard/overview)
15
+ - Click "Create Project" and enter your app name
16
+ - Select the authentication methods you want (Email/Password, OAuth providers, etc.)
63
17
 
64
- | Prop | Type | Description |
65
- |------|------|-------------|
66
- | `publishableKey` | `string` | Your project's publishable key |
67
- | `config` | `AuthonConfig` (optional) | Additional client configuration |
18
+ 2. **Get your API keys** from Project Settings → API Keys
19
+ - **Publishable Key** (`pk_live_...`) — use in your frontend code
20
+ - **Test Key** (`pk_test_...`) for development, enables Dev Teleport
68
21
 
69
- ---
22
+ 3. **Configure OAuth providers** (optional) in Project Settings → OAuth
23
+ - Add Google, Apple, GitHub, etc. with their respective Client ID and Secret
24
+ - Set the redirect URL to `https://api.authon.dev/v1/auth/oauth/redirect`
70
25
 
71
- ### `<SignIn>`
26
+ > **Test vs Live keys:** Use `pk_test_...` during development. Switch to `pk_live_...` before deploying to production. Test keys use a sandbox environment with no rate limits.
72
27
 
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" />
28
+ ## Install
80
29
 
81
- // Embedded mode — renders a container div for the hosted form
82
- <SignIn mode="embedded" />
30
+ ```bash
31
+ npm install @authon/react
83
32
  ```
84
33
 
85
- | Prop | Type | Default | Description |
86
- |------|------|---------|-------------|
87
- | `mode` | `'popup' \| 'embedded'` | `'popup'` | Display mode |
88
- | `redirectUrl` | `string` (optional) | — | URL to redirect after sign-in |
89
-
90
- ---
91
-
92
- ### `<SignUp>`
93
-
94
- Opens the sign-up modal or renders an embedded sign-up form.
34
+ ## Quick Start
95
35
 
96
36
  ```tsx
97
- import { SignUp } from '@authon/react';
98
-
99
- // Popup mode (default)
100
- <SignUp mode="popup" />
101
-
102
- // Embedded mode
103
- <SignUp mode="embedded" />
104
- ```
105
-
106
- | Prop | Type | Default | Description |
107
- |------|------|---------|-------------|
108
- | `mode` | `'popup' \| 'embedded'` | `'popup'` | Display mode |
109
-
110
- ---
37
+ // src/main.tsx complete working file
38
+ import React from 'react';
39
+ import ReactDOM from 'react-dom/client';
40
+ import { AuthonProvider, useAuthon, useUser, SignedIn, SignedOut, UserButton } from '@authon/react';
111
41
 
112
- ### `<UserButton>`
113
-
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.
115
-
116
- ```tsx
117
- import { UserButton } from '@authon/react';
42
+ function App() {
43
+ const { openSignIn, signOut } = useAuthon();
44
+ const { user } = useUser();
118
45
 
119
- function Navbar() {
120
46
  return (
121
- <nav>
122
- <UserButton />
123
- </nav>
47
+ <div>
48
+ <SignedOut>
49
+ <button onClick={() => openSignIn()}>Sign In</button>
50
+ </SignedOut>
51
+ <SignedIn>
52
+ <p>Welcome, {user?.email}</p>
53
+ <UserButton />
54
+ <button onClick={() => signOut()}>Sign Out</button>
55
+ </SignedIn>
56
+ </div>
124
57
  );
125
58
  }
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
59
 
153
- <SignedOut>
154
- <a href="/sign-in">Sign In</a>
155
- </SignedOut>
60
+ ReactDOM.createRoot(document.getElementById('root')!).render(
61
+ <AuthonProvider
62
+ publishableKey="pk_live_YOUR_PUBLISHABLE_KEY"
63
+ >
64
+ <App />
65
+ </AuthonProvider>
66
+ );
156
67
  ```
157
68
 
158
- ---
159
-
160
- ### `<Protect>`
69
+ ## Common Tasks
161
70
 
162
- Guards content based on authentication status and an optional custom condition. Useful for role-based access control.
71
+ ### Add Google OAuth Login
163
72
 
164
73
  ```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
173
- <Protect
174
- fallback={<p>Admin access required.</p>}
175
- condition={(user) => user.publicMetadata?.role === 'admin'}
176
- >
177
- <AdminPanel />
178
- </Protect>
179
- ```
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';
74
+ import { useAuthon } from '@authon/react';
195
75
 
196
- function SocialLogin() {
76
+ function GoogleLoginButton() {
197
77
  const { client } = useAuthon();
198
-
199
78
  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>
79
+ <button onClick={() => client?.signInWithOAuth('google')}>
80
+ Sign in with Google
81
+ </button>
216
82
  );
217
83
  }
218
84
  ```
219
85
 
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>`.
86
+ ### Protect a Route
279
87
 
280
88
  ```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
- }
89
+ import { Protect } from '@authon/react';
291
90
 
91
+ function Dashboard() {
292
92
  return (
293
- <div>
294
- <span>Hello, {user?.displayName}</span>
295
- <button onClick={() => signOut()}>Sign Out</button>
296
- </div>
93
+ <Protect fallback={<p>Please sign in to view this page.</p>}>
94
+ <h1>Dashboard</h1>
95
+ </Protect>
297
96
  );
298
97
  }
299
- ```
300
98
 
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;
99
+ // With role-based condition
100
+ function AdminPanel() {
101
+ return (
102
+ <Protect
103
+ condition={(user) => user.publicMetadata?.role === 'admin'}
104
+ fallback={<p>Admin access required.</p>}
105
+ >
106
+ <h1>Admin Panel</h1>
107
+ </Protect>
108
+ );
313
109
  }
314
110
  ```
315
111
 
316
- ---
317
-
318
- ### `useUser()`
319
-
320
- Shorthand hook that returns only the current user and loading state.
112
+ ### Get Current User
321
113
 
322
114
  ```tsx
323
115
  import { useUser } from '@authon/react';
324
116
 
325
- function WelcomeBanner() {
117
+ function Profile() {
326
118
  const { user, isLoading } = useUser();
327
-
328
119
  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
-
120
+ if (!user) return <p>Not signed in</p>;
490
121
  return (
491
122
  <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>}
123
+ <p>Email: {user.email}</p>
124
+ <p>Name: {user.displayName}</p>
499
125
  </div>
500
126
  );
501
127
  }
502
128
  ```
503
129
 
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.
130
+ ### Add Email/Password Auth
523
131
 
524
132
  ```tsx
525
- import { useAuthonPasswordless } from '@authon/react';
133
+ import { useAuthon } from '@authon/react';
526
134
  import { useState } from 'react';
527
135
 
528
- function PasswordlessLogin() {
529
- const { sendMagicLink, sendEmailOtp, verifyPasswordless, isLoading, error } = useAuthonPasswordless();
136
+ function EmailSignIn() {
137
+ const { client } = useAuthon();
530
138
  const [email, setEmail] = useState('');
531
- const [otpSent, setOtpSent] = useState(false);
532
- const [code, setCode] = useState('');
139
+ const [password, setPassword] = useState('');
533
140
 
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!');
141
+ const handleSubmit = async (e: React.FormEvent) => {
142
+ e.preventDefault();
143
+ await client?.signInWithEmail(email, password);
553
144
  };
554
145
 
555
146
  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>
147
+ <form onSubmit={handleSubmit}>
148
+ <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
149
+ <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
150
+ <button type="submit">Sign In</button>
151
+ </form>
570
152
  );
571
153
  }
572
154
  ```
573
155
 
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).
156
+ ### Handle Sign Out
591
157
 
592
158
  ```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
-
781
- ## Documentation
159
+ import { useAuthon } from '@authon/react';
782
160
 
783
- [authon.dev/docs](https://authon.dev/docs)
161
+ function SignOutButton() {
162
+ const { signOut } = useAuthon();
163
+ return <button onClick={() => signOut()}>Sign Out</button>;
164
+ }
165
+ ```
166
+
167
+ ## Environment Variables
168
+
169
+ | Variable | Required | Description |
170
+ |----------|----------|-------------|
171
+ | `AUTHON_PUBLISHABLE_KEY` | Yes | Project publishable key (`pk_live_...` or `pk_test_...`) |
172
+ | `AUTHON_API_URL` | No | Optional — defaults to `api.authon.dev` |
173
+
174
+ ## API Reference
175
+
176
+ ### Components
177
+
178
+ | Component | Description |
179
+ |-----------|-------------|
180
+ | `<AuthonProvider>` | Context provider. Props: `publishableKey`, `config?` |
181
+ | `<SignIn>` | Pre-built sign-in form (`mode="popup"` or `"embedded"`) |
182
+ | `<SignUp>` | Pre-built sign-up form |
183
+ | `<UserButton>` | Avatar dropdown with user info and sign-out |
184
+ | `<UserProfile>` | Full user profile management |
185
+ | `<SignedIn>` | Renders children only when signed in |
186
+ | `<SignedOut>` | Renders children only when signed out |
187
+ | `<Protect>` | Conditional render with `fallback` and `condition` |
188
+ | `<SocialButton>` | Single OAuth provider button |
189
+ | `<SocialButtons>` | All enabled OAuth provider buttons |
190
+
191
+ ### Hooks
192
+
193
+ | Hook | Returns |
194
+ |------|---------|
195
+ | `useAuthon()` | `{ isSignedIn, isLoading, user, signOut, openSignIn, openSignUp, getToken, client }` |
196
+ | `useUser()` | `{ user, isLoading }` |
197
+ | `useAuthonMfa()` | MFA setup, verification, and management |
198
+ | `useAuthonPasskeys()` | Passkey registration and authentication |
199
+ | `useAuthonPasswordless()` | Magic link and email OTP |
200
+ | `useAuthonWeb3()` | Web3 wallet sign-in and management |
201
+ | `useAuthonSessions()` | List and revoke active sessions |
202
+ | `useOrganization()` | Active organization management |
203
+ | `useOrganizationList()` | List and switch organizations |
204
+
205
+ ## Comparison
206
+
207
+ | Feature | Authon | Clerk | Auth.js |
208
+ |---------|--------|-------|---------|
209
+ | Pricing | Free | $25/mo+ | Free |
210
+ | OAuth providers | 10+ | 20+ | 80+ |
211
+ | ShadowDOM modal | Yes | No | No |
212
+ | MFA/Passkeys | Yes | Yes | Plugin |
213
+ | Web3 auth | Yes | No | No |
214
+ | Organizations | Yes | Yes | No |
784
215
 
785
216
  ## License
786
217
 
787
- [MIT](../../LICENSE)
218
+ MIT