@massimo.mazzoleni/cognito-max 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.
Files changed (64) hide show
  1. package/README.md +2410 -0
  2. package/dist/chunk-AD7T42HJ.js +3 -0
  3. package/dist/chunk-AD7T42HJ.js.map +1 -0
  4. package/dist/chunk-DKPFVGTY.js +683 -0
  5. package/dist/chunk-DKPFVGTY.js.map +1 -0
  6. package/dist/chunk-N4OQLBV6.js +135 -0
  7. package/dist/chunk-N4OQLBV6.js.map +1 -0
  8. package/dist/client-63FraVdm.d.ts +69 -0
  9. package/dist/client-BAoL8h4E.d.cts +69 -0
  10. package/dist/core/index.cjs +696 -0
  11. package/dist/core/index.cjs.map +1 -0
  12. package/dist/core/index.d.cts +3 -0
  13. package/dist/core/index.d.ts +3 -0
  14. package/dist/core/index.js +4 -0
  15. package/dist/core/index.js.map +1 -0
  16. package/dist/errors-BkUDHleb.d.cts +22 -0
  17. package/dist/errors-BkUDHleb.d.ts +22 -0
  18. package/dist/index.cjs +696 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.cts +3 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.js +4 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/react/index.cjs +844 -0
  25. package/dist/react/index.cjs.map +1 -0
  26. package/dist/react/index.d.cts +104 -0
  27. package/dist/react/index.d.ts +104 -0
  28. package/dist/react/index.js +64 -0
  29. package/dist/react/index.js.map +1 -0
  30. package/dist/types-bxA1vonL.d.cts +113 -0
  31. package/dist/types-bxA1vonL.d.ts +113 -0
  32. package/dist/ui/index.cjs +1183 -0
  33. package/dist/ui/index.cjs.map +1 -0
  34. package/dist/ui/index.d.cts +241 -0
  35. package/dist/ui/index.d.ts +241 -0
  36. package/dist/ui/index.js +1109 -0
  37. package/dist/ui/index.js.map +1 -0
  38. package/package.json +81 -0
  39. package/src/core/client.ts +604 -0
  40. package/src/core/errors.ts +91 -0
  41. package/src/core/event-bus.ts +41 -0
  42. package/src/core/index.ts +5 -0
  43. package/src/core/internal/converters.ts +32 -0
  44. package/src/core/storage.ts +79 -0
  45. package/src/core/types.ts +87 -0
  46. package/src/index.ts +1 -0
  47. package/src/react/components/ProtectedRoute.tsx +56 -0
  48. package/src/react/context.tsx +126 -0
  49. package/src/react/hooks/useAuth.ts +75 -0
  50. package/src/react/hooks/useMfa.ts +19 -0
  51. package/src/react/hooks/useSession.ts +16 -0
  52. package/src/react/hooks/useUser.ts +24 -0
  53. package/src/react/index.ts +10 -0
  54. package/src/ui/components/ChangePasswordForm.tsx +105 -0
  55. package/src/ui/components/ForgotPasswordForm.tsx +159 -0
  56. package/src/ui/components/MfaSetupWizard.tsx +136 -0
  57. package/src/ui/components/RegisterForm.tsx +159 -0
  58. package/src/ui/components/SignInForm.tsx +296 -0
  59. package/src/ui/hooks/useChangePasswordForm.ts +81 -0
  60. package/src/ui/hooks/useForgotPasswordForm.ts +109 -0
  61. package/src/ui/hooks/useMfaSetup.ts +93 -0
  62. package/src/ui/hooks/useRegisterForm.ts +120 -0
  63. package/src/ui/hooks/useSignInForm.ts +245 -0
  64. package/src/ui/index.ts +31 -0
