@erikey/react 0.4.26 → 0.4.28

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 (145) hide show
  1. package/dist/index.mjs +1 -1
  2. package/dist/index.mjs.map +1 -1
  3. package/package.json +2 -1
  4. package/src/__tests__/auth-client.test.ts +105 -0
  5. package/src/__tests__/security/localStorage-encryption.test.ts +171 -0
  6. package/src/auth-client.ts +158 -0
  7. package/src/dashboard-client.ts +60 -0
  8. package/src/index.ts +88 -0
  9. package/src/kv-client.ts +316 -0
  10. package/src/lib/cross-origin-auth.ts +99 -0
  11. package/src/stubs/captcha.ts +24 -0
  12. package/src/stubs/hashes.ts +16 -0
  13. package/src/stubs/index.ts +17 -0
  14. package/src/stubs/passkey.ts +12 -0
  15. package/src/stubs/qr-code.ts +10 -0
  16. package/src/stubs/query.ts +16 -0
  17. package/src/stubs/realtime.ts +17 -0
  18. package/src/stubs/use-sync-external-store.ts +12 -0
  19. package/src/styles.css +141 -0
  20. package/src/types.ts +14 -0
  21. package/src/ui/components/auth/auth-callback.tsx +36 -0
  22. package/src/ui/components/auth/auth-form.tsx +310 -0
  23. package/src/ui/components/auth/auth-view.tsx +435 -0
  24. package/src/ui/components/auth/email-otp-button.tsx +53 -0
  25. package/src/ui/components/auth/forms/email-otp-form.tsx +312 -0
  26. package/src/ui/components/auth/forms/email-verification-form.tsx +271 -0
  27. package/src/ui/components/auth/forms/forgot-password-form.tsx +173 -0
  28. package/src/ui/components/auth/forms/magic-link-form.tsx +196 -0
  29. package/src/ui/components/auth/forms/recover-account-form.tsx +143 -0
  30. package/src/ui/components/auth/forms/reset-password-form.tsx +220 -0
  31. package/src/ui/components/auth/forms/sign-in-form.tsx +323 -0
  32. package/src/ui/components/auth/forms/sign-up-form.tsx +820 -0
  33. package/src/ui/components/auth/forms/two-factor-form.tsx +381 -0
  34. package/src/ui/components/auth/magic-link-button.tsx +54 -0
  35. package/src/ui/components/auth/one-tap.tsx +53 -0
  36. package/src/ui/components/auth/otp-input-group.tsx +65 -0
  37. package/src/ui/components/auth/passkey-button.tsx +91 -0
  38. package/src/ui/components/auth/provider-button.tsx +155 -0
  39. package/src/ui/components/auth/sign-out.tsx +25 -0
  40. package/src/ui/components/auth/wallet-button.tsx +192 -0
  41. package/src/ui/components/auth-loading.tsx +21 -0
  42. package/src/ui/components/captcha/captcha.tsx +91 -0
  43. package/src/ui/components/captcha/recaptcha-badge.tsx +61 -0
  44. package/src/ui/components/captcha/recaptcha-v2.tsx +58 -0
  45. package/src/ui/components/captcha/recaptcha-v3.tsx +73 -0
  46. package/src/ui/components/email/email-template.tsx +216 -0
  47. package/src/ui/components/form-error.tsx +27 -0
  48. package/src/ui/components/password-input.tsx +56 -0
  49. package/src/ui/components/provider-icons.tsx +404 -0
  50. package/src/ui/components/redirect-to-sign-in.tsx +16 -0
  51. package/src/ui/components/redirect-to-sign-up.tsx +16 -0
  52. package/src/ui/components/signed-in.tsx +20 -0
  53. package/src/ui/components/signed-out.tsx +20 -0
  54. package/src/ui/components/ui/alert.tsx +66 -0
  55. package/src/ui/components/ui/button.tsx +70 -0
  56. package/src/ui/components/ui/card.tsx +92 -0
  57. package/src/ui/components/ui/checkbox.tsx +66 -0
  58. package/src/ui/components/ui/field.tsx +248 -0
  59. package/src/ui/components/ui/form.tsx +165 -0
  60. package/src/ui/components/ui/input-otp.tsx +77 -0
  61. package/src/ui/components/ui/input.tsx +21 -0
  62. package/src/ui/components/ui/label.tsx +23 -0
  63. package/src/ui/components/ui/separator.tsx +34 -0
  64. package/src/ui/components/ui/skeleton.tsx +13 -0
  65. package/src/ui/components/ui/textarea.tsx +18 -0
  66. package/src/ui/components/user-avatar.tsx +151 -0
  67. package/src/ui/hooks/use-auth-data.ts +193 -0
  68. package/src/ui/hooks/use-authenticate.ts +64 -0
  69. package/src/ui/hooks/use-captcha.tsx +151 -0
  70. package/src/ui/hooks/use-hydrated.ts +13 -0
  71. package/src/ui/hooks/use-lang.ts +32 -0
  72. package/src/ui/hooks/use-success-transition.ts +41 -0
  73. package/src/ui/hooks/use-theme.ts +39 -0
  74. package/src/ui/index.ts +46 -0
  75. package/src/ui/instantdb.ts +1 -0
  76. package/src/ui/lib/auth-data-cache.ts +90 -0
  77. package/src/ui/lib/auth-ui-provider.tsx +769 -0
  78. package/src/ui/lib/gravatar-utils.ts +58 -0
  79. package/src/ui/lib/image-utils.ts +55 -0
  80. package/src/ui/lib/instantdb/model-names.ts +24 -0
  81. package/src/ui/lib/instantdb/use-instant-options.ts +98 -0
  82. package/src/ui/lib/instantdb/use-list-accounts.ts +38 -0
  83. package/src/ui/lib/instantdb/use-list-sessions.ts +53 -0
  84. package/src/ui/lib/instantdb/use-session.ts +55 -0
  85. package/src/ui/lib/social-providers.ts +150 -0
  86. package/src/ui/lib/tanstack/auth-ui-provider-tanstack.tsx +49 -0
  87. package/src/ui/lib/tanstack/use-tanstack-options.ts +112 -0
  88. package/src/ui/lib/triplit/model-names.ts +24 -0
  89. package/src/ui/lib/triplit/use-conditional-query.ts +82 -0
  90. package/src/ui/lib/triplit/use-list-accounts.ts +31 -0
  91. package/src/ui/lib/triplit/use-list-sessions.ts +33 -0
  92. package/src/ui/lib/triplit/use-session.ts +42 -0
  93. package/src/ui/lib/triplit/use-triplit-hooks.ts +68 -0
  94. package/src/ui/lib/triplit/use-triplit-token.ts +44 -0
  95. package/src/ui/lib/utils.ts +119 -0
  96. package/src/ui/lib/view-paths.ts +61 -0
  97. package/src/ui/lib/wallet.ts +129 -0
  98. package/src/ui/localization/admin-error-codes.ts +20 -0
  99. package/src/ui/localization/anonymous-error-codes.ts +6 -0
  100. package/src/ui/localization/api-key-error-codes.ts +32 -0
  101. package/src/ui/localization/auth-localization.ts +865 -0
  102. package/src/ui/localization/base-error-codes.ts +27 -0
  103. package/src/ui/localization/captcha-error-codes.ts +17 -0
  104. package/src/ui/localization/email-otp-error-codes.ts +7 -0
  105. package/src/ui/localization/generic-oauth-error-codes.ts +3 -0
  106. package/src/ui/localization/haveibeenpwned-error-codes.ts +4 -0
  107. package/src/ui/localization/multi-session-error-codes.ts +3 -0
  108. package/src/ui/localization/organization-error-codes.ts +57 -0
  109. package/src/ui/localization/passkey-error-codes.ts +10 -0
  110. package/src/ui/localization/phone-number-error-codes.ts +10 -0
  111. package/src/ui/localization/stripe-localization.ts +12 -0
  112. package/src/ui/localization/team-error-codes.ts +12 -0
  113. package/src/ui/localization/two-factor-error-codes.ts +12 -0
  114. package/src/ui/localization/username-error-codes.ts +9 -0
  115. package/src/ui/server.ts +4 -0
  116. package/src/ui/style.css +146 -0
  117. package/src/ui/tanstack.ts +1 -0
  118. package/src/ui/triplit.ts +1 -0
  119. package/src/ui/types/account-options.ts +35 -0
  120. package/src/ui/types/additional-fields.ts +21 -0
  121. package/src/ui/types/any-auth-client.ts +6 -0
  122. package/src/ui/types/api-key.ts +9 -0
  123. package/src/ui/types/auth-client.ts +41 -0
  124. package/src/ui/types/auth-hooks.ts +81 -0
  125. package/src/ui/types/auth-mutators.ts +21 -0
  126. package/src/ui/types/avatar-options.ts +29 -0
  127. package/src/ui/types/captcha-options.ts +32 -0
  128. package/src/ui/types/captcha-provider.ts +7 -0
  129. package/src/ui/types/credentials-options.ts +38 -0
  130. package/src/ui/types/delete-user-options.ts +7 -0
  131. package/src/ui/types/email-verification-options.ts +7 -0
  132. package/src/ui/types/fetch-error.ts +6 -0
  133. package/src/ui/types/generic-oauth-options.ts +16 -0
  134. package/src/ui/types/gravatar-options.ts +21 -0
  135. package/src/ui/types/image.ts +7 -0
  136. package/src/ui/types/invitation.ts +10 -0
  137. package/src/ui/types/link.ts +7 -0
  138. package/src/ui/types/organization-options.ts +106 -0
  139. package/src/ui/types/password-validation.ts +16 -0
  140. package/src/ui/types/profile.ts +15 -0
  141. package/src/ui/types/refetch.ts +1 -0
  142. package/src/ui/types/render-toast.ts +9 -0
  143. package/src/ui/types/sign-up-options.ts +7 -0
  144. package/src/ui/types/social-options.ts +16 -0
  145. package/src/ui/types/team-options.ts +47 -0
