@passkeyme/react-auth 1.0.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/CHANGELOG.md +86 -0
- package/LICENSE +21 -0
- package/README.md +951 -0
- package/dist/components/DevToolsDashboard.d.ts +18 -0
- package/dist/components/DevToolsDashboard.d.ts.map +1 -0
- package/dist/components/LazyComponents.d.ts +22 -0
- package/dist/components/LazyComponents.d.ts.map +1 -0
- package/dist/components/NotificationProvider.d.ts +31 -0
- package/dist/components/NotificationProvider.d.ts.map +1 -0
- package/dist/components/PasskeymeAuthPanel.d.ts +121 -0
- package/dist/components/PasskeymeAuthPanel.d.ts.map +1 -0
- package/dist/components/PasskeymeButton.d.ts +64 -0
- package/dist/components/PasskeymeButton.d.ts.map +1 -0
- package/dist/components/PasskeymeCallbackHandler.d.ts +50 -0
- package/dist/components/PasskeymeCallbackHandler.d.ts.map +1 -0
- package/dist/components/PasskeymeErrorDisplay.d.ts +28 -0
- package/dist/components/PasskeymeErrorDisplay.d.ts.map +1 -0
- package/dist/components/PasskeymeLoadingIndicator.d.ts +34 -0
- package/dist/components/PasskeymeLoadingIndicator.d.ts.map +1 -0
- package/dist/components/PasskeymeOAuthButton.d.ts +8 -0
- package/dist/components/PasskeymeOAuthButton.d.ts.map +1 -0
- package/dist/components/PasskeymeProtectedRoute.d.ts +16 -0
- package/dist/components/PasskeymeProtectedRoute.d.ts.map +1 -0
- package/dist/components/PasskeymeProvider.d.ts +24 -0
- package/dist/components/PasskeymeProvider.d.ts.map +1 -0
- package/dist/components/PasskeymeUserProfile.d.ts +31 -0
- package/dist/components/PasskeymeUserProfile.d.ts.map +1 -0
- package/dist/components/PerformanceDashboard.d.ts +30 -0
- package/dist/components/PerformanceDashboard.d.ts.map +1 -0
- package/dist/components/VirtualScrollList.d.ts +105 -0
- package/dist/components/VirtualScrollList.d.ts.map +1 -0
- package/dist/hooks/useAuthUtils.d.ts +26 -0
- package/dist/hooks/useAuthUtils.d.ts.map +1 -0
- package/dist/hooks/useLoadingState.d.ts +31 -0
- package/dist/hooks/useLoadingState.d.ts.map +1 -0
- package/dist/hooks/usePerformanceMonitor.d.ts +41 -0
- package/dist/hooks/usePerformanceMonitor.d.ts.map +1 -0
- package/dist/hooks/useUsernameManager.d.ts +62 -0
- package/dist/hooks/useUsernameManager.d.ts.map +1 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +8734 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +8827 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +249 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/utils/analytics.d.ts +112 -0
- package/dist/utils/analytics.d.ts.map +1 -0
- package/dist/utils/debug.d.ts +9 -0
- package/dist/utils/debug.d.ts.map +1 -0
- package/dist/utils/devUtils.d.ts +53 -0
- package/dist/utils/devUtils.d.ts.map +1 -0
- package/dist/utils/enhancedButtonStyles.d.ts +70 -0
- package/dist/utils/enhancedButtonStyles.d.ts.map +1 -0
- package/dist/utils/importOptimizer.d.ts +79 -0
- package/dist/utils/importOptimizer.d.ts.map +1 -0
- package/dist/utils/loadingStates.d.ts +35 -0
- package/dist/utils/loadingStates.d.ts.map +1 -0
- package/dist/utils/logger.d.ts +41 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/performanceProfiler.d.ts +80 -0
- package/dist/utils/performanceProfiler.d.ts.map +1 -0
- package/dist/utils/reactCompat.d.ts +6 -0
- package/dist/utils/reactCompat.d.ts.map +1 -0
- package/dist/utils/storageOptimization.d.ts +173 -0
- package/dist/utils/storageOptimization.d.ts.map +1 -0
- package/dist/utils/testingUtils.d.ts +163 -0
- package/dist/utils/testingUtils.d.ts.map +1 -0
- package/package.json +93 -0
package/README.md
ADDED
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
# @passkeyme/react-auth
|
|
2
|
+
|
|
3
|
+
React integration for PasskeyMe Authentication SDK. Build secure authentication into your React apps with hooks and components.
|
|
4
|
+
|
|
5
|
+
## ๐ Features
|
|
6
|
+
|
|
7
|
+
- **๐ช React Hooks**: `usePasskeyme()`, `useAuth()`, `useAuthState()`
|
|
8
|
+
- **๐งฉ Pre-built Components**: Login buttons, user profiles, protected routes
|
|
9
|
+
- **๐ Automatic State Management**: Context-based state with automatic updates
|
|
10
|
+
- **๐จ Customizable UI**: Styled components with full customization options
|
|
11
|
+
- **๐ก๏ธ Type Safety**: Full TypeScript support
|
|
12
|
+
- **โก Zero Config**: Works out of the box with sensible defaults
|
|
13
|
+
|
|
14
|
+
## ๐ฆ Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @passkeyme/auth @passkeyme/react-auth
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Peer Dependencies:**
|
|
21
|
+
|
|
22
|
+
- React >= 16.8.0
|
|
23
|
+
- React DOM >= 16.8.0
|
|
24
|
+
|
|
25
|
+
## ๐ง Quick Start
|
|
26
|
+
|
|
27
|
+
### 1. Setup Provider
|
|
28
|
+
|
|
29
|
+
Wrap your app with the `PasskeymeProvider`:
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { PasskeymeProvider } from "@passkeyme/react-auth";
|
|
33
|
+
|
|
34
|
+
function App() {
|
|
35
|
+
return (
|
|
36
|
+
<PasskeymeProvider
|
|
37
|
+
config={{
|
|
38
|
+
appId: "your-passkeyme-app-id",
|
|
39
|
+
redirectUri: "http://localhost:3000/auth/callback",
|
|
40
|
+
debug: process.env.NODE_ENV === "development", // Enable debug logging in development
|
|
41
|
+
}}
|
|
42
|
+
>
|
|
43
|
+
<YourApp />
|
|
44
|
+
</PasskeymeProvider>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2. Use Authentication Hooks
|
|
50
|
+
|
|
51
|
+
```tsx
|
|
52
|
+
import { usePasskeyme, PasskeymeCallbackHandler } from "@passkeyme/react-auth";
|
|
53
|
+
import { BrowserRouter, Routes, Route } from "react-router-dom";
|
|
54
|
+
|
|
55
|
+
function App() {
|
|
56
|
+
return (
|
|
57
|
+
<BrowserRouter>
|
|
58
|
+
<Routes>
|
|
59
|
+
{/* Built-in callback handler - handles all authentication flows */}
|
|
60
|
+
<Route path="/auth/callback" element={<PasskeymeCallbackHandler />} />
|
|
61
|
+
<Route path="/" element={<Dashboard />} />
|
|
62
|
+
</Routes>
|
|
63
|
+
</BrowserRouter>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Dashboard() {
|
|
68
|
+
const { user, loginWithOAuth, logout, loading } = usePasskeyme();
|
|
69
|
+
|
|
70
|
+
if (loading) {
|
|
71
|
+
return <div>Loading...</div>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!user) {
|
|
75
|
+
return (
|
|
76
|
+
<button onClick={() => loginWithOAuth("google")}>
|
|
77
|
+
Login with Google
|
|
78
|
+
</button>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div>
|
|
84
|
+
<h1>Welcome, {user.name}!</h1>
|
|
85
|
+
<button onClick={logout}>Logout</button>
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### 3. Use Pre-built Components
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import {
|
|
95
|
+
PasskeymeOAuthButton,
|
|
96
|
+
PasskeymeButton,
|
|
97
|
+
PasskeymeUserProfile,
|
|
98
|
+
PasskeymeProtectedRoute,
|
|
99
|
+
} from "@passkeyme/react-auth";
|
|
100
|
+
|
|
101
|
+
function LoginPage() {
|
|
102
|
+
return (
|
|
103
|
+
<div>
|
|
104
|
+
<PasskeymeOAuthButton provider="google" />
|
|
105
|
+
<PasskeymeOAuthButton provider="github" />
|
|
106
|
+
<PasskeymeButton />
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function Dashboard() {
|
|
112
|
+
return (
|
|
113
|
+
<PasskeymeProtectedRoute fallback={<LoginPage />}>
|
|
114
|
+
<div>
|
|
115
|
+
<PasskeymeUserProfile showLogout />
|
|
116
|
+
<h1>Protected Content</h1>
|
|
117
|
+
</div>
|
|
118
|
+
</PasskeymeProtectedRoute>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## ๐ API Reference
|
|
124
|
+
|
|
125
|
+
### PasskeymeProvider
|
|
126
|
+
|
|
127
|
+
The main provider component that manages authentication state.
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
<PasskeymeProvider
|
|
131
|
+
config={{
|
|
132
|
+
appId: "your-app-id",
|
|
133
|
+
baseUrl: "https://auth.passkeyme.com",
|
|
134
|
+
redirectUri: "http://localhost:3000/callback",
|
|
135
|
+
debug: true, // Enable debug logging for development
|
|
136
|
+
}}
|
|
137
|
+
loadingComponent={<div>Loading...</div>}
|
|
138
|
+
errorComponent={(error) => <div>Error: {error}</div>}
|
|
139
|
+
onAuthChange={(user) => console.log("User changed:", user)}
|
|
140
|
+
onError={(error) => console.error("Auth error:", error)}
|
|
141
|
+
>
|
|
142
|
+
<App />
|
|
143
|
+
</PasskeymeProvider>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
> **Note**: Set `debug: false` or omit the debug flag in production to prevent debug messages from appearing in the browser console.
|
|
147
|
+
|
|
148
|
+
### usePasskeyme Hook
|
|
149
|
+
|
|
150
|
+
Main hook providing full authentication functionality.
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
const {
|
|
154
|
+
user, // Current user or null
|
|
155
|
+
loading, // Loading state
|
|
156
|
+
error, // Error message or null
|
|
157
|
+
isAuthenticated, // Boolean auth status
|
|
158
|
+
|
|
159
|
+
// Login methods
|
|
160
|
+
login, // Generic login with options
|
|
161
|
+
loginWithOAuth, // OAuth login (redirects)
|
|
162
|
+
loginWithPasskey, // Passkey authentication
|
|
163
|
+
loginWithPassword, // Username/password login
|
|
164
|
+
handleCallback, // Handle OAuth callback
|
|
165
|
+
|
|
166
|
+
// Other methods
|
|
167
|
+
logout, // Logout user
|
|
168
|
+
getAccessToken, // Get current token
|
|
169
|
+
refreshToken, // Refresh token
|
|
170
|
+
auth, // Direct access to auth instance
|
|
171
|
+
} = usePasskeyme();
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### useAuthState Hook
|
|
175
|
+
|
|
176
|
+
Get only the authentication state (no methods).
|
|
177
|
+
|
|
178
|
+
```tsx
|
|
179
|
+
const { user, loading, error, isAuthenticated } = useAuthState();
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### useAuth Hook
|
|
183
|
+
|
|
184
|
+
Get only the authentication methods (no state).
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
const {
|
|
188
|
+
login,
|
|
189
|
+
loginWithOAuth,
|
|
190
|
+
loginWithPasskey,
|
|
191
|
+
loginWithPassword,
|
|
192
|
+
logout,
|
|
193
|
+
getAccessToken,
|
|
194
|
+
refreshToken,
|
|
195
|
+
} = useAuth();
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## ๐งฉ Components
|
|
199
|
+
|
|
200
|
+
### PasskeymeCallbackHandler
|
|
201
|
+
|
|
202
|
+
**โญ NEW**: Built-in component that automatically handles all authentication callbacks.
|
|
203
|
+
|
|
204
|
+
The `PasskeymeCallbackHandler` eliminates the need to write custom callback logic. Just add it to your routing:
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
import { PasskeymeCallbackHandler } from '@passkeyme/react-auth';
|
|
208
|
+
|
|
209
|
+
// Basic usage - handles everything automatically
|
|
210
|
+
<Route path="/callback" element={<PasskeymeCallbackHandler />} />
|
|
211
|
+
|
|
212
|
+
// With custom redirects and components
|
|
213
|
+
<Route
|
|
214
|
+
path="/callback"
|
|
215
|
+
element={
|
|
216
|
+
<PasskeymeCallbackHandler
|
|
217
|
+
successRedirect="/dashboard"
|
|
218
|
+
errorRedirect="/login"
|
|
219
|
+
loadingComponent={CustomSpinner}
|
|
220
|
+
errorComponent={CustomError}
|
|
221
|
+
/>
|
|
222
|
+
}
|
|
223
|
+
/>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Features:**
|
|
227
|
+
|
|
228
|
+
- โ
Handles both OAuth (`?code=...`) and hosted auth (`?token=...`) flows
|
|
229
|
+
- โ
Built-in loading and error states with customizable components
|
|
230
|
+
- โ
Automatic token validation and user state management
|
|
231
|
+
- โ
**NEW**: Automatic passkey registration prompting (enabled by default)
|
|
232
|
+
- โ
Configurable success/error redirects
|
|
233
|
+
- โ
Custom event callbacks for advanced use cases
|
|
234
|
+
- โ
URL cleanup after processing
|
|
235
|
+
|
|
236
|
+
See [CALLBACK_HANDLER.md](./CALLBACK_HANDLER.md) for complete documentation.
|
|
237
|
+
|
|
238
|
+
### PasskeymeOAuthButton
|
|
239
|
+
|
|
240
|
+
OAuth login button with built-in styling.
|
|
241
|
+
|
|
242
|
+
```tsx
|
|
243
|
+
<PasskeymeOAuthButton
|
|
244
|
+
provider="google" // 'google' | 'github' | 'facebook'
|
|
245
|
+
variant="default" // 'default' | 'outlined' | 'text'
|
|
246
|
+
size="medium" // 'small' | 'medium' | 'large'
|
|
247
|
+
redirectUri="/custom/callback"
|
|
248
|
+
disabled={false}
|
|
249
|
+
loading={false}
|
|
250
|
+
onClick={() => console.log("Clicked")}
|
|
251
|
+
className="custom-class"
|
|
252
|
+
>
|
|
253
|
+
Custom Button Text
|
|
254
|
+
</PasskeymeOAuthButton>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### PasskeymeButton
|
|
258
|
+
|
|
259
|
+
Passkey authentication button.
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
<PasskeymeButton
|
|
263
|
+
username="user@example.com" // Optional username hint
|
|
264
|
+
variant="default"
|
|
265
|
+
size="medium"
|
|
266
|
+
onSuccess={(user) => console.log("Success:", user)}
|
|
267
|
+
onError={(error) => console.log("Error:", error)}
|
|
268
|
+
>
|
|
269
|
+
Sign in with Passkey
|
|
270
|
+
</PasskeymeButton>
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### PasskeymeUserProfile
|
|
274
|
+
|
|
275
|
+
User profile display with avatar and logout.
|
|
276
|
+
|
|
277
|
+
```tsx
|
|
278
|
+
<PasskeymeUserProfile
|
|
279
|
+
showAvatar={true}
|
|
280
|
+
showName={true}
|
|
281
|
+
showEmail={true}
|
|
282
|
+
showLogout={true}
|
|
283
|
+
avatarSize={40}
|
|
284
|
+
logoutText="Sign Out"
|
|
285
|
+
onLogout={() => console.log("Logged out")}
|
|
286
|
+
className="profile-container"
|
|
287
|
+
/>
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### PasskeymeProtectedRoute
|
|
291
|
+
|
|
292
|
+
Protect content for authenticated users.
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
<PasskeymeProtectedRoute
|
|
296
|
+
fallback={<LoginPage />}
|
|
297
|
+
redirectTo="/login"
|
|
298
|
+
requiredRoles={["admin", "user"]}
|
|
299
|
+
hasAccess={(user) => user.emailVerified}
|
|
300
|
+
>
|
|
301
|
+
<SecretContent />
|
|
302
|
+
</PasskeymeProtectedRoute>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### withAuth HOC
|
|
306
|
+
|
|
307
|
+
Higher-order component for protecting components.
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
const ProtectedComponent = withAuth(MyComponent, {
|
|
311
|
+
fallback: <div>Please login</div>,
|
|
312
|
+
requiredRoles: ["admin"],
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## ๐ Programmatic Authentication
|
|
317
|
+
|
|
318
|
+
### triggerPasskeymeAuth
|
|
319
|
+
|
|
320
|
+
**โญ NEW**: Enhanced programmatic authentication with mode support for hosted vs inline OAuth flows.
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
// Simple hosted flow (default - redirects to hosted auth pages)
|
|
324
|
+
const { triggerPasskeymeAuth } = usePasskeyme();
|
|
325
|
+
triggerPasskeymeAuth();
|
|
326
|
+
|
|
327
|
+
// Custom success/error handling
|
|
328
|
+
triggerPasskeymeAuth({
|
|
329
|
+
username: "user@example.com",
|
|
330
|
+
onSuccess: (user, method) => console.log(`Authenticated via ${method}`, user),
|
|
331
|
+
onError: (error) => console.error("Auth failed:", error),
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Inline mode - developer controls OAuth UI
|
|
335
|
+
triggerPasskeymeAuth({
|
|
336
|
+
mode: "inline",
|
|
337
|
+
onOAuthRequired: (providers) => {
|
|
338
|
+
// Show your custom OAuth UI
|
|
339
|
+
setShowOAuthModal(true);
|
|
340
|
+
setAvailableProviders(providers);
|
|
341
|
+
},
|
|
342
|
+
onSuccess: (user, method) => {
|
|
343
|
+
setShowOAuthModal(false);
|
|
344
|
+
console.log(`Success via ${method}`, user);
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Passkey-only (no fallback)
|
|
349
|
+
triggerPasskeymeAuth({
|
|
350
|
+
forcePasskeyOnly: true,
|
|
351
|
+
onError: (error) => alert("Passkey authentication failed"),
|
|
352
|
+
});
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**Mode Options:**
|
|
356
|
+
|
|
357
|
+
- **`hosted`** (default): Try passkey โ redirect to hosted auth pages
|
|
358
|
+
- **`inline`**: Try passkey โ callback with OAuth providers for custom UI
|
|
359
|
+
|
|
360
|
+
This gives you maximum flexibility - use hosted mode for simple setup, or inline mode for full UI control.
|
|
361
|
+
|
|
362
|
+
### Complete Inline Mode Example
|
|
363
|
+
|
|
364
|
+
```tsx
|
|
365
|
+
function CustomAuthFlow() {
|
|
366
|
+
const { triggerPasskeymeAuth } = usePasskeyme();
|
|
367
|
+
const [showOAuth, setShowOAuth] = useState(false);
|
|
368
|
+
const [providers, setProviders] = useState([]);
|
|
369
|
+
|
|
370
|
+
const handleLogin = () => {
|
|
371
|
+
triggerPasskeymeAuth({
|
|
372
|
+
mode: "inline",
|
|
373
|
+
onOAuthRequired: (availableProviders) => {
|
|
374
|
+
setProviders(availableProviders);
|
|
375
|
+
setShowOAuth(true);
|
|
376
|
+
},
|
|
377
|
+
onSuccess: (user, method) => {
|
|
378
|
+
setShowOAuth(false);
|
|
379
|
+
console.log(`Authenticated via ${method}`);
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<div>
|
|
386
|
+
<button onClick={handleLogin}>Login</button>
|
|
387
|
+
|
|
388
|
+
{showOAuth && (
|
|
389
|
+
<div className="oauth-modal">
|
|
390
|
+
<h3>Choose OAuth Provider</h3>
|
|
391
|
+
{providers.map((provider) => (
|
|
392
|
+
<PasskeymeOAuthButton key={provider} provider={provider}>
|
|
393
|
+
Login with {provider}
|
|
394
|
+
</PasskeymeOAuthButton>
|
|
395
|
+
))}
|
|
396
|
+
<button onClick={() => setShowOAuth(false)}>Cancel</button>
|
|
397
|
+
</div>
|
|
398
|
+
)}
|
|
399
|
+
</div>
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## ๐จ Customization
|
|
405
|
+
|
|
406
|
+
### Custom Styling
|
|
407
|
+
|
|
408
|
+
All components accept `className` and can be styled with CSS:
|
|
409
|
+
|
|
410
|
+
```css
|
|
411
|
+
.login-button {
|
|
412
|
+
background: linear-gradient(45deg, #fe6b8b 30%, #ff8e53 90%);
|
|
413
|
+
border-radius: 20px;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.user-profile {
|
|
417
|
+
border: 2px solid #f0f0f0;
|
|
418
|
+
border-radius: 8px;
|
|
419
|
+
padding: 16px;
|
|
420
|
+
}
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
### Custom Components
|
|
424
|
+
|
|
425
|
+
Create your own components using the hooks:
|
|
426
|
+
|
|
427
|
+
```tsx
|
|
428
|
+
function CustomLoginForm() {
|
|
429
|
+
const { loginWithPassword, loading, error } = usePasskeyme();
|
|
430
|
+
const [email, setEmail] = useState("");
|
|
431
|
+
const [password, setPassword] = useState("");
|
|
432
|
+
|
|
433
|
+
const handleSubmit = async (e) => {
|
|
434
|
+
e.preventDefault();
|
|
435
|
+
try {
|
|
436
|
+
await loginWithPassword(email, password);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
// Error handled by hook
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
return (
|
|
443
|
+
<form onSubmit={handleSubmit}>
|
|
444
|
+
<input
|
|
445
|
+
value={email}
|
|
446
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
447
|
+
placeholder="Email"
|
|
448
|
+
/>
|
|
449
|
+
<input
|
|
450
|
+
type="password"
|
|
451
|
+
value={password}
|
|
452
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
453
|
+
placeholder="Password"
|
|
454
|
+
/>
|
|
455
|
+
<button type="submit" disabled={loading}>
|
|
456
|
+
{loading ? "Signing in..." : "Sign In"}
|
|
457
|
+
</button>
|
|
458
|
+
{error && <div>Error: {error}</div>}
|
|
459
|
+
</form>
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## ๐ Integration Examples
|
|
465
|
+
|
|
466
|
+
### Next.js App Router
|
|
467
|
+
|
|
468
|
+
```tsx
|
|
469
|
+
// app/layout.tsx
|
|
470
|
+
import { PasskeymeProvider } from "@passkeyme/react-auth";
|
|
471
|
+
|
|
472
|
+
export default function RootLayout({ children }) {
|
|
473
|
+
return (
|
|
474
|
+
<html>
|
|
475
|
+
<body>
|
|
476
|
+
<PasskeymeProvider
|
|
477
|
+
config={{ appId: process.env.NEXT_PUBLIC_PASSKEYME_APP_ID }}
|
|
478
|
+
>
|
|
479
|
+
{children}
|
|
480
|
+
</PasskeymeProvider>
|
|
481
|
+
</body>
|
|
482
|
+
</html>
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// app/dashboard/page.tsx
|
|
487
|
+
import { ProtectedRoute, UserProfile } from "@passkeyme/react-auth";
|
|
488
|
+
|
|
489
|
+
export default function Dashboard() {
|
|
490
|
+
return (
|
|
491
|
+
<ProtectedRoute>
|
|
492
|
+
<div>
|
|
493
|
+
<UserProfile />
|
|
494
|
+
<h1>Dashboard</h1>
|
|
495
|
+
</div>
|
|
496
|
+
</ProtectedRoute>
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### React Router
|
|
502
|
+
|
|
503
|
+
```tsx
|
|
504
|
+
import { BrowserRouter, Route, Routes, Navigate } from "react-router-dom";
|
|
505
|
+
import { usePasskeyme } from "@passkeyme/react-auth";
|
|
506
|
+
|
|
507
|
+
function AppRouter() {
|
|
508
|
+
const { isAuthenticated, loading } = usePasskeyme();
|
|
509
|
+
|
|
510
|
+
if (loading) return <div>Loading...</div>;
|
|
511
|
+
|
|
512
|
+
return (
|
|
513
|
+
<BrowserRouter>
|
|
514
|
+
<Routes>
|
|
515
|
+
<Route path="/login" element={<LoginPage />} />
|
|
516
|
+
<Route
|
|
517
|
+
path="/dashboard"
|
|
518
|
+
element={isAuthenticated ? <Dashboard /> : <Navigate to="/login" />}
|
|
519
|
+
/>
|
|
520
|
+
</Routes>
|
|
521
|
+
</BrowserRouter>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## ๐ TypeScript Support
|
|
527
|
+
|
|
528
|
+
This package provides comprehensive TypeScript definitions for type-safe authentication.
|
|
529
|
+
|
|
530
|
+
### Type Imports
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
import type {
|
|
534
|
+
User,
|
|
535
|
+
AuthConfig,
|
|
536
|
+
AuthState,
|
|
537
|
+
PasskeyCredential,
|
|
538
|
+
OAuthProvider,
|
|
539
|
+
UsePasskeymeReturn,
|
|
540
|
+
} from "@passkeyme/react-auth";
|
|
541
|
+
|
|
542
|
+
// User object structure
|
|
543
|
+
interface User {
|
|
544
|
+
id: string;
|
|
545
|
+
email: string;
|
|
546
|
+
name?: string;
|
|
547
|
+
avatar?: string;
|
|
548
|
+
metadata?: Record<string, any>;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Authentication configuration
|
|
552
|
+
interface AuthConfig {
|
|
553
|
+
appId: string;
|
|
554
|
+
baseUrl?: string;
|
|
555
|
+
redirectUri: string;
|
|
556
|
+
debug?: boolean;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Hook return type
|
|
560
|
+
interface UsePasskeymeReturn {
|
|
561
|
+
// State
|
|
562
|
+
user: User | null;
|
|
563
|
+
isAuthenticated: boolean;
|
|
564
|
+
loading: boolean;
|
|
565
|
+
error: Error | null;
|
|
566
|
+
|
|
567
|
+
// Actions
|
|
568
|
+
loginWithOAuth: (provider: OAuthProvider) => Promise<void>;
|
|
569
|
+
loginWithPasskey: () => Promise<void>;
|
|
570
|
+
loginWithCredentials: (email: string, password: string) => Promise<void>;
|
|
571
|
+
register: (email: string, password: string, name?: string) => Promise<void>;
|
|
572
|
+
logout: () => Promise<void>;
|
|
573
|
+
|
|
574
|
+
// Utility
|
|
575
|
+
refreshToken: () => Promise<void>;
|
|
576
|
+
getAuthHeader: () => string | null;
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### Type-Safe Configuration
|
|
581
|
+
|
|
582
|
+
```typescript
|
|
583
|
+
import { PasskeymeProvider } from "@passkeyme/react-auth";
|
|
584
|
+
import type { AuthConfig } from "@passkeyme/react-auth";
|
|
585
|
+
|
|
586
|
+
const authConfig: AuthConfig = {
|
|
587
|
+
appId: process.env.REACT_APP_PASSKEYME_APP_ID!,
|
|
588
|
+
redirectUri: `${window.location.origin}/auth/callback`,
|
|
589
|
+
debug: process.env.NODE_ENV === "development",
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
function App() {
|
|
593
|
+
return (
|
|
594
|
+
<PasskeymeProvider config={authConfig}>
|
|
595
|
+
<AppContent />
|
|
596
|
+
</PasskeymeProvider>
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
### Custom Hook with TypeScript
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
import { usePasskeyme } from "@passkeyme/react-auth";
|
|
605
|
+
import type { User } from "@passkeyme/react-auth";
|
|
606
|
+
|
|
607
|
+
// Custom hook for user profile management
|
|
608
|
+
function useUserProfile() {
|
|
609
|
+
const { user, updateProfile } = usePasskeyme();
|
|
610
|
+
|
|
611
|
+
const updateUserProfile = async (updates: Partial<User>) => {
|
|
612
|
+
if (!user) throw new Error("User not authenticated");
|
|
613
|
+
|
|
614
|
+
return updateProfile({
|
|
615
|
+
...user,
|
|
616
|
+
...updates,
|
|
617
|
+
});
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
return {
|
|
621
|
+
user,
|
|
622
|
+
updateUserProfile,
|
|
623
|
+
hasProfile: Boolean(user?.name),
|
|
624
|
+
isEmailVerified: user?.emailVerified ?? false,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
## ๐ Integration Guides
|
|
630
|
+
|
|
631
|
+
### Next.js Integration
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
// pages/_app.tsx
|
|
635
|
+
import { AppProps } from "next/app";
|
|
636
|
+
import { PasskeymeProvider } from "@passkeyme/react-auth";
|
|
637
|
+
|
|
638
|
+
export default function App({ Component, pageProps }: AppProps) {
|
|
639
|
+
return (
|
|
640
|
+
<PasskeymeProvider
|
|
641
|
+
config={{
|
|
642
|
+
appId: process.env.NEXT_PUBLIC_PASSKEYME_APP_ID!,
|
|
643
|
+
redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/auth/callback`,
|
|
644
|
+
}}
|
|
645
|
+
>
|
|
646
|
+
<Component {...pageProps} />
|
|
647
|
+
</PasskeymeProvider>
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// pages/auth/callback.tsx
|
|
652
|
+
import { PasskeymeCallbackHandler } from "@passkeyme/react-auth";
|
|
653
|
+
|
|
654
|
+
export default function AuthCallback() {
|
|
655
|
+
return <PasskeymeCallbackHandler />;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Custom hook for Next.js router integration
|
|
659
|
+
import { useRouter } from "next/router";
|
|
660
|
+
import { usePasskeyme } from "@passkeyme/react-auth";
|
|
661
|
+
import { useEffect } from "react";
|
|
662
|
+
|
|
663
|
+
export function useAuthRedirect() {
|
|
664
|
+
const { isAuthenticated, loading } = usePasskeyme();
|
|
665
|
+
const router = useRouter();
|
|
666
|
+
|
|
667
|
+
useEffect(() => {
|
|
668
|
+
if (!loading && !isAuthenticated) {
|
|
669
|
+
router.push("/login");
|
|
670
|
+
}
|
|
671
|
+
}, [isAuthenticated, loading, router]);
|
|
672
|
+
|
|
673
|
+
return { isAuthenticated, loading };
|
|
674
|
+
}
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### Vite/React Integration
|
|
678
|
+
|
|
679
|
+
```typescript
|
|
680
|
+
// main.tsx
|
|
681
|
+
import React from "react";
|
|
682
|
+
import ReactDOM from "react-dom/client";
|
|
683
|
+
import { PasskeymeProvider } from "@passkeyme/react-auth";
|
|
684
|
+
import App from "./App";
|
|
685
|
+
|
|
686
|
+
ReactDOM.createRoot(document.getElementById("root")!).render(
|
|
687
|
+
<React.StrictMode>
|
|
688
|
+
<PasskeymeProvider
|
|
689
|
+
config={{
|
|
690
|
+
appId: import.meta.env.VITE_PASSKEYME_APP_ID,
|
|
691
|
+
redirectUri: `${window.location.origin}/auth/callback`,
|
|
692
|
+
debug: import.meta.env.DEV,
|
|
693
|
+
}}
|
|
694
|
+
>
|
|
695
|
+
<App />
|
|
696
|
+
</PasskeymeProvider>
|
|
697
|
+
</React.StrictMode>
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
// vite-env.d.ts - Environment variables
|
|
701
|
+
interface ImportMetaEnv {
|
|
702
|
+
readonly VITE_PASSKEYME_APP_ID: string;
|
|
703
|
+
// Add other env variables as needed
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
interface ImportMeta {
|
|
707
|
+
readonly env: ImportMetaEnv;
|
|
708
|
+
}
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### React Router Integration
|
|
712
|
+
|
|
713
|
+
```typescript
|
|
714
|
+
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
|
|
715
|
+
import { PasskeymeCallbackHandler, usePasskeyme } from "@passkeyme/react-auth";
|
|
716
|
+
|
|
717
|
+
// Protected Route Component
|
|
718
|
+
interface ProtectedRouteProps {
|
|
719
|
+
children: React.ReactNode;
|
|
720
|
+
fallback?: React.ReactNode;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function ProtectedRoute({ children, fallback }: ProtectedRouteProps) {
|
|
724
|
+
const { isAuthenticated, loading } = usePasskeyme();
|
|
725
|
+
|
|
726
|
+
if (loading) return <div>Loading...</div>;
|
|
727
|
+
|
|
728
|
+
if (!isAuthenticated) {
|
|
729
|
+
return fallback ? <>{fallback}</> : <Navigate to="/login" replace />;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return <>{children}</>;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function AppRouter() {
|
|
736
|
+
return (
|
|
737
|
+
<BrowserRouter>
|
|
738
|
+
<Routes>
|
|
739
|
+
<Route path="/login" element={<LoginPage />} />
|
|
740
|
+
<Route path="/auth/callback" element={<PasskeymeCallbackHandler />} />
|
|
741
|
+
<Route
|
|
742
|
+
path="/dashboard"
|
|
743
|
+
element={
|
|
744
|
+
<ProtectedRoute>
|
|
745
|
+
<Dashboard />
|
|
746
|
+
</ProtectedRoute>
|
|
747
|
+
}
|
|
748
|
+
/>
|
|
749
|
+
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
|
750
|
+
</Routes>
|
|
751
|
+
</BrowserRouter>
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
### Error Handling Best Practices
|
|
757
|
+
|
|
758
|
+
```typescript
|
|
759
|
+
import { usePasskeyme } from "@passkeyme/react-auth";
|
|
760
|
+
import { useEffect, useState } from "react";
|
|
761
|
+
|
|
762
|
+
function LoginWithErrorHandling() {
|
|
763
|
+
const { loginWithOAuth, error, loading } = usePasskeyme();
|
|
764
|
+
const [loginError, setLoginError] = useState<string | null>(null);
|
|
765
|
+
|
|
766
|
+
const handleLogin = async (provider: "google" | "github") => {
|
|
767
|
+
try {
|
|
768
|
+
setLoginError(null);
|
|
769
|
+
await loginWithOAuth(provider);
|
|
770
|
+
} catch (err) {
|
|
771
|
+
setLoginError(err instanceof Error ? err.message : "Login failed");
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
// Handle authentication errors
|
|
776
|
+
useEffect(() => {
|
|
777
|
+
if (error) {
|
|
778
|
+
console.error("Authentication error:", error);
|
|
779
|
+
setLoginError(error.message);
|
|
780
|
+
}
|
|
781
|
+
}, [error]);
|
|
782
|
+
|
|
783
|
+
return (
|
|
784
|
+
<div>
|
|
785
|
+
{loginError && (
|
|
786
|
+
<div className="error-banner" role="alert">
|
|
787
|
+
{loginError}
|
|
788
|
+
<button onClick={() => setLoginError(null)}>ร</button>
|
|
789
|
+
</div>
|
|
790
|
+
)}
|
|
791
|
+
|
|
792
|
+
<button onClick={() => handleLogin("google")} disabled={loading}>
|
|
793
|
+
{loading ? "Signing in..." : "Sign in with Google"}
|
|
794
|
+
</button>
|
|
795
|
+
</div>
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
## ๐งช Testing Integration
|
|
801
|
+
|
|
802
|
+
### Jest Testing Setup
|
|
803
|
+
|
|
804
|
+
```typescript
|
|
805
|
+
// test-utils.tsx
|
|
806
|
+
import React from "react";
|
|
807
|
+
import { render, RenderOptions } from "@testing-library/react";
|
|
808
|
+
import { PasskeymeProvider } from "@passkeyme/react-auth";
|
|
809
|
+
import type { AuthConfig } from "@passkeyme/react-auth";
|
|
810
|
+
|
|
811
|
+
const mockAuthConfig: AuthConfig = {
|
|
812
|
+
appId: "test-app-id",
|
|
813
|
+
redirectUri: "http://localhost:3000/callback",
|
|
814
|
+
debug: false,
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
interface CustomRenderOptions extends Omit<RenderOptions, "wrapper"> {
|
|
818
|
+
authConfig?: Partial<AuthConfig>;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
export function renderWithAuth(
|
|
822
|
+
ui: React.ReactElement,
|
|
823
|
+
{ authConfig = {}, ...options }: CustomRenderOptions = {}
|
|
824
|
+
) {
|
|
825
|
+
const config = { ...mockAuthConfig, ...authConfig };
|
|
826
|
+
|
|
827
|
+
function Wrapper({ children }: { children: React.ReactNode }) {
|
|
828
|
+
return <PasskeymeProvider config={config}>{children}</PasskeymeProvider>;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return render(ui, { wrapper: Wrapper, ...options });
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Component test example
|
|
835
|
+
import { screen, fireEvent, waitFor } from "@testing-library/react";
|
|
836
|
+
import { usePasskeyme } from "@passkeyme/react-auth";
|
|
837
|
+
|
|
838
|
+
function TestComponent() {
|
|
839
|
+
const { user, loginWithOAuth, loading } = usePasskeyme();
|
|
840
|
+
|
|
841
|
+
return (
|
|
842
|
+
<div>
|
|
843
|
+
{user ? (
|
|
844
|
+
<div data-testid="user-info">{user.email}</div>
|
|
845
|
+
) : (
|
|
846
|
+
<button onClick={() => loginWithOAuth("google")}>
|
|
847
|
+
{loading ? "Loading..." : "Login"}
|
|
848
|
+
</button>
|
|
849
|
+
)}
|
|
850
|
+
</div>
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
test("handles login flow", async () => {
|
|
855
|
+
renderWithAuth(<TestComponent />);
|
|
856
|
+
|
|
857
|
+
const loginButton = screen.getByText("Login");
|
|
858
|
+
fireEvent.click(loginButton);
|
|
859
|
+
|
|
860
|
+
expect(screen.getByText("Loading...")).toBeInTheDocument();
|
|
861
|
+
|
|
862
|
+
await waitFor(() => {
|
|
863
|
+
expect(screen.getByTestId("user-info")).toBeInTheDocument();
|
|
864
|
+
});
|
|
865
|
+
});
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
## ๐ Package Integration
|
|
869
|
+
|
|
870
|
+
### Using with @passkeyme/react-core
|
|
871
|
+
|
|
872
|
+
This package builds on top of `@passkeyme/react-core` for shared patterns:
|
|
873
|
+
|
|
874
|
+
```typescript
|
|
875
|
+
// Access core patterns directly
|
|
876
|
+
import {
|
|
877
|
+
usePasskeymeContext,
|
|
878
|
+
PasskeymeProvider as CoreProvider,
|
|
879
|
+
} from "@passkeyme/react-core";
|
|
880
|
+
|
|
881
|
+
// Or use the enhanced React-specific provider
|
|
882
|
+
import { PasskeymeProvider } from "@passkeyme/react-auth";
|
|
883
|
+
|
|
884
|
+
// Custom hook using core patterns
|
|
885
|
+
function useCustomAuth() {
|
|
886
|
+
const { state, dispatch } = usePasskeymeContext();
|
|
887
|
+
|
|
888
|
+
const customLogin = async () => {
|
|
889
|
+
dispatch({ type: "SET_LOADING", payload: true });
|
|
890
|
+
try {
|
|
891
|
+
// Custom authentication logic
|
|
892
|
+
const user = await customAuthFlow();
|
|
893
|
+
dispatch({ type: "SET_USER", payload: user });
|
|
894
|
+
} catch (error) {
|
|
895
|
+
dispatch({ type: "SET_ERROR", payload: error });
|
|
896
|
+
} finally {
|
|
897
|
+
dispatch({ type: "SET_LOADING", payload: false });
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
return { ...state, customLogin };
|
|
902
|
+
}
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
### Using with @passkeyme/auth Core
|
|
906
|
+
|
|
907
|
+
```typescript
|
|
908
|
+
import { PasskeymeAuth } from "@passkeyme/auth";
|
|
909
|
+
import { usePasskeyme } from "@passkeyme/react-auth";
|
|
910
|
+
|
|
911
|
+
// Access the core auth instance
|
|
912
|
+
function AdvancedAuthComponent() {
|
|
913
|
+
const { auth } = usePasskeyme();
|
|
914
|
+
|
|
915
|
+
const handleAdvancedFlow = async () => {
|
|
916
|
+
// Direct access to core SDK methods
|
|
917
|
+
const tokens = await auth.getTokens();
|
|
918
|
+
const userInfo = await auth.getUserInfo();
|
|
919
|
+
|
|
920
|
+
// Custom API calls with auth headers
|
|
921
|
+
const response = await fetch("/api/protected", {
|
|
922
|
+
headers: {
|
|
923
|
+
Authorization: `Bearer ${tokens.accessToken}`,
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
return <button onClick={handleAdvancedFlow}>Advanced Flow</button>;
|
|
929
|
+
}
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
## ๐ vs Other Solutions
|
|
933
|
+
|
|
934
|
+
| Feature | **@passkeyme/react-auth** | **Firebase Auth** | **Auth0 React** |
|
|
935
|
+
| --------------------- | ------------------------- | ----------------- | ------------------ |
|
|
936
|
+
| **Setup Lines** | โ
5 lines | โ 20+ lines | โ 15+ lines |
|
|
937
|
+
| **Built-in Passkeys** | โ
Native support | โ Manual setup | โ Manual setup |
|
|
938
|
+
| **Type Safety** | โ
Full TypeScript | โ ๏ธ Partial | โ
Full TypeScript |
|
|
939
|
+
| **Bundle Size** | โ
Small | โ Large | โ Large |
|
|
940
|
+
| **Self-Hosting** | โ
Yes | โ No | โ No |
|
|
941
|
+
| **Vendor Lock-in** | โ
None | โ High | โ High |
|
|
942
|
+
|
|
943
|
+
## ๐ License
|
|
944
|
+
|
|
945
|
+
MIT - see LICENSE file for details.
|
|
946
|
+
|
|
947
|
+
## ๐ค Support
|
|
948
|
+
|
|
949
|
+
- **Documentation**: https://passkeyme.com/docs/react
|
|
950
|
+
- **Issues**: https://github.com/passkeyme/passkeyme/issues
|
|
951
|
+
- **Discord**: https://discord.gg/passkeyme
|