@@ -0,0 +1,296 @@
1
+ import { useEffect, useId, useRef, type ReactNode } from 'react'
2
+ import { useSignInForm, type UseSignInFormOptions } from '../hooks/useSignInForm'
3
+
4
+ export interface SignInFormProps extends UseSignInFormOptions {
5
+ className?: string
6
+ labels?: {
7
+ email?: string
8
+ password?: string
9
+ submit?: string
10
+ mfaCode?: string
11
+ mfaSubmit?: string
12
+ newPassword?: string
13
+ confirmNewPassword?: string
14
+ newPasswordSubmit?: string
15
+ mfaSetupScanInstruction?: string
16
+ mfaSetupSecretLabel?: string
17
+ mfaSetupCode?: string
18
+ mfaSetupSubmit?: string
19
+ }
20
+ /** Slot per il link "Password dimenticata?" */
21
+ forgotPasswordLink?: ReactNode
22
+ /** Slot per il link "Crea account" */
23
+ registerLink?: ReactNode
24
+ /**
25
+ * Render del QR code per lo step mfa_setup.
26
+ * Riceve il URI otpauth:// — usa react-qr-code o simile.
27
+ * Default: mostra il URI come testo copiabile.
28
+ */
29
+ renderQrCode?: (uri: string) => ReactNode
30
+ }
31
+
32
+ export function SignInForm({
33
+ className,
34
+ labels = {},
35
+ forgotPasswordLink,
36
+ registerLink,
37
+ renderQrCode,
38
+ onSuccess,
39
+ onError,
40
+ }: SignInFormProps) {
41
+ const uid = useId()
42
+ const form = useSignInForm({ onSuccess, onError })
43
+ const firstInputRef = useRef<HTMLInputElement>(null)
44
+
45
+ useEffect(() => {
46
+ firstInputRef.current?.focus()
47
+ // Auto-start TOTP challenge setup when step transitions to mfa_setup
48
+ if (form.step === 'mfa_setup') {
49
+ void form.mfaSetup.start()
50
+ }
51
+ // eslint-disable-next-line react-hooks/exhaustive-deps
52
+ }, [form.step])
53
+
54
+ const l = {
55
+ email: labels.email ?? 'Email',
56
+ password: labels.password ?? 'Password',
57
+ submit: labels.submit ?? 'Accedi',
58
+ mfaCode: labels.mfaCode ?? 'Codice',
59
+ mfaSubmit: labels.mfaSubmit ?? 'Verifica',
60
+ newPassword: labels.newPassword ?? 'Nuova password',
61
+ confirmNewPassword: labels.confirmNewPassword ?? 'Conferma password',
62
+ newPasswordSubmit: labels.newPasswordSubmit ?? 'Imposta password',
63
+ mfaSetupScanInstruction:
64
+ labels.mfaSetupScanInstruction ??
65
+ "Scansiona il QR code con la tua app authenticator (Google Authenticator, Authy, 1Password…)",
66
+ mfaSetupSecretLabel: labels.mfaSetupSecretLabel ?? 'Oppure inserisci il codice manualmente',
67
+ mfaSetupCode: labels.mfaSetupCode ?? 'Codice a 6 cifre',
68
+ mfaSetupSubmit: labels.mfaSetupSubmit ?? 'Verifica e attiva',
69
+ }
70
+
71
+ const errorId = `${uid}-error`
72
+
73
+ if (form.step === 'mfa') {
74
+ return (
75
+ <form
76
+ onSubmit={form.mfa.onSubmit}
77
+ className={className}
78
+ aria-label="Verifica codice MFA"
79
+ >
80
+ <p>
81
+ Inserisci il codice{' '}
82
+ {form.mfaType === 'TOTP' ? "dall'app authenticator" : 'ricevuto via SMS'}
83
+ </p>
84
+ <div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
85
+ {form.error?.message}
86
+ </div>
87
+ <div>
88
+ <label htmlFor={`${uid}-mfa-code`}>{l.mfaCode}</label>
89
+ <input
90
+ id={`${uid}-mfa-code`}
91
+ ref={firstInputRef}
92
+ type="text"
93
+ inputMode="numeric"
94
+ maxLength={6}
95
+ autoComplete="one-time-code"
96
+ autoFocus
97
+ required
98
+ aria-required="true"
99
+ aria-invalid={!!form.error}
100
+ aria-describedby={form.error ? errorId : undefined}
101
+ value={form.mfa.code}
102
+ onChange={(e) => form.mfa.setCode(e.target.value)}
103
+ />
104
+ </div>
105
+ <button type="submit" disabled={form.isLoading} aria-busy={form.isLoading}>
106
+ {form.isLoading ? (
107
+ <>
108
+ <span aria-hidden="true">...</span>
109
+ <span className="sr-only">Caricamento...</span>
110
+ </>
111
+ ) : (
112
+ l.mfaSubmit
113
+ )}
114
+ </button>
115
+ </form>
116
+ )
117
+ }
118
+
119
+ if (form.step === 'new_password') {
120
+ return (
121
+ <form
122
+ onSubmit={form.newPassword.onSubmit}
123
+ className={className}
124
+ aria-label="Imposta nuova password"
125
+ >
126
+ <p>Devi impostare una nuova password per continuare.</p>
127
+ <div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
128
+ {form.error?.message}
129
+ </div>
130
+ <div>
131
+ <label htmlFor={`${uid}-new-password`}>{l.newPassword}</label>
132
+ <input
133
+ id={`${uid}-new-password`}
134
+ ref={firstInputRef}
135
+ type="password"
136
+ autoComplete="new-password"
137
+ autoFocus
138
+ required
139
+ aria-required="true"
140
+ aria-invalid={!!form.error}
141
+ aria-describedby={form.error ? errorId : undefined}
142
+ value={form.newPassword.password}
143
+ onChange={(e) => form.newPassword.setPassword(e.target.value)}
144
+ />
145
+ </div>
146
+ <div>
147
+ <label htmlFor={`${uid}-confirm-password`}>{l.confirmNewPassword}</label>
148
+ <input
149
+ id={`${uid}-confirm-password`}
150
+ type="password"
151
+ autoComplete="new-password"
152
+ required
153
+ aria-required="true"
154
+ aria-invalid={!!form.error}
155
+ value={form.newPassword.confirmPassword}
156
+ onChange={(e) => form.newPassword.setConfirmPassword(e.target.value)}
157
+ />
158
+ </div>
159
+ <button type="submit" disabled={form.isLoading} aria-busy={form.isLoading}>
160
+ {form.isLoading ? (
161
+ <>
162
+ <span aria-hidden="true">...</span>
163
+ <span className="sr-only">Caricamento...</span>
164
+ </>
165
+ ) : (
166
+ l.newPasswordSubmit
167
+ )}
168
+ </button>
169
+ </form>
170
+ )
171
+ }
172
+
173
+ if (form.step === 'mfa_setup') {
174
+ return (
175
+ <div className={className}>
176
+ <div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
177
+ {form.error?.message}
178
+ </div>
179
+
180
+ {form.isLoading && !form.mfaSetup.qrCodeUri && (
181
+ <p aria-live="polite" aria-atomic="true">
182
+ Inizializzazione in corso...
183
+ </p>
184
+ )}
185
+
186
+ {form.mfaSetup.qrCodeUri && (
187
+ <div>
188
+ <p>{l.mfaSetupScanInstruction}</p>
189
+ {renderQrCode ? (
190
+ renderQrCode(form.mfaSetup.qrCodeUri)
191
+ ) : (
192
+ <code style={{ wordBreak: 'break-all', fontSize: '0.75em' }}>
193
+ {form.mfaSetup.qrCodeUri}
194
+ </code>
195
+ )}
196
+ {form.mfaSetup.secretCode && (
197
+ <p>
198
+ {l.mfaSetupSecretLabel}: <code>{form.mfaSetup.secretCode}</code>
199
+ </p>
200
+ )}
201
+ </div>
202
+ )}
203
+
204
+ <form onSubmit={form.mfaSetup.onVerify} aria-label="Verifica configurazione MFA">
205
+ <div>
206
+ <label htmlFor={`${uid}-totp-code`}>{l.mfaSetupCode}</label>
207
+ <input
208
+ id={`${uid}-totp-code`}
209
+ ref={firstInputRef}
210
+ type="text"
211
+ inputMode="numeric"
212
+ maxLength={6}
213
+ autoComplete="one-time-code"
214
+ autoFocus
215
+ required
216
+ aria-required="true"
217
+ aria-invalid={!!form.error}
218
+ aria-describedby={form.error ? errorId : undefined}
219
+ disabled={form.isLoading && !form.mfaSetup.qrCodeUri}
220
+ value={form.mfaSetup.totpCode}
221
+ onChange={(e) => form.mfaSetup.setTotpCode(e.target.value)}
222
+ />
223
+ </div>
224
+ <button
225
+ type="submit"
226
+ disabled={form.isLoading || form.mfaSetup.totpCode.length !== 6}
227
+ aria-busy={form.isLoading}
228
+ >
229
+ {form.isLoading ? (
230
+ <>
231
+ <span aria-hidden="true">...</span>
232
+ <span className="sr-only">Caricamento...</span>
233
+ </>
234
+ ) : (
235
+ l.mfaSetupSubmit
236
+ )}
237
+ </button>
238
+ </form>
239
+ </div>
240
+ )
241
+ }
242
+
243
+ // Default: credentials step
244
+ return (
245
+ <form
246
+ onSubmit={form.credentials.onSubmit}
247
+ className={className}
248
+ aria-label="Accedi al tuo account"
249
+ >
250
+ <div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
251
+ {form.error?.message}
252
+ </div>
253
+ <div>
254
+ <label htmlFor={`${uid}-email`}>{l.email}</label>
255
+ <input
256
+ id={`${uid}-email`}
257
+ ref={firstInputRef}
258
+ type="email"
259
+ autoComplete="email"
260
+ autoFocus
261
+ required
262
+ aria-required="true"
263
+ aria-invalid={!!form.error}
264
+ aria-describedby={form.error ? errorId : undefined}
265
+ value={form.credentials.email}
266
+ onChange={(e) => form.credentials.setEmail(e.target.value)}
267
+ />
268
+ </div>
269
+ <div>
270
+ <label htmlFor={`${uid}-password`}>{l.password}</label>
271
+ <input
272
+ id={`${uid}-password`}
273
+ type="password"
274
+ autoComplete="current-password"
275
+ required
276
+ aria-required="true"
277
+ aria-invalid={!!form.error}
278
+ value={form.credentials.password}
279
+ onChange={(e) => form.credentials.setPassword(e.target.value)}
280
+ />
281
+ </div>
282
+ {forgotPasswordLink}
283
+ <button type="submit" disabled={form.isLoading} aria-busy={form.isLoading}>
284
+ {form.isLoading ? (
285
+ <>
286
+ <span aria-hidden="true">...</span>
287
+ <span className="sr-only">Caricamento...</span>
288
+ </>
289
+ ) : (
290
+ l.submit
291
+ )}
292
+ </button>
293
+ {registerLink}
294
+ </form>
295
+ )
296
+ }
@@ -0,0 +1,81 @@
1
+ import { useCallback, useState, type FormEvent } from 'react'
2
+ import { useAuth } from '../../react/hooks/useAuth'
3
+ import type { CognitoAuthError } from '../../core/errors'
4
+
5
+ export interface UseChangePasswordFormOptions {
6
+ onSuccess?: () => void
7
+ onError?: (error: CognitoAuthError) => void
8
+ }
9
+
10
+ export interface UseChangePasswordFormReturn {
11
+ isLoading: boolean
12
+ error: CognitoAuthError | null
13
+ success: boolean
14
+ currentPassword: string
15
+ newPassword: string
16
+ confirmPassword: string
17
+ setCurrentPassword(v: string): void
18
+ setNewPassword(v: string): void
19
+ setConfirmPassword(v: string): void
20
+ onSubmit(e: FormEvent): void
21
+ reset(): void
22
+ }
23
+
24
+ export function useChangePasswordForm(
25
+ options: UseChangePasswordFormOptions = {},
26
+ ): UseChangePasswordFormReturn {
27
+ const { changePassword } = useAuth()
28
+
29
+ const [isLoading, setLoading] = useState(false)
30
+ const [error, setError] = useState<CognitoAuthError | null>(null)
31
+ const [success, setSuccess] = useState(false)
32
+ const [currentPassword, setCurrentPassword] = useState('')
33
+ const [newPassword, setNewPassword] = useState('')
34
+ const [confirmPassword, setConfirmPassword] = useState('')
35
+
36
+ const handleSubmit = useCallback(
37
+ async (e: FormEvent) => {
38
+ e.preventDefault()
39
+ if (newPassword !== confirmPassword) {
40
+ setError({ message: 'Le nuove password non coincidono', code: 'INVALID_PASSWORD' } as CognitoAuthError)
41
+ return
42
+ }
43
+ setError(null)
44
+ setLoading(true)
45
+ try {
46
+ await changePassword(currentPassword, newPassword)
47
+ setSuccess(true)
48
+ options.onSuccess?.()
49
+ } catch (err) {
50
+ const authErr = err as CognitoAuthError
51
+ setError(authErr)
52
+ options.onError?.(authErr)
53
+ } finally {
54
+ setLoading(false)
55
+ }
56
+ },
57
+ [currentPassword, newPassword, confirmPassword, changePassword, options],
58
+ )
59
+
60
+ const reset = useCallback(() => {
61
+ setCurrentPassword('')
62
+ setNewPassword('')
63
+ setConfirmPassword('')
64
+ setError(null)
65
+ setSuccess(false)
66
+ }, [])
67
+
68
+ return {
69
+ isLoading,
70
+ error,
71
+ success,
72
+ currentPassword,
73
+ newPassword,
74
+ confirmPassword,
75
+ setCurrentPassword,
76
+ setNewPassword,
77
+ setConfirmPassword,
78
+ onSubmit: handleSubmit,
79
+ reset,
80
+ }
81
+ }
@@ -0,0 +1,109 @@
1
+ import { useCallback, useState, type FormEvent } from 'react'
2
+ import { useAuth } from '../../react/hooks/useAuth'
3
+ import type { CognitoAuthError } from '../../core/errors'
4
+
5
+ export type ForgotPasswordStep = 'request' | 'reset'
6
+
7
+ export interface UseForgotPasswordFormOptions {
8
+ onSuccess?: () => void
9
+ onError?: (error: CognitoAuthError) => void
10
+ }
11
+
12
+ export interface UseForgotPasswordFormReturn {
13
+ step: ForgotPasswordStep
14
+ isLoading: boolean
15
+ error: CognitoAuthError | null
16
+
17
+ request: {
18
+ email: string
19
+ setEmail(v: string): void
20
+ onSubmit(e: FormEvent): void
21
+ }
22
+
23
+ reset: {
24
+ email: string
25
+ code: string
26
+ newPassword: string
27
+ confirmPassword: string
28
+ setCode(v: string): void
29
+ setNewPassword(v: string): void
30
+ setConfirmPassword(v: string): void
31
+ onSubmit(e: FormEvent): void
32
+ }
33
+
34
+ restart(): void
35
+ }
36
+
37
+ export function useForgotPasswordForm(
38
+ options: UseForgotPasswordFormOptions = {},
39
+ ): UseForgotPasswordFormReturn {
40
+ const { forgotPassword, confirmForgotPassword } = useAuth()
41
+
42
+ const [step, setStep] = useState<ForgotPasswordStep>('request')
43
+ const [isLoading, setLoading] = useState(false)
44
+ const [error, setError] = useState<CognitoAuthError | null>(null)
45
+
46
+ const [email, setEmail] = useState('')
47
+ const [code, setCode] = useState('')
48
+ const [newPassword, setNewPassword] = useState('')
49
+ const [confirmPassword, setConfirmPassword] = useState('')
50
+
51
+ const handleRequestSubmit = useCallback(
52
+ async (e: FormEvent) => {
53
+ e.preventDefault()
54
+ setError(null)
55
+ setLoading(true)
56
+ try {
57
+ await forgotPassword(email)
58
+ setStep('reset')
59
+ } catch (err) {
60
+ const authErr = err as CognitoAuthError
61
+ setError(authErr)
62
+ options.onError?.(authErr)
63
+ } finally {
64
+ setLoading(false)
65
+ }
66
+ },
67
+ [email, forgotPassword, options],
68
+ )
69
+
70
+ const handleResetSubmit = useCallback(
71
+ async (e: FormEvent) => {
72
+ e.preventDefault()
73
+ if (newPassword !== confirmPassword) {
74
+ setError({ message: 'Le password non coincidono', code: 'INVALID_PASSWORD' } as CognitoAuthError)
75
+ return
76
+ }
77
+ setError(null)
78
+ setLoading(true)
79
+ try {
80
+ await confirmForgotPassword(email, code, newPassword)
81
+ options.onSuccess?.()
82
+ } catch (err) {
83
+ const authErr = err as CognitoAuthError
84
+ setError(authErr)
85
+ options.onError?.(authErr)
86
+ } finally {
87
+ setLoading(false)
88
+ }
89
+ },
90
+ [email, code, newPassword, confirmPassword, confirmForgotPassword, options],
91
+ )
92
+
93
+ const restart = useCallback(() => {
94
+ setStep('request')
95
+ setCode('')
96
+ setNewPassword('')
97
+ setConfirmPassword('')
98
+ setError(null)
99
+ }, [])
100
+
101
+ return {
102
+ step,
103
+ isLoading,
104
+ error,
105
+ request: { email, setEmail, onSubmit: handleRequestSubmit },
106
+ reset: { email, code, newPassword, confirmPassword, setCode, setNewPassword, setConfirmPassword, onSubmit: handleResetSubmit },
107
+ restart,
108
+ }
109
+ }
@@ -0,0 +1,93 @@
1
+ import { useCallback, useState, type FormEvent } from 'react'
2
+ import { useMfa } from '../../react/hooks/useMfa'
3
+ import type { CognitoAuthError } from '../../core/errors'
4
+
5
+ export type MfaSetupStep = 'idle' | 'loading' | 'scan' | 'verify' | 'done'
6
+
7
+ export interface UseMfaSetupOptions {
8
+ onSuccess?: () => void
9
+ onError?: (error: CognitoAuthError) => void
10
+ }
11
+
12
+ export interface UseMfaSetupReturn {
13
+ step: MfaSetupStep
14
+ isLoading: boolean
15
+ error: CognitoAuthError | null
16
+ /** URI otpauth:// da passare a un QR code renderer */
17
+ qrCodeUri: string | null
18
+ /** Secret testuale (per inserimento manuale nell'app) */
19
+ secretCode: string | null
20
+ totpCode: string
21
+ setTotpCode(v: string): void
22
+ /** Avvia il setup TOTP */
23
+ start(): Promise<void>
24
+ /** Verifica il codice inserito dall'app authenticator */
25
+ onVerify(e: FormEvent): void
26
+ reset(): void
27
+ }
28
+
29
+ export function useMfaSetup(options: UseMfaSetupOptions = {}): UseMfaSetupReturn {
30
+ const { setup, verifySetup } = useMfa()
31
+
32
+ const [step, setStep] = useState<MfaSetupStep>('idle')
33
+ const [error, setError] = useState<CognitoAuthError | null>(null)
34
+ const [qrCodeUri, setQrCodeUri] = useState<string | null>(null)
35
+ const [secretCode, setSecretCode] = useState<string | null>(null)
36
+ const [totpCode, setTotpCode] = useState('')
37
+
38
+ const start = useCallback(async () => {
39
+ setError(null)
40
+ setStep('loading')
41
+ try {
42
+ const result = await setup()
43
+ setQrCodeUri(result.qrCodeUri)
44
+ setSecretCode(result.secretCode)
45
+ setStep('scan')
46
+ } catch (err) {
47
+ const authErr = err as CognitoAuthError
48
+ setError(authErr)
49
+ setStep('idle')
50
+ options.onError?.(authErr)
51
+ }
52
+ }, [setup, options])
53
+
54
+ const handleVerify = useCallback(
55
+ async (e: FormEvent) => {
56
+ e.preventDefault()
57
+ setError(null)
58
+ setStep('loading')
59
+ try {
60
+ await verifySetup(totpCode)
61
+ setStep('done')
62
+ options.onSuccess?.()
63
+ } catch (err) {
64
+ const authErr = err as CognitoAuthError
65
+ setError(authErr)
66
+ setStep('verify')
67
+ options.onError?.(authErr)
68
+ }
69
+ },
70
+ [totpCode, verifySetup, options],
71
+ )
72
+
73
+ const reset = useCallback(() => {
74
+ setStep('idle')
75
+ setError(null)
76
+ setQrCodeUri(null)
77
+ setSecretCode(null)
78
+ setTotpCode('')
79
+ }, [])
80
+
81
+ return {
82
+ step,
83
+ isLoading: step === 'loading',
84
+ error,
85
+ qrCodeUri,
86
+ secretCode,
87
+ totpCode,
88
+ setTotpCode,
89
+ start,
90
+ onVerify: handleVerify,
91
+ reset,
92
+ }
93
+ }
@@ -0,0 +1,120 @@
1
+ import { useCallback, useState, type FormEvent } from 'react'
2
+ import { useAuth } from '../../react/hooks/useAuth'
3
+ import type { CognitoAuthError } from '../../core/errors'
4
+
5
+ export type RegisterStep = 'register' | 'confirm'
6
+
7
+ export interface UseRegisterFormOptions {
8
+ onSuccess?: () => void
9
+ onError?: (error: CognitoAuthError) => void
10
+ /** Attributi aggiuntivi (es. name, phone_number) */
11
+ extraAttributes?: Record<string, string>
12
+ }
13
+
14
+ export interface UseRegisterFormReturn {
15
+ step: RegisterStep
16
+ isLoading: boolean
17
+ error: CognitoAuthError | null
18
+
19
+ register: {
20
+ email: string
21
+ password: string
22
+ confirmPassword: string
23
+ setEmail(v: string): void
24
+ setPassword(v: string): void
25
+ setConfirmPassword(v: string): void
26
+ onSubmit(e: FormEvent): void
27
+ }
28
+
29
+ confirm: {
30
+ email: string
31
+ code: string
32
+ setCode(v: string): void
33
+ onSubmit(e: FormEvent): void
34
+ resend(): Promise<void>
35
+ }
36
+
37
+ reset(): void
38
+ }
39
+
40
+ export function useRegisterForm(options: UseRegisterFormOptions = {}): UseRegisterFormReturn {
41
+ const { signUp, confirmSignUp, resendConfirmationCode } = useAuth()
42
+
43
+ const [step, setStep] = useState<RegisterStep>('register')
44
+ const [isLoading, setLoading] = useState(false)
45
+ const [error, setError] = useState<CognitoAuthError | null>(null)
46
+
47
+ const [email, setEmail] = useState('')
48
+ const [password, setPassword] = useState('')
49
+ const [confirmPassword, setConfirmPassword] = useState('')
50
+ const [code, setCode] = useState('')
51
+
52
+ const handleRegisterSubmit = useCallback(
53
+ async (e: FormEvent) => {
54
+ e.preventDefault()
55
+ if (password !== confirmPassword) {
56
+ setError({ message: 'Le password non coincidono', code: 'INVALID_PASSWORD' } as CognitoAuthError)
57
+ return
58
+ }
59
+ setError(null)
60
+ setLoading(true)
61
+ try {
62
+ await signUp(email, password, options.extraAttributes)
63
+ setStep('confirm')
64
+ } catch (err) {
65
+ const authErr = err as CognitoAuthError
66
+ setError(authErr)
67
+ options.onError?.(authErr)
68
+ } finally {
69
+ setLoading(false)
70
+ }
71
+ },
72
+ [email, password, confirmPassword, signUp, options],
73
+ )
74
+
75
+ const handleConfirmSubmit = useCallback(
76
+ async (e: FormEvent) => {
77
+ e.preventDefault()
78
+ setError(null)
79
+ setLoading(true)
80
+ try {
81
+ await confirmSignUp(email, code)
82
+ options.onSuccess?.()
83
+ } catch (err) {
84
+ const authErr = err as CognitoAuthError
85
+ setError(authErr)
86
+ options.onError?.(authErr)
87
+ } finally {
88
+ setLoading(false)
89
+ }
90
+ },
91
+ [email, code, confirmSignUp, options],
92
+ )
93
+
94
+ const resend = useCallback(async () => {
95
+ setError(null)
96
+ try {
97
+ await resendConfirmationCode(email)
98
+ } catch (err) {
99
+ setError(err as CognitoAuthError)
100
+ }
101
+ }, [email, resendConfirmationCode])
102
+
103
+ const reset = useCallback(() => {
104
+ setStep('register')
105
+ setEmail('')
106
+ setPassword('')
107
+ setConfirmPassword('')
108
+ setCode('')
109
+ setError(null)
110
+ }, [])
111
+
112
+ return {
113
+ step,
114
+ isLoading,
115
+ error,
116
+ register: { email, password, confirmPassword, setEmail, setPassword, setConfirmPassword, onSubmit: handleRegisterSubmit },
117
+ confirm: { email, code, setCode, onSubmit: handleConfirmSubmit, resend },
118
+ reset,
119
+ }
120
+ }