@pixygon/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/README.md +267 -0
- package/dist/chunk-E34M2RJD.mjs +1003 -0
- package/dist/components/index.d.mts +14 -0
- package/dist/components/index.d.ts +14 -0
- package/dist/components/index.js +1720 -0
- package/dist/components/index.mjs +1646 -0
- package/dist/index-CIK2MKl9.d.mts +201 -0
- package/dist/index-CIK2MKl9.d.ts +201 -0
- package/dist/index.d.mts +233 -0
- package/dist/index.d.ts +233 -0
- package/dist/index.js +1037 -0
- package/dist/index.mjs +28 -0
- package/package.json +58 -0
- package/src/api/client.ts +358 -0
- package/src/components/ForgotPasswordForm.tsx +410 -0
- package/src/components/LoginForm.tsx +323 -0
- package/src/components/PixygonAuth.tsx +170 -0
- package/src/components/RegisterForm.tsx +463 -0
- package/src/components/VerifyForm.tsx +411 -0
- package/src/components/index.ts +40 -0
- package/src/components/styles.ts +282 -0
- package/src/hooks/index.ts +284 -0
- package/src/hooks/useProfileSync.ts +201 -0
- package/src/index.ts +99 -0
- package/src/providers/AuthProvider.tsx +480 -0
- package/src/types/index.ts +310 -0
- package/src/utils/storage.ts +167 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pixygon/auth - Verify Form Component
|
|
3
|
+
* Email verification form for Pixygon Account
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useRef, useEffect, type FormEvent, type KeyboardEvent, type ClipboardEvent } from 'react';
|
|
7
|
+
import { useAuth } from '../hooks';
|
|
8
|
+
import type { VerifyFormProps, AuthError } from '../types';
|
|
9
|
+
|
|
10
|
+
export function VerifyForm({
|
|
11
|
+
userName,
|
|
12
|
+
onSuccess,
|
|
13
|
+
onError,
|
|
14
|
+
onNavigateLogin,
|
|
15
|
+
showBranding = true,
|
|
16
|
+
className = '',
|
|
17
|
+
}: VerifyFormProps) {
|
|
18
|
+
const { verify, resendVerification, isLoading, error } = useAuth();
|
|
19
|
+
|
|
20
|
+
const [code, setCode] = useState(['', '', '', '', '', '']);
|
|
21
|
+
const [localError, setLocalError] = useState<string | null>(null);
|
|
22
|
+
const [resendSuccess, setResendSuccess] = useState(false);
|
|
23
|
+
const [resendCooldown, setResendCooldown] = useState(0);
|
|
24
|
+
|
|
25
|
+
const inputRefs = useRef<(HTMLInputElement | null)[]>([]);
|
|
26
|
+
|
|
27
|
+
// Focus first input on mount
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
inputRefs.current[0]?.focus();
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
// Resend cooldown timer
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (resendCooldown > 0) {
|
|
35
|
+
const timer = setTimeout(() => setResendCooldown(resendCooldown - 1), 1000);
|
|
36
|
+
return () => clearTimeout(timer);
|
|
37
|
+
}
|
|
38
|
+
}, [resendCooldown]);
|
|
39
|
+
|
|
40
|
+
const handleChange = useCallback(
|
|
41
|
+
(index: number, value: string) => {
|
|
42
|
+
if (!/^\d*$/.test(value)) return; // Only allow digits
|
|
43
|
+
|
|
44
|
+
const newCode = [...code];
|
|
45
|
+
newCode[index] = value.slice(-1); // Only keep last digit
|
|
46
|
+
setCode(newCode);
|
|
47
|
+
|
|
48
|
+
// Auto-focus next input
|
|
49
|
+
if (value && index < 5) {
|
|
50
|
+
inputRefs.current[index + 1]?.focus();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Auto-submit when all filled
|
|
54
|
+
if (newCode.every((digit) => digit) && value) {
|
|
55
|
+
handleSubmitCode(newCode.join(''));
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
[code]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const handleKeyDown = useCallback(
|
|
62
|
+
(index: number, e: KeyboardEvent<HTMLInputElement>) => {
|
|
63
|
+
if (e.key === 'Backspace' && !code[index] && index > 0) {
|
|
64
|
+
inputRefs.current[index - 1]?.focus();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
[code]
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const handlePaste = useCallback(
|
|
71
|
+
(e: ClipboardEvent<HTMLInputElement>) => {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
const pasted = e.clipboardData.getData('text').replace(/\D/g, '').slice(0, 6);
|
|
74
|
+
if (pasted.length === 6) {
|
|
75
|
+
const newCode = pasted.split('');
|
|
76
|
+
setCode(newCode);
|
|
77
|
+
inputRefs.current[5]?.focus();
|
|
78
|
+
handleSubmitCode(pasted);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
[]
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
const handleSubmitCode = useCallback(
|
|
85
|
+
async (verificationCode: string) => {
|
|
86
|
+
setLocalError(null);
|
|
87
|
+
setResendSuccess(false);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const response = await verify({ userName, verificationCode });
|
|
91
|
+
onSuccess?.(response.user);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const authError = err as AuthError;
|
|
94
|
+
onError?.(authError);
|
|
95
|
+
// Clear code on error
|
|
96
|
+
setCode(['', '', '', '', '', '']);
|
|
97
|
+
inputRefs.current[0]?.focus();
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
[userName, verify, onSuccess, onError]
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const handleSubmit = useCallback(
|
|
104
|
+
(e: FormEvent) => {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
const verificationCode = code.join('');
|
|
107
|
+
if (verificationCode.length !== 6) {
|
|
108
|
+
setLocalError('Please enter the complete 6-digit code');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
handleSubmitCode(verificationCode);
|
|
112
|
+
},
|
|
113
|
+
[code, handleSubmitCode]
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const handleResend = useCallback(async () => {
|
|
117
|
+
if (resendCooldown > 0) return;
|
|
118
|
+
|
|
119
|
+
setLocalError(null);
|
|
120
|
+
setResendSuccess(false);
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
await resendVerification({ userName });
|
|
124
|
+
setResendSuccess(true);
|
|
125
|
+
setResendCooldown(60); // 60 second cooldown
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const authError = err as AuthError;
|
|
128
|
+
setLocalError(authError.message);
|
|
129
|
+
}
|
|
130
|
+
}, [userName, resendVerification, resendCooldown]);
|
|
131
|
+
|
|
132
|
+
const displayError = localError || error?.message;
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className={`pixygon-auth-container ${className}`}>
|
|
136
|
+
<style>{`
|
|
137
|
+
.pixygon-auth-container {
|
|
138
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
139
|
+
background: #0f0f0f;
|
|
140
|
+
color: #ffffff;
|
|
141
|
+
padding: 2rem;
|
|
142
|
+
border-radius: 1rem;
|
|
143
|
+
max-width: 400px;
|
|
144
|
+
width: 100%;
|
|
145
|
+
margin: 0 auto;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.pixygon-auth-header {
|
|
149
|
+
text-align: center;
|
|
150
|
+
margin-bottom: 2rem;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.pixygon-auth-logo {
|
|
154
|
+
width: 64px;
|
|
155
|
+
height: 64px;
|
|
156
|
+
margin: 0 auto 1rem;
|
|
157
|
+
display: block;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.pixygon-auth-title {
|
|
161
|
+
font-size: 1.5rem;
|
|
162
|
+
font-weight: 600;
|
|
163
|
+
margin: 0 0 0.5rem;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.pixygon-auth-subtitle {
|
|
167
|
+
font-size: 0.875rem;
|
|
168
|
+
color: #a3a3a3;
|
|
169
|
+
margin: 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.pixygon-auth-form {
|
|
173
|
+
display: flex;
|
|
174
|
+
flex-direction: column;
|
|
175
|
+
gap: 1.5rem;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.pixygon-auth-code-container {
|
|
179
|
+
display: flex;
|
|
180
|
+
gap: 0.5rem;
|
|
181
|
+
justify-content: center;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.pixygon-auth-code-input {
|
|
185
|
+
width: 3rem;
|
|
186
|
+
height: 3.5rem;
|
|
187
|
+
text-align: center;
|
|
188
|
+
font-size: 1.5rem;
|
|
189
|
+
font-weight: 600;
|
|
190
|
+
background: #262626;
|
|
191
|
+
border: 1px solid #404040;
|
|
192
|
+
border-radius: 0.5rem;
|
|
193
|
+
color: #ffffff;
|
|
194
|
+
outline: none;
|
|
195
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.pixygon-auth-code-input:focus {
|
|
199
|
+
border-color: #6366f1;
|
|
200
|
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.pixygon-auth-code-input.error {
|
|
204
|
+
border-color: #ef4444;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.pixygon-auth-button {
|
|
208
|
+
background: #6366f1;
|
|
209
|
+
color: white;
|
|
210
|
+
border: none;
|
|
211
|
+
border-radius: 0.5rem;
|
|
212
|
+
padding: 0.875rem 1.5rem;
|
|
213
|
+
font-size: 1rem;
|
|
214
|
+
font-weight: 600;
|
|
215
|
+
cursor: pointer;
|
|
216
|
+
transition: background 0.2s;
|
|
217
|
+
display: flex;
|
|
218
|
+
align-items: center;
|
|
219
|
+
justify-content: center;
|
|
220
|
+
gap: 0.5rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.pixygon-auth-button:hover:not(:disabled) {
|
|
224
|
+
background: #4f46e5;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.pixygon-auth-button:disabled {
|
|
228
|
+
opacity: 0.6;
|
|
229
|
+
cursor: not-allowed;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.pixygon-auth-error {
|
|
233
|
+
background: rgba(239, 68, 68, 0.1);
|
|
234
|
+
border: 1px solid #ef4444;
|
|
235
|
+
border-radius: 0.5rem;
|
|
236
|
+
padding: 0.75rem 1rem;
|
|
237
|
+
color: #fca5a5;
|
|
238
|
+
font-size: 0.875rem;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.pixygon-auth-success {
|
|
242
|
+
background: rgba(34, 197, 94, 0.1);
|
|
243
|
+
border: 1px solid #22c55e;
|
|
244
|
+
border-radius: 0.5rem;
|
|
245
|
+
padding: 0.75rem 1rem;
|
|
246
|
+
color: #86efac;
|
|
247
|
+
font-size: 0.875rem;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.pixygon-auth-link {
|
|
251
|
+
color: #6366f1;
|
|
252
|
+
text-decoration: none;
|
|
253
|
+
font-size: 0.875rem;
|
|
254
|
+
cursor: pointer;
|
|
255
|
+
background: none;
|
|
256
|
+
border: none;
|
|
257
|
+
padding: 0;
|
|
258
|
+
font-family: inherit;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.pixygon-auth-link:hover {
|
|
262
|
+
color: #818cf8;
|
|
263
|
+
text-decoration: underline;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.pixygon-auth-link:disabled {
|
|
267
|
+
color: #737373;
|
|
268
|
+
cursor: not-allowed;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.pixygon-auth-footer {
|
|
272
|
+
text-align: center;
|
|
273
|
+
margin-top: 1.5rem;
|
|
274
|
+
font-size: 0.875rem;
|
|
275
|
+
color: #a3a3a3;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.pixygon-auth-branding {
|
|
279
|
+
display: flex;
|
|
280
|
+
align-items: center;
|
|
281
|
+
justify-content: center;
|
|
282
|
+
gap: 0.5rem;
|
|
283
|
+
margin-top: 1.5rem;
|
|
284
|
+
font-size: 0.75rem;
|
|
285
|
+
color: #737373;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.pixygon-auth-spinner {
|
|
289
|
+
width: 20px;
|
|
290
|
+
height: 20px;
|
|
291
|
+
border: 2px solid transparent;
|
|
292
|
+
border-top-color: currentColor;
|
|
293
|
+
border-radius: 50%;
|
|
294
|
+
animation: pixygon-spin 0.8s linear infinite;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
@keyframes pixygon-spin {
|
|
298
|
+
to { transform: rotate(360deg); }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.pixygon-auth-resend {
|
|
302
|
+
text-align: center;
|
|
303
|
+
font-size: 0.875rem;
|
|
304
|
+
color: #a3a3a3;
|
|
305
|
+
}
|
|
306
|
+
`}</style>
|
|
307
|
+
|
|
308
|
+
<div className="pixygon-auth-header">
|
|
309
|
+
<svg
|
|
310
|
+
className="pixygon-auth-logo"
|
|
311
|
+
viewBox="0 0 100 100"
|
|
312
|
+
fill="none"
|
|
313
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
314
|
+
>
|
|
315
|
+
<circle cx="50" cy="50" r="45" fill="#6366f1" />
|
|
316
|
+
<path
|
|
317
|
+
d="M30 45L45 30L70 55L55 70L30 45Z"
|
|
318
|
+
fill="white"
|
|
319
|
+
fillOpacity="0.9"
|
|
320
|
+
/>
|
|
321
|
+
<path
|
|
322
|
+
d="M35 55L50 70L45 75L30 60L35 55Z"
|
|
323
|
+
fill="white"
|
|
324
|
+
fillOpacity="0.7"
|
|
325
|
+
/>
|
|
326
|
+
</svg>
|
|
327
|
+
<h1 className="pixygon-auth-title">Verify Email</h1>
|
|
328
|
+
<p className="pixygon-auth-subtitle">
|
|
329
|
+
Enter the 6-digit code sent to your email
|
|
330
|
+
</p>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
<form className="pixygon-auth-form" onSubmit={handleSubmit}>
|
|
334
|
+
{displayError && (
|
|
335
|
+
<div className="pixygon-auth-error">{displayError}</div>
|
|
336
|
+
)}
|
|
337
|
+
|
|
338
|
+
{resendSuccess && (
|
|
339
|
+
<div className="pixygon-auth-success">
|
|
340
|
+
Verification code sent! Check your email.
|
|
341
|
+
</div>
|
|
342
|
+
)}
|
|
343
|
+
|
|
344
|
+
<div className="pixygon-auth-code-container">
|
|
345
|
+
{code.map((digit, index) => (
|
|
346
|
+
<input
|
|
347
|
+
key={index}
|
|
348
|
+
ref={(el) => (inputRefs.current[index] = el)}
|
|
349
|
+
className={`pixygon-auth-code-input ${displayError ? 'error' : ''}`}
|
|
350
|
+
type="text"
|
|
351
|
+
inputMode="numeric"
|
|
352
|
+
maxLength={1}
|
|
353
|
+
value={digit}
|
|
354
|
+
onChange={(e) => handleChange(index, e.target.value)}
|
|
355
|
+
onKeyDown={(e) => handleKeyDown(index, e)}
|
|
356
|
+
onPaste={handlePaste}
|
|
357
|
+
disabled={isLoading}
|
|
358
|
+
aria-label={`Digit ${index + 1}`}
|
|
359
|
+
/>
|
|
360
|
+
))}
|
|
361
|
+
</div>
|
|
362
|
+
|
|
363
|
+
<button
|
|
364
|
+
type="submit"
|
|
365
|
+
className="pixygon-auth-button"
|
|
366
|
+
disabled={isLoading || code.some((d) => !d)}
|
|
367
|
+
>
|
|
368
|
+
{isLoading ? (
|
|
369
|
+
<>
|
|
370
|
+
<div className="pixygon-auth-spinner" />
|
|
371
|
+
Verifying...
|
|
372
|
+
</>
|
|
373
|
+
) : (
|
|
374
|
+
'Verify Email'
|
|
375
|
+
)}
|
|
376
|
+
</button>
|
|
377
|
+
|
|
378
|
+
<div className="pixygon-auth-resend">
|
|
379
|
+
Didn't receive the code?{' '}
|
|
380
|
+
<button
|
|
381
|
+
type="button"
|
|
382
|
+
className="pixygon-auth-link"
|
|
383
|
+
onClick={handleResend}
|
|
384
|
+
disabled={resendCooldown > 0}
|
|
385
|
+
>
|
|
386
|
+
{resendCooldown > 0 ? `Resend in ${resendCooldown}s` : 'Resend code'}
|
|
387
|
+
</button>
|
|
388
|
+
</div>
|
|
389
|
+
</form>
|
|
390
|
+
|
|
391
|
+
{onNavigateLogin && (
|
|
392
|
+
<div className="pixygon-auth-footer">
|
|
393
|
+
Wrong account?{' '}
|
|
394
|
+
<button
|
|
395
|
+
type="button"
|
|
396
|
+
className="pixygon-auth-link"
|
|
397
|
+
onClick={onNavigateLogin}
|
|
398
|
+
>
|
|
399
|
+
Sign in with a different account
|
|
400
|
+
</button>
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
|
|
404
|
+
{showBranding && (
|
|
405
|
+
<div className="pixygon-auth-branding">
|
|
406
|
+
Secured by Pixygon Account
|
|
407
|
+
</div>
|
|
408
|
+
)}
|
|
409
|
+
</div>
|
|
410
|
+
);
|
|
411
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pixygon/auth/components
|
|
3
|
+
* Branded authentication components for Pixygon Account
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ```tsx
|
|
7
|
+
* import { PixygonAuth, LoginForm } from '@pixygon/auth/components';
|
|
8
|
+
*
|
|
9
|
+
* // All-in-one component
|
|
10
|
+
* <PixygonAuth
|
|
11
|
+
* mode="login"
|
|
12
|
+
* onSuccess={(user) => console.log('Logged in:', user)}
|
|
13
|
+
* onModeChange={(mode) => router.push(`/auth/${mode}`)}
|
|
14
|
+
* />
|
|
15
|
+
*
|
|
16
|
+
* // Or individual forms
|
|
17
|
+
* <LoginForm
|
|
18
|
+
* onSuccess={(user) => console.log('Logged in:', user)}
|
|
19
|
+
* onNavigateRegister={() => router.push('/register')}
|
|
20
|
+
* />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Main component
|
|
25
|
+
export { PixygonAuth } from './PixygonAuth';
|
|
26
|
+
|
|
27
|
+
// Individual forms
|
|
28
|
+
export { LoginForm } from './LoginForm';
|
|
29
|
+
export { RegisterForm } from './RegisterForm';
|
|
30
|
+
export { VerifyForm } from './VerifyForm';
|
|
31
|
+
export { ForgotPasswordForm } from './ForgotPasswordForm';
|
|
32
|
+
|
|
33
|
+
// Re-export types for convenience
|
|
34
|
+
export type {
|
|
35
|
+
PixygonAuthProps,
|
|
36
|
+
LoginFormProps,
|
|
37
|
+
RegisterFormProps,
|
|
38
|
+
VerifyFormProps,
|
|
39
|
+
ForgotPasswordFormProps,
|
|
40
|
+
} from '../types';
|