better-auth-ui 3.2.5

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 (226) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/dist/auth-hooks-IOEvlYYv.d.cts +6966 -0
  4. package/dist/auth-hooks-IOEvlYYv.d.ts +6966 -0
  5. package/dist/auth-mutators-DdqOmQ32.d.cts +29 -0
  6. package/dist/auth-mutators-DdqOmQ32.d.ts +29 -0
  7. package/dist/auth-ui-provider-BsH3xJDw.d.ts +697 -0
  8. package/dist/auth-ui-provider-DhZfncd3.d.cts +697 -0
  9. package/dist/chunk-BDFQSFBU.js +750 -0
  10. package/dist/chunk-CRAHKL2C.cjs +801 -0
  11. package/dist/chunk-MJPOA6PK.js +801 -0
  12. package/dist/chunk-SV64DXGW.cjs +750 -0
  13. package/dist/index.cjs +12618 -0
  14. package/dist/index.d.cts +771 -0
  15. package/dist/index.d.ts +771 -0
  16. package/dist/index.js +12618 -0
  17. package/dist/instantdb.cjs +189 -0
  18. package/dist/instantdb.d.cts +36 -0
  19. package/dist/instantdb.d.ts +36 -0
  20. package/dist/instantdb.js +189 -0
  21. package/dist/metafile-cjs.json +1 -0
  22. package/dist/metafile-esm.json +1 -0
  23. package/dist/server.cjs +194 -0
  24. package/dist/server.d.cts +35 -0
  25. package/dist/server.d.ts +35 -0
  26. package/dist/server.js +194 -0
  27. package/dist/style.css +1 -0
  28. package/dist/tanstack.cjs +153 -0
  29. package/dist/tanstack.d.cts +18 -0
  30. package/dist/tanstack.d.ts +18 -0
  31. package/dist/tanstack.js +153 -0
  32. package/dist/triplit.cjs +201 -0
  33. package/dist/triplit.d.cts +31 -0
  34. package/dist/triplit.d.ts +31 -0
  35. package/dist/triplit.js +201 -0
  36. package/dist/utils-C5R37WDe.d.cts +3 -0
  37. package/dist/utils-C5R37WDe.d.ts +3 -0
  38. package/dist/view-paths-CHSJf5dv.d.cts +645 -0
  39. package/dist/view-paths-CHSJf5dv.d.ts +645 -0
  40. package/package.json +156 -0
  41. package/src/components/account/account-view.tsx +220 -0
  42. package/src/components/auth/auth-callback.tsx +36 -0
  43. package/src/components/auth/auth-form.tsx +277 -0
  44. package/src/components/auth/auth-view.tsx +389 -0
  45. package/src/components/auth/email-otp-button.tsx +53 -0
  46. package/src/components/auth/forms/email-otp-form.tsx +288 -0
  47. package/src/components/auth/forms/forgot-password-form.tsx +168 -0
  48. package/src/components/auth/forms/magic-link-form.tsx +191 -0
  49. package/src/components/auth/forms/recover-account-form.tsx +138 -0
  50. package/src/components/auth/forms/reset-password-form.tsx +215 -0
  51. package/src/components/auth/forms/sign-in-form.tsx +289 -0
  52. package/src/components/auth/forms/sign-up-form.tsx +788 -0
  53. package/src/components/auth/forms/two-factor-form.tsx +372 -0
  54. package/src/components/auth/magic-link-button.tsx +54 -0
  55. package/src/components/auth/one-tap.tsx +48 -0
  56. package/src/components/auth/otp-input-group.tsx +65 -0
  57. package/src/components/auth/passkey-button.tsx +85 -0
  58. package/src/components/auth/provider-button.tsx +141 -0
  59. package/src/components/auth/sign-out.tsx +25 -0
  60. package/src/components/auth-loading.tsx +21 -0
  61. package/src/components/captcha/captcha.tsx +79 -0
  62. package/src/components/captcha/recaptcha-badge.tsx +61 -0
  63. package/src/components/captcha/recaptcha-v2.tsx +58 -0
  64. package/src/components/captcha/recaptcha-v3.tsx +73 -0
  65. package/src/components/email/email-template.tsx +216 -0
  66. package/src/components/form-error.tsx +27 -0
  67. package/src/components/organization/accept-invitation-card.tsx +362 -0
  68. package/src/components/organization/create-organization-dialog.tsx +395 -0
  69. package/src/components/organization/delete-organization-card.tsx +101 -0
  70. package/src/components/organization/delete-organization-dialog.tsx +209 -0
  71. package/src/components/organization/invitation-cell.tsx +156 -0
  72. package/src/components/organization/invite-member-dialog.tsx +258 -0
  73. package/src/components/organization/leave-organization-dialog.tsx +150 -0
  74. package/src/components/organization/member-cell.tsx +187 -0
  75. package/src/components/organization/organization-cell-view.tsx +122 -0
  76. package/src/components/organization/organization-cell.tsx +154 -0
  77. package/src/components/organization/organization-invitations-card.tsx +94 -0
  78. package/src/components/organization/organization-logo-card.tsx +308 -0
  79. package/src/components/organization/organization-logo.tsx +120 -0
  80. package/src/components/organization/organization-members-card.tsx +155 -0
  81. package/src/components/organization/organization-name-card.tsx +204 -0
  82. package/src/components/organization/organization-settings-cards.tsx +67 -0
  83. package/src/components/organization/organization-slug-card.tsx +223 -0
  84. package/src/components/organization/organization-switcher.tsx +512 -0
  85. package/src/components/organization/organization-view.tsx +228 -0
  86. package/src/components/organization/organizations-card.tsx +72 -0
  87. package/src/components/organization/personal-account-view.tsx +115 -0
  88. package/src/components/organization/remove-member-dialog.tsx +144 -0
  89. package/src/components/organization/update-member-role-dialog.tsx +213 -0
  90. package/src/components/organization/user-invitations-card.tsx +238 -0
  91. package/src/components/password-input.tsx +56 -0
  92. package/src/components/provider-icons.tsx +385 -0
  93. package/src/components/redirect-to-sign-in.tsx +16 -0
  94. package/src/components/redirect-to-sign-up.tsx +16 -0
  95. package/src/components/settings/account/account-cell.tsx +158 -0
  96. package/src/components/settings/account/accounts-card.tsx +75 -0
  97. package/src/components/settings/account/delete-account-card.tsx +65 -0
  98. package/src/components/settings/account/delete-account-dialog.tsx +231 -0
  99. package/src/components/settings/account/update-avatar-card.tsx +198 -0
  100. package/src/components/settings/account/update-field-card.tsx +282 -0
  101. package/src/components/settings/account/update-name-card.tsx +39 -0
  102. package/src/components/settings/account/update-username-card.tsx +42 -0
  103. package/src/components/settings/account-settings-cards.tsx +123 -0
  104. package/src/components/settings/api-key/api-key-cell.tsx +108 -0
  105. package/src/components/settings/api-key/api-key-delete-dialog.tsx +162 -0
  106. package/src/components/settings/api-key/api-key-display-dialog.tsx +110 -0
  107. package/src/components/settings/api-key/api-keys-card.tsx +98 -0
  108. package/src/components/settings/api-key/create-api-key-dialog.tsx +376 -0
  109. package/src/components/settings/passkey/passkey-cell.tsx +113 -0
  110. package/src/components/settings/passkey/passkeys-card.tsx +111 -0
  111. package/src/components/settings/providers/provider-cell.tsx +152 -0
  112. package/src/components/settings/providers/providers-card.tsx +108 -0
  113. package/src/components/settings/security/change-email-card.tsx +200 -0
  114. package/src/components/settings/security/change-password-card.tsx +326 -0
  115. package/src/components/settings/security/session-cell.tsx +120 -0
  116. package/src/components/settings/security/sessions-card.tsx +58 -0
  117. package/src/components/settings/security-settings-cards.tsx +111 -0
  118. package/src/components/settings/shared/session-freshness-dialog.tsx +97 -0
  119. package/src/components/settings/shared/settings-action-button.tsx +51 -0
  120. package/src/components/settings/shared/settings-card-footer.tsx +94 -0
  121. package/src/components/settings/shared/settings-card-header.tsx +67 -0
  122. package/src/components/settings/shared/settings-card.tsx +106 -0
  123. package/src/components/settings/skeletons/input-field-skeleton.tsx +18 -0
  124. package/src/components/settings/skeletons/settings-cell-skeleton.tsx +37 -0
  125. package/src/components/settings/two-factor/backup-codes-dialog.tsx +113 -0
  126. package/src/components/settings/two-factor/two-factor-card.tsx +63 -0
  127. package/src/components/settings/two-factor/two-factor-password-dialog.tsx +226 -0
  128. package/src/components/signed-in.tsx +20 -0
  129. package/src/components/signed-out.tsx +20 -0
  130. package/src/components/ui/alert.tsx +66 -0
  131. package/src/components/ui/avatar.tsx +53 -0
  132. package/src/components/ui/button.tsx +59 -0
  133. package/src/components/ui/card.tsx +92 -0
  134. package/src/components/ui/checkbox.tsx +32 -0
  135. package/src/components/ui/dialog.tsx +143 -0
  136. package/src/components/ui/drawer.tsx +135 -0
  137. package/src/components/ui/dropdown-menu.tsx +257 -0
  138. package/src/components/ui/form.tsx +167 -0
  139. package/src/components/ui/input-otp.tsx +77 -0
  140. package/src/components/ui/input.tsx +21 -0
  141. package/src/components/ui/label.tsx +24 -0
  142. package/src/components/ui/select.tsx +185 -0
  143. package/src/components/ui/separator.tsx +28 -0
  144. package/src/components/ui/skeleton.tsx +13 -0
  145. package/src/components/ui/tabs.tsx +66 -0
  146. package/src/components/ui/textarea.tsx +18 -0
  147. package/src/components/user-avatar.tsx +147 -0
  148. package/src/components/user-button.tsx +409 -0
  149. package/src/components/user-view.tsx +138 -0
  150. package/src/hooks/use-auth-data.ts +184 -0
  151. package/src/hooks/use-authenticate.ts +62 -0
  152. package/src/hooks/use-captcha.tsx +138 -0
  153. package/src/hooks/use-current-organization.ts +59 -0
  154. package/src/hooks/use-hydrated.ts +13 -0
  155. package/src/hooks/use-lang.ts +32 -0
  156. package/src/hooks/use-success-transition.ts +51 -0
  157. package/src/hooks/use-theme.ts +39 -0
  158. package/src/index.ts +65 -0
  159. package/src/instantdb.ts +1 -0
  160. package/src/lib/auth-data-cache.ts +90 -0
  161. package/src/lib/auth-ui-provider.tsx +658 -0
  162. package/src/lib/gravatar-utils.ts +58 -0
  163. package/src/lib/image-utils.ts +55 -0
  164. package/src/lib/instantdb/model-names.ts +24 -0
  165. package/src/lib/instantdb/use-instant-options.ts +98 -0
  166. package/src/lib/instantdb/use-list-accounts.ts +38 -0
  167. package/src/lib/instantdb/use-list-sessions.ts +53 -0
  168. package/src/lib/instantdb/use-session.ts +55 -0
  169. package/src/lib/organization-refetcher.tsx +56 -0
  170. package/src/lib/social-providers.ts +144 -0
  171. package/src/lib/tanstack/auth-ui-provider-tanstack.tsx +49 -0
  172. package/src/lib/tanstack/use-tanstack-options.ts +112 -0
  173. package/src/lib/triplit/model-names.ts +24 -0
  174. package/src/lib/triplit/use-conditional-query.ts +82 -0
  175. package/src/lib/triplit/use-list-accounts.ts +31 -0
  176. package/src/lib/triplit/use-list-sessions.ts +33 -0
  177. package/src/lib/triplit/use-session.ts +42 -0
  178. package/src/lib/triplit/use-triplit-hooks.ts +68 -0
  179. package/src/lib/triplit/use-triplit-token.ts +44 -0
  180. package/src/lib/utils.ts +105 -0
  181. package/src/lib/view-paths.ts +55 -0
  182. package/src/localization/admin-error-codes.ts +20 -0
  183. package/src/localization/anonymous-error-codes.ts +6 -0
  184. package/src/localization/api-key-error-codes.ts +32 -0
  185. package/src/localization/auth-localization.ts +740 -0
  186. package/src/localization/base-error-codes.ts +27 -0
  187. package/src/localization/captcha-error-codes.ts +17 -0
  188. package/src/localization/email-otp-error-codes.ts +7 -0
  189. package/src/localization/generic-oauth-error-codes.ts +3 -0
  190. package/src/localization/haveibeenpwned-error-codes.ts +4 -0
  191. package/src/localization/multi-session-error-codes.ts +3 -0
  192. package/src/localization/organization-error-codes.ts +57 -0
  193. package/src/localization/passkey-error-codes.ts +10 -0
  194. package/src/localization/phone-number-error-codes.ts +10 -0
  195. package/src/localization/stripe-localization.ts +12 -0
  196. package/src/localization/two-factor-error-codes.ts +12 -0
  197. package/src/localization/username-error-codes.ts +9 -0
  198. package/src/server.ts +4 -0
  199. package/src/style.css +1 -0
  200. package/src/tanstack.ts +1 -0
  201. package/src/triplit.ts +1 -0
  202. package/src/types/account-options.ts +35 -0
  203. package/src/types/additional-fields.ts +21 -0
  204. package/src/types/any-auth-client.ts +6 -0
  205. package/src/types/api-key.ts +9 -0
  206. package/src/types/auth-client.ts +37 -0
  207. package/src/types/auth-hooks.ts +61 -0
  208. package/src/types/auth-mutators.ts +17 -0
  209. package/src/types/avatar-options.ts +29 -0
  210. package/src/types/captcha-options.ts +32 -0
  211. package/src/types/captcha-provider.ts +6 -0
  212. package/src/types/credentials-options.ts +32 -0
  213. package/src/types/delete-user-options.ts +7 -0
  214. package/src/types/fetch-error.ts +6 -0
  215. package/src/types/generic-oauth-options.ts +16 -0
  216. package/src/types/gravatar-options.ts +21 -0
  217. package/src/types/image.ts +7 -0
  218. package/src/types/invitation.ts +10 -0
  219. package/src/types/link.ts +7 -0
  220. package/src/types/organization-options.ts +106 -0
  221. package/src/types/password-validation.ts +16 -0
  222. package/src/types/profile.ts +15 -0
  223. package/src/types/refetch.ts +1 -0
  224. package/src/types/render-toast.ts +9 -0
  225. package/src/types/sign-up-options.ts +7 -0
  226. package/src/types/social-options.ts +16 -0
