@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.
- package/README.md +2410 -0
- package/dist/chunk-AD7T42HJ.js +3 -0
- package/dist/chunk-AD7T42HJ.js.map +1 -0
- package/dist/chunk-DKPFVGTY.js +683 -0
- package/dist/chunk-DKPFVGTY.js.map +1 -0
- package/dist/chunk-N4OQLBV6.js +135 -0
- package/dist/chunk-N4OQLBV6.js.map +1 -0
- package/dist/client-63FraVdm.d.ts +69 -0
- package/dist/client-BAoL8h4E.d.cts +69 -0
- package/dist/core/index.cjs +696 -0
- package/dist/core/index.cjs.map +1 -0
- package/dist/core/index.d.cts +3 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +4 -0
- package/dist/core/index.js.map +1 -0
- package/dist/errors-BkUDHleb.d.cts +22 -0
- package/dist/errors-BkUDHleb.d.ts +22 -0
- package/dist/index.cjs +696 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +844 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +104 -0
- package/dist/react/index.d.ts +104 -0
- package/dist/react/index.js +64 -0
- package/dist/react/index.js.map +1 -0
- package/dist/types-bxA1vonL.d.cts +113 -0
- package/dist/types-bxA1vonL.d.ts +113 -0
- package/dist/ui/index.cjs +1183 -0
- package/dist/ui/index.cjs.map +1 -0
- package/dist/ui/index.d.cts +241 -0
- package/dist/ui/index.d.ts +241 -0
- package/dist/ui/index.js +1109 -0
- package/dist/ui/index.js.map +1 -0
- package/package.json +81 -0
- package/src/core/client.ts +604 -0
- package/src/core/errors.ts +91 -0
- package/src/core/event-bus.ts +41 -0
- package/src/core/index.ts +5 -0
- package/src/core/internal/converters.ts +32 -0
- package/src/core/storage.ts +79 -0
- package/src/core/types.ts +87 -0
- package/src/index.ts +1 -0
- package/src/react/components/ProtectedRoute.tsx +56 -0
- package/src/react/context.tsx +126 -0
- package/src/react/hooks/useAuth.ts +75 -0
- package/src/react/hooks/useMfa.ts +19 -0
- package/src/react/hooks/useSession.ts +16 -0
- package/src/react/hooks/useUser.ts +24 -0
- package/src/react/index.ts +10 -0
- package/src/ui/components/ChangePasswordForm.tsx +105 -0
- package/src/ui/components/ForgotPasswordForm.tsx +159 -0
- package/src/ui/components/MfaSetupWizard.tsx +136 -0
- package/src/ui/components/RegisterForm.tsx +159 -0
- package/src/ui/components/SignInForm.tsx +296 -0
- package/src/ui/hooks/useChangePasswordForm.ts +81 -0
- package/src/ui/hooks/useForgotPasswordForm.ts +109 -0
- package/src/ui/hooks/useMfaSetup.ts +93 -0
- package/src/ui/hooks/useRegisterForm.ts +120 -0
- package/src/ui/hooks/useSignInForm.ts +245 -0
- package/src/ui/index.ts +31 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useEffect, useId, useRef, type ReactNode } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
useForgotPasswordForm,
|
|
4
|
+
type UseForgotPasswordFormOptions,
|
|
5
|
+
} from '../hooks/useForgotPasswordForm'
|
|
6
|
+
|
|
7
|
+
export interface ForgotPasswordFormProps extends UseForgotPasswordFormOptions {
|
|
8
|
+
className?: string
|
|
9
|
+
labels?: {
|
|
10
|
+
email?: string
|
|
11
|
+
requestSubmit?: string
|
|
12
|
+
code?: string
|
|
13
|
+
newPassword?: string
|
|
14
|
+
confirmPassword?: string
|
|
15
|
+
resetSubmit?: string
|
|
16
|
+
}
|
|
17
|
+
signInLink?: ReactNode
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ForgotPasswordForm({
|
|
21
|
+
className,
|
|
22
|
+
labels = {},
|
|
23
|
+
signInLink,
|
|
24
|
+
onSuccess,
|
|
25
|
+
onError,
|
|
26
|
+
}: ForgotPasswordFormProps) {
|
|
27
|
+
const uid = useId()
|
|
28
|
+
const form = useForgotPasswordForm({ onSuccess, onError })
|
|
29
|
+
const firstInputRef = useRef<HTMLInputElement>(null)
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
firstInputRef.current?.focus()
|
|
33
|
+
}, [form.step])
|
|
34
|
+
|
|
35
|
+
const l = {
|
|
36
|
+
email: labels.email ?? 'Email',
|
|
37
|
+
requestSubmit: labels.requestSubmit ?? 'Invia codice',
|
|
38
|
+
code: labels.code ?? 'Codice di verifica',
|
|
39
|
+
newPassword: labels.newPassword ?? 'Nuova password',
|
|
40
|
+
confirmPassword: labels.confirmPassword ?? 'Conferma password',
|
|
41
|
+
resetSubmit: labels.resetSubmit ?? 'Reimposta password',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const errorId = `${uid}-error`
|
|
45
|
+
|
|
46
|
+
if (form.step === 'reset') {
|
|
47
|
+
return (
|
|
48
|
+
<form
|
|
49
|
+
onSubmit={form.reset.onSubmit}
|
|
50
|
+
className={className}
|
|
51
|
+
aria-label="Reimposta password"
|
|
52
|
+
>
|
|
53
|
+
<p>
|
|
54
|
+
Abbiamo inviato un codice a <strong>{form.reset.email}</strong>.
|
|
55
|
+
</p>
|
|
56
|
+
<div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
|
|
57
|
+
{form.error?.message}
|
|
58
|
+
</div>
|
|
59
|
+
<div>
|
|
60
|
+
<label htmlFor={`${uid}-code`}>{l.code}</label>
|
|
61
|
+
<input
|
|
62
|
+
id={`${uid}-code`}
|
|
63
|
+
ref={firstInputRef}
|
|
64
|
+
type="text"
|
|
65
|
+
inputMode="numeric"
|
|
66
|
+
maxLength={6}
|
|
67
|
+
autoComplete="one-time-code"
|
|
68
|
+
autoFocus
|
|
69
|
+
required
|
|
70
|
+
aria-required="true"
|
|
71
|
+
aria-invalid={!!form.error}
|
|
72
|
+
aria-describedby={form.error ? errorId : undefined}
|
|
73
|
+
value={form.reset.code}
|
|
74
|
+
onChange={(e) => form.reset.setCode(e.target.value)}
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
<div>
|
|
78
|
+
<label htmlFor={`${uid}-new-password`}>{l.newPassword}</label>
|
|
79
|
+
<input
|
|
80
|
+
id={`${uid}-new-password`}
|
|
81
|
+
type="password"
|
|
82
|
+
autoComplete="new-password"
|
|
83
|
+
required
|
|
84
|
+
aria-required="true"
|
|
85
|
+
aria-invalid={!!form.error}
|
|
86
|
+
value={form.reset.newPassword}
|
|
87
|
+
onChange={(e) => form.reset.setNewPassword(e.target.value)}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
<div>
|
|
91
|
+
<label htmlFor={`${uid}-confirm-password`}>{l.confirmPassword}</label>
|
|
92
|
+
<input
|
|
93
|
+
id={`${uid}-confirm-password`}
|
|
94
|
+
type="password"
|
|
95
|
+
autoComplete="new-password"
|
|
96
|
+
required
|
|
97
|
+
aria-required="true"
|
|
98
|
+
aria-invalid={!!form.error}
|
|
99
|
+
value={form.reset.confirmPassword}
|
|
100
|
+
onChange={(e) => form.reset.setConfirmPassword(e.target.value)}
|
|
101
|
+
/>
|
|
102
|
+
</div>
|
|
103
|
+
<button type="submit" disabled={form.isLoading} aria-busy={form.isLoading}>
|
|
104
|
+
{form.isLoading ? (
|
|
105
|
+
<>
|
|
106
|
+
<span aria-hidden="true">...</span>
|
|
107
|
+
<span className="sr-only">Caricamento...</span>
|
|
108
|
+
</>
|
|
109
|
+
) : (
|
|
110
|
+
l.resetSubmit
|
|
111
|
+
)}
|
|
112
|
+
</button>
|
|
113
|
+
<button type="button" onClick={form.restart}>
|
|
114
|
+
Torna all'inizio
|
|
115
|
+
</button>
|
|
116
|
+
</form>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<form
|
|
122
|
+
onSubmit={form.request.onSubmit}
|
|
123
|
+
className={className}
|
|
124
|
+
aria-label="Richiedi codice di reimpostazione password"
|
|
125
|
+
>
|
|
126
|
+
<p>Inserisci la tua email per ricevere il codice di reimpostazione.</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}-email`}>{l.email}</label>
|
|
132
|
+
<input
|
|
133
|
+
id={`${uid}-email`}
|
|
134
|
+
ref={firstInputRef}
|
|
135
|
+
type="email"
|
|
136
|
+
autoComplete="email"
|
|
137
|
+
autoFocus
|
|
138
|
+
required
|
|
139
|
+
aria-required="true"
|
|
140
|
+
aria-invalid={!!form.error}
|
|
141
|
+
aria-describedby={form.error ? errorId : undefined}
|
|
142
|
+
value={form.request.email}
|
|
143
|
+
onChange={(e) => form.request.setEmail(e.target.value)}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
<button type="submit" disabled={form.isLoading} aria-busy={form.isLoading}>
|
|
147
|
+
{form.isLoading ? (
|
|
148
|
+
<>
|
|
149
|
+
<span aria-hidden="true">...</span>
|
|
150
|
+
<span className="sr-only">Caricamento...</span>
|
|
151
|
+
</>
|
|
152
|
+
) : (
|
|
153
|
+
l.requestSubmit
|
|
154
|
+
)}
|
|
155
|
+
</button>
|
|
156
|
+
{signInLink}
|
|
157
|
+
</form>
|
|
158
|
+
)
|
|
159
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useEffect, useId, useRef, type ReactNode } from 'react'
|
|
2
|
+
import { useMfaSetup, type UseMfaSetupOptions } from '../hooks/useMfaSetup'
|
|
3
|
+
|
|
4
|
+
export interface MfaSetupWizardProps extends UseMfaSetupOptions {
|
|
5
|
+
className?: string
|
|
6
|
+
labels?: {
|
|
7
|
+
start?: string
|
|
8
|
+
scanInstruction?: string
|
|
9
|
+
manualEntry?: string
|
|
10
|
+
codeLabel?: string
|
|
11
|
+
verifySubmit?: string
|
|
12
|
+
successMessage?: string
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Render del QR code. Riceve il URI otpauth:// — usa react-qr-code o simile.
|
|
16
|
+
* Default: mostra il URI come testo copiabile.
|
|
17
|
+
*/
|
|
18
|
+
renderQrCode?: (uri: string) => ReactNode
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function MfaSetupWizard({
|
|
22
|
+
className,
|
|
23
|
+
labels = {},
|
|
24
|
+
renderQrCode,
|
|
25
|
+
onSuccess,
|
|
26
|
+
onError,
|
|
27
|
+
}: MfaSetupWizardProps) {
|
|
28
|
+
const uid = useId()
|
|
29
|
+
const wizard = useMfaSetup({ onSuccess, onError })
|
|
30
|
+
const firstInputRef = useRef<HTMLInputElement>(null)
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
firstInputRef.current?.focus()
|
|
34
|
+
}, [wizard.step])
|
|
35
|
+
|
|
36
|
+
const l = {
|
|
37
|
+
start: labels.start ?? 'Configura autenticazione a due fattori (TOTP)',
|
|
38
|
+
scanInstruction:
|
|
39
|
+
labels.scanInstruction ??
|
|
40
|
+
'Scansiona il QR code con la tua app authenticator (Google Authenticator, Authy, 1Password…)',
|
|
41
|
+
manualEntry: labels.manualEntry ?? 'Oppure inserisci il codice manualmente:',
|
|
42
|
+
codeLabel: labels.codeLabel ?? 'Codice a 6 cifre',
|
|
43
|
+
verifySubmit: labels.verifySubmit ?? 'Verifica e attiva',
|
|
44
|
+
successMessage: labels.successMessage ?? 'Autenticazione a due fattori attivata con successo.',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const errorId = `${uid}-error`
|
|
48
|
+
|
|
49
|
+
if (wizard.step === 'done') {
|
|
50
|
+
return <p role="status">{l.successMessage}</p>
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (wizard.step === 'idle') {
|
|
54
|
+
return (
|
|
55
|
+
<div className={className}>
|
|
56
|
+
<div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
|
|
57
|
+
{wizard.error?.message}
|
|
58
|
+
</div>
|
|
59
|
+
{/* autoFocus handles focus on initial render; firstInputRef targets inputs in later steps */}
|
|
60
|
+
<button type="button" autoFocus onClick={wizard.start}>
|
|
61
|
+
{l.start}
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (wizard.step === 'loading') {
|
|
68
|
+
return (
|
|
69
|
+
<div className={className} aria-live="polite" aria-atomic="true">
|
|
70
|
+
...
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// step === 'scan' | 'verify'
|
|
76
|
+
return (
|
|
77
|
+
<div className={className}>
|
|
78
|
+
<div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
|
|
79
|
+
{wizard.error?.message}
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{wizard.qrCodeUri && (
|
|
83
|
+
<div>
|
|
84
|
+
<p>{l.scanInstruction}</p>
|
|
85
|
+
{renderQrCode ? (
|
|
86
|
+
renderQrCode(wizard.qrCodeUri)
|
|
87
|
+
) : (
|
|
88
|
+
<code style={{ wordBreak: 'break-all', fontSize: '0.75em' }}>
|
|
89
|
+
{wizard.qrCodeUri}
|
|
90
|
+
</code>
|
|
91
|
+
)}
|
|
92
|
+
{wizard.secretCode && (
|
|
93
|
+
<p>
|
|
94
|
+
{l.manualEntry} <code>{wizard.secretCode}</code>
|
|
95
|
+
</p>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
<form onSubmit={wizard.onVerify} aria-label="Verifica dispositivo TOTP">
|
|
101
|
+
<div>
|
|
102
|
+
<label htmlFor={`${uid}-totp-code`}>{l.codeLabel}</label>
|
|
103
|
+
<input
|
|
104
|
+
id={`${uid}-totp-code`}
|
|
105
|
+
ref={firstInputRef}
|
|
106
|
+
type="text"
|
|
107
|
+
inputMode="numeric"
|
|
108
|
+
maxLength={6}
|
|
109
|
+
autoComplete="one-time-code"
|
|
110
|
+
autoFocus
|
|
111
|
+
required
|
|
112
|
+
aria-required="true"
|
|
113
|
+
aria-invalid={!!wizard.error}
|
|
114
|
+
aria-describedby={wizard.error ? errorId : undefined}
|
|
115
|
+
value={wizard.totpCode}
|
|
116
|
+
onChange={(e) => wizard.setTotpCode(e.target.value)}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
<button
|
|
120
|
+
type="submit"
|
|
121
|
+
disabled={wizard.isLoading || wizard.totpCode.length !== 6}
|
|
122
|
+
aria-busy={wizard.isLoading}
|
|
123
|
+
>
|
|
124
|
+
{wizard.isLoading ? (
|
|
125
|
+
<>
|
|
126
|
+
<span aria-hidden="true">...</span>
|
|
127
|
+
<span className="sr-only">Caricamento...</span>
|
|
128
|
+
</>
|
|
129
|
+
) : (
|
|
130
|
+
l.verifySubmit
|
|
131
|
+
)}
|
|
132
|
+
</button>
|
|
133
|
+
</form>
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { useEffect, useId, useRef, type ReactNode } from 'react'
|
|
2
|
+
import { useRegisterForm, type UseRegisterFormOptions } from '../hooks/useRegisterForm'
|
|
3
|
+
|
|
4
|
+
export interface RegisterFormProps extends UseRegisterFormOptions {
|
|
5
|
+
className?: string
|
|
6
|
+
labels?: {
|
|
7
|
+
email?: string
|
|
8
|
+
password?: string
|
|
9
|
+
confirmPassword?: string
|
|
10
|
+
submit?: string
|
|
11
|
+
code?: string
|
|
12
|
+
confirmSubmit?: string
|
|
13
|
+
resend?: string
|
|
14
|
+
}
|
|
15
|
+
signInLink?: ReactNode
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function RegisterForm({
|
|
19
|
+
className,
|
|
20
|
+
labels = {},
|
|
21
|
+
signInLink,
|
|
22
|
+
onSuccess,
|
|
23
|
+
onError,
|
|
24
|
+
extraAttributes,
|
|
25
|
+
}: RegisterFormProps) {
|
|
26
|
+
const uid = useId()
|
|
27
|
+
const form = useRegisterForm({ onSuccess, onError, extraAttributes })
|
|
28
|
+
const firstInputRef = useRef<HTMLInputElement>(null)
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
firstInputRef.current?.focus()
|
|
32
|
+
}, [form.step])
|
|
33
|
+
|
|
34
|
+
const l = {
|
|
35
|
+
email: labels.email ?? 'Email',
|
|
36
|
+
password: labels.password ?? 'Password',
|
|
37
|
+
confirmPassword: labels.confirmPassword ?? 'Conferma password',
|
|
38
|
+
submit: labels.submit ?? 'Registrati',
|
|
39
|
+
code: labels.code ?? 'Codice di verifica',
|
|
40
|
+
confirmSubmit: labels.confirmSubmit ?? 'Conferma',
|
|
41
|
+
resend: labels.resend ?? 'Invia di nuovo',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const errorId = `${uid}-error`
|
|
45
|
+
|
|
46
|
+
if (form.step === 'confirm') {
|
|
47
|
+
return (
|
|
48
|
+
<form
|
|
49
|
+
onSubmit={form.confirm.onSubmit}
|
|
50
|
+
className={className}
|
|
51
|
+
aria-label="Conferma registrazione"
|
|
52
|
+
>
|
|
53
|
+
<p>
|
|
54
|
+
Controlla la tua email <strong>{form.confirm.email}</strong> e inserisci il codice
|
|
55
|
+
ricevuto.
|
|
56
|
+
</p>
|
|
57
|
+
<div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
|
|
58
|
+
{form.error?.message}
|
|
59
|
+
</div>
|
|
60
|
+
<div>
|
|
61
|
+
<label htmlFor={`${uid}-code`}>{l.code}</label>
|
|
62
|
+
<input
|
|
63
|
+
id={`${uid}-code`}
|
|
64
|
+
ref={firstInputRef}
|
|
65
|
+
type="text"
|
|
66
|
+
inputMode="numeric"
|
|
67
|
+
maxLength={6}
|
|
68
|
+
autoComplete="one-time-code"
|
|
69
|
+
autoFocus
|
|
70
|
+
required
|
|
71
|
+
aria-required="true"
|
|
72
|
+
aria-invalid={!!form.error}
|
|
73
|
+
aria-describedby={form.error ? errorId : undefined}
|
|
74
|
+
value={form.confirm.code}
|
|
75
|
+
onChange={(e) => form.confirm.setCode(e.target.value)}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
<button type="submit" disabled={form.isLoading} aria-busy={form.isLoading}>
|
|
79
|
+
{form.isLoading ? (
|
|
80
|
+
<>
|
|
81
|
+
<span aria-hidden="true">...</span>
|
|
82
|
+
<span className="sr-only">Caricamento...</span>
|
|
83
|
+
</>
|
|
84
|
+
) : (
|
|
85
|
+
l.confirmSubmit
|
|
86
|
+
)}
|
|
87
|
+
</button>
|
|
88
|
+
<button type="button" onClick={form.confirm.resend}>
|
|
89
|
+
{l.resend}
|
|
90
|
+
</button>
|
|
91
|
+
</form>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<form
|
|
97
|
+
onSubmit={form.register.onSubmit}
|
|
98
|
+
className={className}
|
|
99
|
+
aria-label="Crea un nuovo account"
|
|
100
|
+
>
|
|
101
|
+
<div id={errorId} role="alert" aria-live="assertive" aria-atomic="true">
|
|
102
|
+
{form.error?.message}
|
|
103
|
+
</div>
|
|
104
|
+
<div>
|
|
105
|
+
<label htmlFor={`${uid}-email`}>{l.email}</label>
|
|
106
|
+
<input
|
|
107
|
+
id={`${uid}-email`}
|
|
108
|
+
ref={firstInputRef}
|
|
109
|
+
type="email"
|
|
110
|
+
autoComplete="email"
|
|
111
|
+
autoFocus
|
|
112
|
+
required
|
|
113
|
+
aria-required="true"
|
|
114
|
+
aria-invalid={!!form.error}
|
|
115
|
+
aria-describedby={form.error ? errorId : undefined}
|
|
116
|
+
value={form.register.email}
|
|
117
|
+
onChange={(e) => form.register.setEmail(e.target.value)}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
<div>
|
|
121
|
+
<label htmlFor={`${uid}-password`}>{l.password}</label>
|
|
122
|
+
<input
|
|
123
|
+
id={`${uid}-password`}
|
|
124
|
+
type="password"
|
|
125
|
+
autoComplete="new-password"
|
|
126
|
+
required
|
|
127
|
+
aria-required="true"
|
|
128
|
+
aria-invalid={!!form.error}
|
|
129
|
+
value={form.register.password}
|
|
130
|
+
onChange={(e) => form.register.setPassword(e.target.value)}
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
<div>
|
|
134
|
+
<label htmlFor={`${uid}-confirm-password`}>{l.confirmPassword}</label>
|
|
135
|
+
<input
|
|
136
|
+
id={`${uid}-confirm-password`}
|
|
137
|
+
type="password"
|
|
138
|
+
autoComplete="new-password"
|
|
139
|
+
required
|
|
140
|
+
aria-required="true"
|
|
141
|
+
aria-invalid={!!form.error}
|
|
142
|
+
value={form.register.confirmPassword}
|
|
143
|
+
onChange={(e) => form.register.setConfirmPassword(e.target.value)}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
<button type="submit" disabled={form.isLoading} aria-busy={form.isLoading}>
|
|
147
|
+
{form.isLoading ? (
|
|
148
|
+
<>
|
|
149
|
+
<span aria-hidden="true">...</span>
|
|
150
|
+
<span className="sr-only">Caricamento...</span>
|
|
151
|
+
</>
|
|
152
|
+
) : (
|
|
153
|
+
l.submit
|
|
154
|
+
)}
|
|
155
|
+
</button>
|
|
156
|
+
{signInLink}
|
|
157
|
+
</form>
|
|
158
|
+
)
|
|
159
|
+
}
|