@@ -0,0 +1,312 @@
1
+ "use client"
2
+
3
+ import { zodResolver } from "@hookform/resolvers/zod"
4
+ import type { BetterFetchOption } from "better-auth/react"
5
+ import { Loader2 } from "lucide-react"
6
+ import { useContext, useEffect, useState } from "react"
7
+ import { useForm } from "react-hook-form"
8
+ import * as z from "zod"
9
+ import { useCaptcha } from "../../../hooks/use-captcha"
10
+ import { useIsHydrated } from "../../../hooks/use-hydrated"
11
+ import { useOnSuccessTransition } from "../../../hooks/use-success-transition"
12
+ import { AuthUIContext } from "../../../lib/auth-ui-provider"
13
+ import { cn, getLocalizedError } from "../../../lib/utils"
14
+ import type { AuthLocalization } from "../../../localization/auth-localization"
15
+ import { Captcha } from "../../captcha/captcha"
16
+ import { Button } from "../../ui/button"
17
+ import {
18
+ Form,
19
+ FormControl,
20
+ FormField,
21
+ FormItem,
22
+ FormLabel,
23
+ FormMessage
24
+ } from "../../ui/form"
25
+ import { Input } from "../../ui/input"
26
+ import { InputOTP } from "../../ui/input-otp"
27
+ import type { AuthFormClassNames } from "../auth-form"
28
+ import { OTPInputGroup } from "../otp-input-group"
29
+
30
+ export interface EmailOTPFormProps {
31
+ className?: string
32
+ classNames?: AuthFormClassNames
33
+ callbackURL?: string
34
+ isSubmitting?: boolean
35
+ localization: Partial<AuthLocalization>
36
+ otpSeparators?: 0 | 1 | 2
37
+ redirectTo?: string
38
+ setIsSubmitting?: (value: boolean) => void
39
+ }
40
+
41
+ export function EmailOTPForm(props: EmailOTPFormProps) {
42
+ const [email, setEmail] = useState<string | undefined>()
43
+
44
+ if (!email) {
45
+ return <EmailForm {...props} setEmail={setEmail} />
46
+ }
47
+
48
+ return <OTPForm {...props} email={email} />
49
+ }
50
+
51
+ function EmailForm({
52
+ className,
53
+ classNames,
54
+ isSubmitting,
55
+ localization,
56
+ setIsSubmitting,
57
+ setEmail
58
+ }: EmailOTPFormProps & {
59
+ setEmail: (email: string) => void
60
+ }) {
61
+ const isHydrated = useIsHydrated()
62
+ const { captchaRef, getCaptchaHeaders } = useCaptcha({ localization })
63
+
64
+ const {
65
+ authClient,
66
+ localization: contextLocalization,
67
+ toast,
68
+ localizeErrors
69
+ } = useContext(AuthUIContext)
70
+
71
+ localization = { ...contextLocalization, ...localization }
72
+
73
+ const formSchema = z.object({
74
+ email: z.string().email({
75
+ message: `${localization.EMAIL} ${localization.IS_INVALID}`
76
+ })
77
+ })
78
+
79
+ const form = useForm({
80
+ resolver: zodResolver(formSchema),
81
+ defaultValues: {
82
+ email: ""
83
+ }
84
+ })
85
+
86
+ isSubmitting = isSubmitting || form.formState.isSubmitting
87
+
88
+ useEffect(() => {
89
+ setIsSubmitting?.(form.formState.isSubmitting)
90
+ }, [form.formState.isSubmitting, setIsSubmitting])
91
+
92
+ async function sendEmailOTP({ email }: z.infer<typeof formSchema>) {
93
+ const fetchOptions: BetterFetchOption = {
94
+ throw: true,
95
+ headers: await getCaptchaHeaders("/email-otp/send-verification-otp")
96
+ }
97
+
98
+ try {
99
+ await authClient.emailOtp.sendVerificationOtp({
100
+ email,
101
+ type: "sign-in",
102
+ fetchOptions
103
+ })
104
+
105
+ toast({
106
+ variant: "success",
107
+ message: localization.EMAIL_OTP_VERIFICATION_SENT
108
+ })
109
+
110
+ setEmail(email)
111
+ } catch (error) {
112
+ toast({
113
+ variant: "error",
114
+ message: getLocalizedError({
115
+ error,
116
+ localization,
117
+ localizeErrors
118
+ })
119
+ })
120
+ }
121
+ }
122
+
123
+ return (
124
+ <Form {...form}>
125
+ <form
126
+ onSubmit={form.handleSubmit(sendEmailOTP)}
127
+ noValidate={isHydrated}
128
+ className={cn("grid w-full gap-6", className, classNames?.base)}
129
+ >
130
+ <FormField
131
+ control={form.control}
132
+ name="email"
133
+ render={({ field }) => (
134
+ <FormItem>
135
+ <FormLabel className={classNames?.label}>
136
+ {localization.EMAIL}
137
+ </FormLabel>
138
+
139
+ <FormControl>
140
+ <Input
141
+ className={classNames?.input}
142
+ type="email"
143
+ placeholder={localization.EMAIL_PLACEHOLDER}
144
+ disabled={isSubmitting}
145
+ {...field}
146
+ />
147
+ </FormControl>
148
+
149
+ <FormMessage className={classNames?.error} />
150
+ </FormItem>
151
+ )}
152
+ />
153
+
154
+ <Captcha
155
+ ref={captchaRef}
156
+ localization={localization}
157
+ action="/email-otp/send-verification-otp"
158
+ />
159
+
160
+ <Button
161
+ type="submit"
162
+ disabled={isSubmitting}
163
+ className={cn(
164
+ "w-full",
165
+ classNames?.button,
166
+ classNames?.primaryButton
167
+ )}
168
+ >
169
+ {isSubmitting ? (
170
+ <Loader2 className="animate-spin" />
171
+ ) : (
172
+ localization.EMAIL_OTP_SEND_ACTION
173
+ )}
174
+ </Button>
175
+ </form>
176
+ </Form>
177
+ )
178
+ }
179
+
180
+ export function OTPForm({
181
+ className,
182
+ classNames,
183
+ isSubmitting,
184
+ localization,
185
+ otpSeparators = 0,
186
+ redirectTo,
187
+ setIsSubmitting,
188
+ email
189
+ }: EmailOTPFormProps & {
190
+ email: string
191
+ }) {
192
+ const {
193
+ authClient,
194
+ localization: contextLocalization,
195
+ toast,
196
+ localizeErrors
197
+ } = useContext(AuthUIContext)
198
+
199
+ localization = { ...contextLocalization, ...localization }
200
+
201
+ const { onSuccess, isPending: transitionPending } = useOnSuccessTransition({
202
+ redirectTo
203
+ })
204
+
205
+ const formSchema = z.object({
206
+ code: z
207
+ .string()
208
+ .min(1, {
209
+ message: `${localization.EMAIL_OTP} ${localization.IS_REQUIRED}`
210
+ })
211
+ .min(6, {
212
+ message: `${localization.EMAIL_OTP} ${localization.IS_INVALID}`
213
+ })
214
+ })
215
+
216
+ const form = useForm({
217
+ resolver: zodResolver(formSchema),
218
+ defaultValues: {
219
+ code: ""
220
+ }
221
+ })
222
+
223
+ isSubmitting =
224
+ isSubmitting || form.formState.isSubmitting || transitionPending
225
+
226
+ useEffect(() => {
227
+ setIsSubmitting?.(form.formState.isSubmitting || transitionPending)
228
+ }, [form.formState.isSubmitting, transitionPending, setIsSubmitting])
229
+
230
+ async function verifyCode({ code }: z.infer<typeof formSchema>) {
231
+ try {
232
+ await authClient.signIn.emailOtp({
233
+ email,
234
+ otp: code,
235
+ fetchOptions: { throw: true }
236
+ })
237
+
238
+ await onSuccess()
239
+ } catch (error) {
240
+ toast({
241
+ variant: "error",
242
+ message: getLocalizedError({
243
+ error,
244
+ localization,
245
+ localizeErrors
246
+ })
247
+ })
248
+
249
+ form.reset()
250
+ }
251
+ }
252
+
253
+ return (
254
+ <Form {...form}>
255
+ <form
256
+ onSubmit={form.handleSubmit(verifyCode)}
257
+ className={cn("grid w-full gap-6", className, classNames?.base)}
258
+ >
259
+ <FormField
260
+ control={form.control}
261
+ name="code"
262
+ render={({ field }) => (
263
+ <FormItem>
264
+ <FormLabel className={classNames?.label}>
265
+ {localization.EMAIL_OTP}
266
+ </FormLabel>
267
+
268
+ <FormControl>
269
+ <InputOTP
270
+ {...field}
271
+ maxLength={6}
272
+ onChange={(value) => {
273
+ field.onChange(value)
274
+
275
+ if (value.length === 6) {
276
+ form.handleSubmit(verifyCode)()
277
+ }
278
+ }}
279
+ containerClassName={
280
+ classNames?.otpInputContainer
281
+ }
282
+ className={classNames?.otpInput}
283
+ disabled={isSubmitting}
284
+ >
285
+ <OTPInputGroup
286
+ otpSeparators={otpSeparators}
287
+ />
288
+ </InputOTP>
289
+ </FormControl>
290
+
291
+ <FormMessage className={classNames?.error} />
292
+ </FormItem>
293
+ )}
294
+ />
295
+
296
+ <div className="grid gap-4">
297
+ <Button
298
+ type="submit"
299
+ disabled={isSubmitting}
300
+ className={cn(
301
+ classNames?.button,
302
+ classNames?.primaryButton
303
+ )}
304
+ >
305
+ {isSubmitting && <Loader2 className="animate-spin" />}
306
+ {localization.EMAIL_OTP_VERIFY_ACTION}
307
+ </Button>
308
+ </div>
309
+ </form>
310
+ </Form>
311
+ )
312
+ }
@@ -0,0 +1,271 @@
1
+ "use client"
2
+
3
+ import { zodResolver } from "@hookform/resolvers/zod"
4
+ import { Loader2 } from "lucide-react"
5
+ import { useContext, useEffect, useState } from "react"
6
+ import { useForm } from "react-hook-form"
7
+ import * as z from "zod"
8
+ import { useOnSuccessTransition } from "../../../hooks/use-success-transition"
9
+ import { AuthUIContext } from "../../../lib/auth-ui-provider"
10
+ import { cn, getLocalizedError } from "../../../lib/utils"
11
+ import type { AuthLocalization } from "../../../localization/auth-localization"
12
+ import { Button } from "../../ui/button"
13
+ import {
14
+ Form,
15
+ FormControl,
16
+ FormField,
17
+ FormItem,
18
+ FormLabel,
19
+ FormMessage
20
+ } from "../../ui/form"
21
+ import { InputOTP } from "../../ui/input-otp"
22
+ import type { AuthFormClassNames } from "../auth-form"
23
+ import { OTPInputGroup } from "../otp-input-group"
24
+
25
+ export interface EmailVerificationFormProps {
26
+ className?: string
27
+ classNames?: AuthFormClassNames
28
+ callbackURL?: string
29
+ isSubmitting?: boolean
30
+ localization: Partial<AuthLocalization>
31
+ otpSeparators?: 0 | 1 | 2
32
+ redirectTo?: string
33
+ setIsSubmitting?: (value: boolean) => void
34
+ onCancel?: () => void
35
+ }
36
+
37
+ export function EmailVerificationForm({
38
+ onCancel,
39
+ localization,
40
+ className,
41
+ classNames,
42
+ otpSeparators,
43
+ callbackURL,
44
+ isSubmitting,
45
+ redirectTo,
46
+ setIsSubmitting
47
+ }: EmailVerificationFormProps) {
48
+ const [resendDisabled, setResendDisabled] = useState(true)
49
+ const [countdown, setCountdown] = useState(30)
50
+
51
+ const {
52
+ authClient,
53
+ localization: contextLocalization,
54
+ toast,
55
+ localizeErrors,
56
+ navigate,
57
+ basePath,
58
+ viewPaths
59
+ } = useContext(AuthUIContext)
60
+
61
+ localization = { ...contextLocalization, ...localization }
62
+
63
+ const email =
64
+ typeof window !== "undefined"
65
+ ? new URLSearchParams(window.location.search).get("email") || ""
66
+ : ""
67
+
68
+ const { onSuccess, isPending: transitionPending } = useOnSuccessTransition({
69
+ redirectTo
70
+ })
71
+
72
+ const formSchema = z.object({
73
+ code: z
74
+ .string()
75
+ .min(1, {
76
+ message: `${localization.EMAIL_OTP} ${localization.IS_REQUIRED}`
77
+ })
78
+ .min(6, {
79
+ message: `${localization.EMAIL_OTP} ${localization.IS_INVALID}`
80
+ })
81
+ })
82
+
83
+ const form = useForm({
84
+ resolver: zodResolver(formSchema),
85
+ defaultValues: {
86
+ code: ""
87
+ }
88
+ })
89
+
90
+ const currentIsSubmitting =
91
+ isSubmitting || form.formState.isSubmitting || transitionPending
92
+
93
+ useEffect(() => {
94
+ setIsSubmitting?.(form.formState.isSubmitting || transitionPending)
95
+ }, [form.formState.isSubmitting, transitionPending, setIsSubmitting])
96
+
97
+ useEffect(() => {
98
+ if (countdown > 0) {
99
+ const timer = setTimeout(() => setCountdown(countdown - 1), 1000)
100
+ return () => clearTimeout(timer)
101
+ } else {
102
+ setResendDisabled(false)
103
+ }
104
+ }, [countdown])
105
+
106
+ async function verifyCode({ code }: z.infer<typeof formSchema>) {
107
+ try {
108
+ const data = await authClient.emailOtp.verifyEmail({
109
+ email,
110
+ otp: code,
111
+ fetchOptions: { throw: true }
112
+ })
113
+
114
+ if ("token" in data && data.token) {
115
+ await onSuccess()
116
+ } else {
117
+ navigate(
118
+ `${basePath}/${viewPaths.SIGN_IN}${window.location.search}`
119
+ )
120
+ toast({
121
+ variant: "success",
122
+ message: localization.EMAIL_VERIFICATION_SUCCESS!
123
+ })
124
+ }
125
+ } catch (error) {
126
+ toast({
127
+ variant: "error",
128
+ message: getLocalizedError({
129
+ error,
130
+ localization,
131
+ localizeErrors
132
+ })
133
+ })
134
+
135
+ form.reset()
136
+ }
137
+ }
138
+
139
+ async function resendCode() {
140
+ if (resendDisabled) return
141
+
142
+ setResendDisabled(true)
143
+ setCountdown(30)
144
+
145
+ try {
146
+ await authClient.emailOtp.sendVerificationOtp({
147
+ email,
148
+ type: "email-verification",
149
+ fetchOptions: { throw: true }
150
+ })
151
+
152
+ toast({
153
+ variant: "success",
154
+ message: localization.EMAIL_OTP_VERIFICATION_SENT!
155
+ })
156
+ } catch (error) {
157
+ toast({
158
+ variant: "error",
159
+ message: getLocalizedError({
160
+ error,
161
+ localization,
162
+ localizeErrors
163
+ })
164
+ })
165
+ setResendDisabled(false)
166
+ setCountdown(0)
167
+ }
168
+ }
169
+
170
+ if (!email) {
171
+ return (
172
+ <div className={cn("grid w-full gap-6", className)}>
173
+ <div className="text-center">
174
+ <h2 className="font-semibold text-destructive text-lg">
175
+ Invalid Request
176
+ </h2>
177
+ <p className="text-muted-foreground text-sm">
178
+ {localization.EMAIL_REQUIRED ||
179
+ "Email address is required"}
180
+ </p>
181
+ </div>
182
+ </div>
183
+ )
184
+ }
185
+
186
+ return (
187
+ <Form {...form}>
188
+ <form
189
+ onSubmit={form.handleSubmit(verifyCode)}
190
+ className={cn("grid w-full gap-6", className, classNames?.base)}
191
+ >
192
+ <FormField
193
+ control={form.control}
194
+ name="code"
195
+ render={({ field }) => (
196
+ <FormItem>
197
+ <FormLabel className={classNames?.label}>
198
+ {localization.EMAIL_OTP}
199
+ </FormLabel>
200
+
201
+ <FormControl>
202
+ <InputOTP
203
+ {...field}
204
+ maxLength={6}
205
+ onChange={(value) => {
206
+ field.onChange(value)
207
+
208
+ if (value.length === 6) {
209
+ form.handleSubmit(verifyCode)()
210
+ }
211
+ }}
212
+ containerClassName={
213
+ classNames?.otpInputContainer
214
+ }
215
+ className={classNames?.otpInput}
216
+ disabled={currentIsSubmitting}
217
+ >
218
+ <OTPInputGroup
219
+ otpSeparators={otpSeparators}
220
+ />
221
+ </InputOTP>
222
+ </FormControl>
223
+
224
+ <FormMessage className={classNames?.error} />
225
+ </FormItem>
226
+ )}
227
+ />
228
+
229
+ <div className="grid gap-4">
230
+ <Button
231
+ type="submit"
232
+ disabled={currentIsSubmitting}
233
+ className={cn(
234
+ classNames?.button,
235
+ classNames?.primaryButton
236
+ )}
237
+ >
238
+ {currentIsSubmitting && (
239
+ <Loader2 className="animate-spin" />
240
+ )}
241
+ {localization.EMAIL_OTP_VERIFY_ACTION}
242
+ </Button>
243
+
244
+ <Button
245
+ type="button"
246
+ variant="outline"
247
+ onClick={resendCode}
248
+ disabled={resendDisabled || currentIsSubmitting}
249
+ className={cn("w-full", classNames?.button)}
250
+ >
251
+ {resendDisabled
252
+ ? `${localization.RESEND_VERIFICATION_EMAIL} (${countdown}s)`
253
+ : localization.RESEND_VERIFICATION_EMAIL}
254
+ </Button>
255
+
256
+ {onCancel && (
257
+ <Button
258
+ type="button"
259
+ variant="ghost"
260
+ onClick={onCancel}
261
+ disabled={currentIsSubmitting}
262
+ className="w-full"
263
+ >
264
+ {localization.CANCEL}
265
+ </Button>
266
+ )}
267
+ </div>
268
+ </form>
269
+ </Form>
270
+ )
271
+ }