@@ -0,0 +1,788 @@
1
+ "use client"
2
+
3
+ import { zodResolver } from "@hookform/resolvers/zod"
4
+ import type { BetterFetchOption } from "better-auth/react"
5
+ import { Loader2, Trash2Icon, UploadCloudIcon } from "lucide-react"
6
+ import { useCallback, useContext, useEffect, useRef, useState } from "react"
7
+ import { useForm } from "react-hook-form"
8
+ import * as z from "zod"
9
+
10
+ import { useCaptcha } from "../../../hooks/use-captcha"
11
+ import { useIsHydrated } from "../../../hooks/use-hydrated"
12
+ import { useOnSuccessTransition } from "../../../hooks/use-success-transition"
13
+ import { AuthUIContext } from "../../../lib/auth-ui-provider"
14
+ import { fileToBase64, resizeAndCropImage } from "../../../lib/image-utils"
15
+ import {
16
+ cn,
17
+ getLocalizedError,
18
+ getPasswordSchema,
19
+ getSearchParam
20
+ } from "../../../lib/utils"
21
+ import type { AuthLocalization } from "../../../localization/auth-localization"
22
+ import type { PasswordValidation } from "../../../types/password-validation"
23
+ import { Captcha } from "../../captcha/captcha"
24
+ import { PasswordInput } from "../../password-input"
25
+ import { Button } from "../../ui/button"
26
+ import { Checkbox } from "../../ui/checkbox"
27
+ import {
28
+ DropdownMenu,
29
+ DropdownMenuContent,
30
+ DropdownMenuItem,
31
+ DropdownMenuTrigger
32
+ } from "../../ui/dropdown-menu"
33
+ import {
34
+ Form,
35
+ FormControl,
36
+ FormField,
37
+ FormItem,
38
+ FormLabel,
39
+ FormMessage
40
+ } from "../../ui/form"
41
+ import { Input } from "../../ui/input"
42
+ import { Textarea } from "../../ui/textarea"
43
+ import { UserAvatar } from "../../user-avatar"
44
+ import type { AuthFormClassNames } from "../auth-form"
45
+
46
+ export interface SignUpFormProps {
47
+ className?: string
48
+ classNames?: AuthFormClassNames
49
+ callbackURL?: string
50
+ isSubmitting?: boolean
51
+ localization: Partial<AuthLocalization>
52
+ redirectTo?: string
53
+ setIsSubmitting?: (value: boolean) => void
54
+ passwordValidation?: PasswordValidation
55
+ }
56
+
57
+ export function SignUpForm({
58
+ className,
59
+ classNames,
60
+ callbackURL,
61
+ isSubmitting,
62
+ localization,
63
+ redirectTo,
64
+ setIsSubmitting,
65
+ passwordValidation
66
+ }: SignUpFormProps) {
67
+ const isHydrated = useIsHydrated()
68
+ const { captchaRef, getCaptchaHeaders, resetCaptcha } = useCaptcha({
69
+ localization
70
+ })
71
+
72
+ const {
73
+ additionalFields,
74
+ authClient,
75
+ basePath,
76
+ baseURL,
77
+ credentials,
78
+ localization: contextLocalization,
79
+ nameRequired,
80
+ persistClient,
81
+ redirectTo: contextRedirectTo,
82
+ signUp: signUpOptions,
83
+ viewPaths,
84
+ navigate,
85
+ toast,
86
+ avatar
87
+ } = useContext(AuthUIContext)
88
+
89
+ const confirmPasswordEnabled = credentials?.confirmPassword
90
+ const usernameEnabled = credentials?.username
91
+ const contextPasswordValidation = credentials?.passwordValidation
92
+ const signUpFields = signUpOptions?.fields
93
+
94
+ localization = { ...contextLocalization, ...localization }
95
+ passwordValidation = { ...contextPasswordValidation, ...passwordValidation }
96
+
97
+ // Avatar upload state
98
+ const fileInputRef = useRef<HTMLInputElement>(null)
99
+ const [avatarImage, setAvatarImage] = useState<string | null>(null)
100
+ const [uploadingAvatar, setUploadingAvatar] = useState(false)
101
+
102
+ const getRedirectTo = useCallback(
103
+ () => redirectTo || getSearchParam("redirectTo") || contextRedirectTo,
104
+ [redirectTo, contextRedirectTo]
105
+ )
106
+
107
+ const getCallbackURL = useCallback(
108
+ () =>
109
+ `${baseURL}${
110
+ callbackURL ||
111
+ (persistClient
112
+ ? `${basePath}/${viewPaths.CALLBACK}?redirectTo=${getRedirectTo()}`
113
+ : getRedirectTo())
114
+ }`,
115
+ [
116
+ callbackURL,
117
+ persistClient,
118
+ basePath,
119
+ viewPaths,
120
+ baseURL,
121
+ getRedirectTo
122
+ ]
123
+ )
124
+
125
+ const { onSuccess, isPending: transitionPending } = useOnSuccessTransition({
126
+ redirectTo
127
+ })
128
+
129
+ // Create the base schema for standard fields
130
+ const defaultFields = {
131
+ email: z.string().email({
132
+ message: `${localization.EMAIL} ${localization.IS_INVALID}`
133
+ }),
134
+ password: getPasswordSchema(passwordValidation, localization),
135
+ name:
136
+ signUpFields?.includes("name") && nameRequired
137
+ ? z.string().min(1, {
138
+ message: `${localization.NAME} ${localization.IS_REQUIRED}`
139
+ })
140
+ : z.string().optional(),
141
+ image: z.string().optional(),
142
+ username: usernameEnabled
143
+ ? z.string().min(1, {
144
+ message: `${localization.USERNAME} ${localization.IS_REQUIRED}`
145
+ })
146
+ : z.string().optional(),
147
+ confirmPassword: confirmPasswordEnabled
148
+ ? getPasswordSchema(passwordValidation, {
149
+ PASSWORD_REQUIRED: localization.CONFIRM_PASSWORD_REQUIRED,
150
+ PASSWORD_TOO_SHORT: localization.PASSWORD_TOO_SHORT,
151
+ PASSWORD_TOO_LONG: localization.PASSWORD_TOO_LONG,
152
+ INVALID_PASSWORD: localization.INVALID_PASSWORD
153
+ })
154
+ : z.string().optional()
155
+ }
156
+
157
+ const schemaFields: Record<string, z.ZodTypeAny> = {}
158
+
159
+ // Add additional fields from signUpFields
160
+ if (signUpFields) {
161
+ for (const field of signUpFields) {
162
+ if (field === "name") continue // Already handled above
163
+ if (field === "image") continue // Already handled above
164
+
165
+ const additionalField = additionalFields?.[field]
166
+ if (!additionalField) continue
167
+
168
+ let fieldSchema: z.ZodTypeAny
169
+
170
+ // Create the appropriate schema based on field type
171
+ if (additionalField.type === "number") {
172
+ fieldSchema = additionalField.required
173
+ ? z.preprocess(
174
+ (val) => (!val ? undefined : Number(val)),
175
+ z.number({
176
+ message: `${additionalField.label} ${localization.IS_INVALID}`
177
+ })
178
+ )
179
+ : z.coerce
180
+ .number({
181
+ message: `${additionalField.label} ${localization.IS_INVALID}`
182
+ })
183
+ .optional()
184
+ } else if (additionalField.type === "boolean") {
185
+ fieldSchema = additionalField.required
186
+ ? z.coerce
187
+ .boolean({
188
+ message: `${additionalField.label} ${localization.IS_INVALID}`
189
+ })
190
+ .refine((val) => val === true, {
191
+ message: `${additionalField.label} ${localization.IS_REQUIRED}`
192
+ })
193
+ : z.coerce
194
+ .boolean({
195
+ message: `${additionalField.label} ${localization.IS_INVALID}`
196
+ })
197
+ .optional()
198
+ } else {
199
+ fieldSchema = additionalField.required
200
+ ? z
201
+ .string()
202
+ .min(
203
+ 1,
204
+ `${additionalField.label} ${localization.IS_REQUIRED}`
205
+ )
206
+ : z.string().optional()
207
+ }
208
+
209
+ schemaFields[field] = fieldSchema
210
+ }
211
+ }
212
+
213
+ const formSchema = z
214
+ .object(defaultFields)
215
+ .extend(schemaFields)
216
+ .refine(
217
+ (data) => {
218
+ // Skip validation if confirmPassword is not enabled
219
+ if (!confirmPasswordEnabled) return true
220
+ return data.password === data.confirmPassword
221
+ },
222
+ {
223
+ message: localization.PASSWORDS_DO_NOT_MATCH!,
224
+ path: ["confirmPassword"]
225
+ }
226
+ )
227
+
228
+ // Create default values for the form
229
+ const defaultValues: Record<string, unknown> = {
230
+ email: "",
231
+ password: "",
232
+ ...(confirmPasswordEnabled && { confirmPassword: "" }),
233
+ ...(signUpFields?.includes("name") ? { name: "" } : {}),
234
+ ...(usernameEnabled ? { username: "" } : {}),
235
+ ...(signUpFields?.includes("image") && avatar ? { image: "" } : {})
236
+ }
237
+
238
+ // Add default values for additional fields
239
+ if (signUpFields) {
240
+ for (const field of signUpFields) {
241
+ if (field === "name") continue
242
+ if (field === "image") continue
243
+ const additionalField = additionalFields?.[field]
244
+ if (!additionalField) continue
245
+
246
+ defaultValues[field] =
247
+ additionalField.type === "boolean" ? false : ""
248
+ }
249
+ }
250
+
251
+ const form = useForm<z.infer<typeof formSchema>>({
252
+ resolver: zodResolver(formSchema),
253
+ defaultValues
254
+ })
255
+
256
+ isSubmitting =
257
+ isSubmitting || form.formState.isSubmitting || transitionPending
258
+
259
+ useEffect(() => {
260
+ setIsSubmitting?.(form.formState.isSubmitting || transitionPending)
261
+ }, [form.formState.isSubmitting, transitionPending, setIsSubmitting])
262
+
263
+ const handleAvatarChange = async (file: File) => {
264
+ if (!avatar) return
265
+
266
+ setUploadingAvatar(true)
267
+
268
+ try {
269
+ const resizedFile = await resizeAndCropImage(
270
+ file,
271
+ crypto.randomUUID(),
272
+ avatar.size,
273
+ avatar.extension
274
+ )
275
+
276
+ let image: string | undefined | null
277
+
278
+ if (avatar.upload) {
279
+ image = await avatar.upload(resizedFile)
280
+ } else {
281
+ image = await fileToBase64(resizedFile)
282
+ }
283
+
284
+ if (image) {
285
+ setAvatarImage(image)
286
+ form.setValue("image", image)
287
+ } else {
288
+ setAvatarImage(null)
289
+ form.setValue("image", "")
290
+ }
291
+ } catch (error) {
292
+ console.error(error)
293
+ toast({
294
+ variant: "error",
295
+ message: getLocalizedError({ error, localization })
296
+ })
297
+ }
298
+
299
+ setUploadingAvatar(false)
300
+ }
301
+
302
+ const handleDeleteAvatar = () => {
303
+ setAvatarImage(null)
304
+ form.setValue("image", "")
305
+ }
306
+
307
+ const openFileDialog = () => fileInputRef.current?.click()
308
+
309
+ async function signUp({
310
+ email,
311
+ password,
312
+ name,
313
+ username,
314
+ confirmPassword,
315
+ image,
316
+ ...additionalFieldValues
317
+ }: z.infer<typeof formSchema>) {
318
+ try {
319
+ // Validate additional fields with custom validators if provided
320
+ for (const [field, value] of Object.entries(
321
+ additionalFieldValues
322
+ )) {
323
+ const additionalField = additionalFields?.[field]
324
+ if (!additionalField?.validate) continue
325
+
326
+ if (
327
+ typeof value === "string" &&
328
+ !(await additionalField.validate(value))
329
+ ) {
330
+ form.setError(field, {
331
+ message: `${additionalField.label} ${localization.IS_INVALID}`
332
+ })
333
+ return
334
+ }
335
+ }
336
+
337
+ const fetchOptions: BetterFetchOption = {
338
+ throw: true,
339
+ headers: await getCaptchaHeaders("/sign-up/email")
340
+ }
341
+
342
+ const additionalParams: Record<string, unknown> = {}
343
+
344
+ if (username !== undefined) {
345
+ additionalParams.username = username
346
+ }
347
+
348
+ if (image !== undefined) {
349
+ additionalParams.image = image
350
+ }
351
+
352
+ const data = await authClient.signUp.email({
353
+ email: email as string,
354
+ password: password as string,
355
+ name: (name as string) || "",
356
+ ...additionalParams,
357
+ ...additionalFieldValues,
358
+ callbackURL: getCallbackURL(),
359
+ fetchOptions
360
+ })
361
+
362
+ if ("token" in data && data.token) {
363
+ await onSuccess()
364
+ } else {
365
+ navigate(
366
+ `${basePath}/${viewPaths.SIGN_IN}${window.location.search}`
367
+ )
368
+ toast({
369
+ variant: "success",
370
+ message: localization.SIGN_UP_EMAIL!
371
+ })
372
+ }
373
+ } catch (error) {
374
+ toast({
375
+ variant: "error",
376
+ message: getLocalizedError({ error, localization })
377
+ })
378
+
379
+ form.resetField("password")
380
+ form.resetField("confirmPassword")
381
+ resetCaptcha()
382
+ }
383
+ }
384
+
385
+ return (
386
+ <Form {...form}>
387
+ <form
388
+ onSubmit={form.handleSubmit(signUp)}
389
+ noValidate={isHydrated}
390
+ className={cn("grid w-full gap-6", className, classNames?.base)}
391
+ >
392
+ {signUpFields?.includes("image") && avatar && (
393
+ <>
394
+ <input
395
+ ref={fileInputRef}
396
+ accept="image/*"
397
+ disabled={uploadingAvatar}
398
+ hidden
399
+ type="file"
400
+ onChange={(e) => {
401
+ const file = e.target.files?.item(0)
402
+ if (file) handleAvatarChange(file)
403
+ e.target.value = ""
404
+ }}
405
+ />
406
+
407
+ <FormField
408
+ control={form.control}
409
+ name="image"
410
+ render={() => (
411
+ <FormItem>
412
+ <FormLabel>{localization.AVATAR}</FormLabel>
413
+
414
+ <div className="flex items-center gap-4">
415
+ <DropdownMenu>
416
+ <DropdownMenuTrigger asChild>
417
+ <Button
418
+ className="size-fit rounded-full"
419
+ size="icon"
420
+ variant="ghost"
421
+ type="button"
422
+ >
423
+ <UserAvatar
424
+ isPending={
425
+ uploadingAvatar
426
+ }
427
+ className="size-16"
428
+ user={
429
+ avatarImage
430
+ ? {
431
+ name: form.watch(
432
+ "name"
433
+ ) as string,
434
+ email: form.watch(
435
+ "email"
436
+ ) as string,
437
+ image: avatarImage
438
+ }
439
+ : null
440
+ }
441
+ localization={
442
+ localization
443
+ }
444
+ />
445
+ </Button>
446
+ </DropdownMenuTrigger>
447
+
448
+ <DropdownMenuContent
449
+ align="start"
450
+ onCloseAutoFocus={(e) =>
451
+ e.preventDefault()
452
+ }
453
+ >
454
+ <DropdownMenuItem
455
+ onClick={openFileDialog}
456
+ disabled={uploadingAvatar}
457
+ >
458
+ <UploadCloudIcon />
459
+ {localization.UPLOAD_AVATAR}
460
+ </DropdownMenuItem>
461
+
462
+ {avatarImage && (
463
+ <DropdownMenuItem
464
+ onClick={
465
+ handleDeleteAvatar
466
+ }
467
+ disabled={
468
+ uploadingAvatar
469
+ }
470
+ variant="destructive"
471
+ >
472
+ <Trash2Icon />
473
+ {
474
+ localization.DELETE_AVATAR
475
+ }
476
+ </DropdownMenuItem>
477
+ )}
478
+ </DropdownMenuContent>
479
+ </DropdownMenu>
480
+
481
+ <Button
482
+ type="button"
483
+ variant="outline"
484
+ onClick={openFileDialog}
485
+ disabled={uploadingAvatar}
486
+ >
487
+ {uploadingAvatar && (
488
+ <Loader2 className="animate-spin" />
489
+ )}
490
+
491
+ {localization.UPLOAD}
492
+ </Button>
493
+ </div>
494
+
495
+ <FormMessage />
496
+ </FormItem>
497
+ )}
498
+ />
499
+ </>
500
+ )}
501
+
502
+ {signUpFields?.includes("name") && (
503
+ <FormField
504
+ control={form.control}
505
+ name="name"
506
+ render={({ field }) => (
507
+ <FormItem>
508
+ <FormLabel className={classNames?.label}>
509
+ {localization.NAME}
510
+ </FormLabel>
511
+
512
+ <FormControl>
513
+ <Input
514
+ className={classNames?.input}
515
+ placeholder={
516
+ localization.NAME_PLACEHOLDER
517
+ }
518
+ disabled={isSubmitting}
519
+ {...field}
520
+ value={field.value as string}
521
+ />
522
+ </FormControl>
523
+
524
+ <FormMessage className={classNames?.error} />
525
+ </FormItem>
526
+ )}
527
+ />
528
+ )}
529
+
530
+ {usernameEnabled && (
531
+ <FormField
532
+ control={form.control}
533
+ name="username"
534
+ render={({ field }) => (
535
+ <FormItem>
536
+ <FormLabel className={classNames?.label}>
537
+ {localization.USERNAME}
538
+ </FormLabel>
539
+
540
+ <FormControl>
541
+ <Input
542
+ className={classNames?.input}
543
+ placeholder={
544
+ localization.USERNAME_PLACEHOLDER
545
+ }
546
+ disabled={isSubmitting}
547
+ {...field}
548
+ value={field.value as string}
549
+ />
550
+ </FormControl>
551
+
552
+ <FormMessage className={classNames?.error} />
553
+ </FormItem>
554
+ )}
555
+ />
556
+ )}
557
+
558
+ <FormField
559
+ control={form.control}
560
+ name="email"
561
+ render={({ field }) => (
562
+ <FormItem>
563
+ <FormLabel className={classNames?.label}>
564
+ {localization.EMAIL}
565
+ </FormLabel>
566
+
567
+ <FormControl>
568
+ <Input
569
+ className={classNames?.input}
570
+ type="email"
571
+ placeholder={localization.EMAIL_PLACEHOLDER}
572
+ disabled={isSubmitting}
573
+ {...field}
574
+ value={field.value as string}
575
+ />
576
+ </FormControl>
577
+
578
+ <FormMessage className={classNames?.error} />
579
+ </FormItem>
580
+ )}
581
+ />
582
+
583
+ <FormField
584
+ control={form.control}
585
+ name="password"
586
+ render={({ field }) => (
587
+ <FormItem>
588
+ <FormLabel className={classNames?.label}>
589
+ {localization.PASSWORD}
590
+ </FormLabel>
591
+
592
+ <FormControl>
593
+ <PasswordInput
594
+ autoComplete="new-password"
595
+ className={classNames?.input}
596
+ placeholder={
597
+ localization.PASSWORD_PLACEHOLDER
598
+ }
599
+ disabled={isSubmitting}
600
+ enableToggle
601
+ {...field}
602
+ value={field.value as string}
603
+ />
604
+ </FormControl>
605
+
606
+ <FormMessage className={classNames?.error} />
607
+ </FormItem>
608
+ )}
609
+ />
610
+
611
+ {confirmPasswordEnabled && (
612
+ <FormField
613
+ control={form.control}
614
+ name="confirmPassword"
615
+ render={({ field }) => (
616
+ <FormItem>
617
+ <FormLabel className={classNames?.label}>
618
+ {localization.CONFIRM_PASSWORD}
619
+ </FormLabel>
620
+
621
+ <FormControl>
622
+ <PasswordInput
623
+ autoComplete="new-password"
624
+ className={classNames?.input}
625
+ placeholder={
626
+ localization.CONFIRM_PASSWORD_PLACEHOLDER
627
+ }
628
+ disabled={isSubmitting}
629
+ enableToggle
630
+ {...field}
631
+ value={field.value as string}
632
+ />
633
+ </FormControl>
634
+
635
+ <FormMessage className={classNames?.error} />
636
+ </FormItem>
637
+ )}
638
+ />
639
+ )}
640
+
641
+ {signUpFields
642
+ ?.filter((field) => field !== "name" && field !== "image")
643
+ .map((field) => {
644
+ const additionalField = additionalFields?.[field]
645
+ if (!additionalField) {
646
+ console.error(`Additional field ${field} not found`)
647
+ return null
648
+ }
649
+
650
+ return additionalField.type === "boolean" ? (
651
+ <FormField
652
+ key={field}
653
+ control={form.control}
654
+ name={field}
655
+ render={({ field: formField }) => (
656
+ <FormItem className="flex">
657
+ <FormControl>
658
+ <Checkbox
659
+ checked={
660
+ formField.value as boolean
661
+ }
662
+ onCheckedChange={
663
+ formField.onChange
664
+ }
665
+ disabled={isSubmitting}
666
+ />
667
+ </FormControl>
668
+
669
+ <FormLabel
670
+ className={classNames?.label}
671
+ >
672
+ {additionalField.label}
673
+ </FormLabel>
674
+
675
+ <FormMessage
676
+ className={classNames?.error}
677
+ />
678
+ </FormItem>
679
+ )}
680
+ />
681
+ ) : (
682
+ <FormField
683
+ key={field}
684
+ control={form.control}
685
+ name={field}
686
+ render={({ field: formField }) => (
687
+ <FormItem>
688
+ <FormLabel
689
+ className={classNames?.label}
690
+ >
691
+ {additionalField.label}
692
+ </FormLabel>
693
+
694
+ <FormControl>
695
+ {additionalField.type ===
696
+ "number" ? (
697
+ <Input
698
+ className={
699
+ classNames?.input
700
+ }
701
+ type="number"
702
+ placeholder={
703
+ additionalField.placeholder ||
704
+ (typeof additionalField.label ===
705
+ "string"
706
+ ? additionalField.label
707
+ : "")
708
+ }
709
+ disabled={isSubmitting}
710
+ {...formField}
711
+ value={
712
+ formField.value as number
713
+ }
714
+ />
715
+ ) : additionalField.multiline ? (
716
+ <Textarea
717
+ className={
718
+ classNames?.input
719
+ }
720
+ placeholder={
721
+ additionalField.placeholder ||
722
+ (typeof additionalField.label ===
723
+ "string"
724
+ ? additionalField.label
725
+ : "")
726
+ }
727
+ disabled={isSubmitting}
728
+ {...formField}
729
+ value={
730
+ formField.value as string
731
+ }
732
+ />
733
+ ) : (
734
+ <Input
735
+ className={
736
+ classNames?.input
737
+ }
738
+ type="text"
739
+ placeholder={
740
+ additionalField.placeholder ||
741
+ (typeof additionalField.label ===
742
+ "string"
743
+ ? additionalField.label
744
+ : "")
745
+ }
746
+ disabled={isSubmitting}
747
+ {...formField}
748
+ value={
749
+ formField.value as string
750
+ }
751
+ />
752
+ )}
753
+ </FormControl>
754
+
755
+ <FormMessage
756
+ className={classNames?.error}
757
+ />
758
+ </FormItem>
759
+ )}
760
+ />
761
+ )
762
+ })}
763
+
764
+ <Captcha
765
+ ref={captchaRef}
766
+ localization={localization}
767
+ action="/sign-up/email"
768
+ />
769
+
770
+ <Button
771
+ type="submit"
772
+ disabled={isSubmitting}
773
+ className={cn(
774
+ "w-full",
775
+ classNames?.button,
776
+ classNames?.primaryButton
777
+ )}
778
+ >
779
+ {isSubmitting ? (
780
+ <Loader2 className="animate-spin" />
781
+ ) : (
782
+ localization.SIGN_UP_ACTION
783
+ )}
784
+ </Button>
785
+ </form>
786
+ </Form>
787
+ )
788
+ }