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,138 @@
1
+ "use client"
2
+ import { zodResolver } from "@hookform/resolvers/zod"
3
+ import { Loader2 } from "lucide-react"
4
+ import { useContext, useEffect } from "react"
5
+ import { useForm } from "react-hook-form"
6
+ import * as z from "zod"
7
+
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 { Input } from "../../ui/input"
22
+ import type { AuthFormClassNames } from "../auth-form"
23
+
24
+ export interface RecoverAccountFormProps {
25
+ className?: string
26
+ classNames?: AuthFormClassNames
27
+ isSubmitting?: boolean
28
+ localization: Partial<AuthLocalization>
29
+ redirectTo?: string
30
+ setIsSubmitting?: (value: boolean) => void
31
+ }
32
+
33
+ export function RecoverAccountForm({
34
+ className,
35
+ classNames,
36
+ isSubmitting,
37
+ localization,
38
+ redirectTo,
39
+ setIsSubmitting
40
+ }: RecoverAccountFormProps) {
41
+ const {
42
+ authClient,
43
+ localization: contextLocalization,
44
+ toast
45
+ } = useContext(AuthUIContext)
46
+
47
+ localization = { ...contextLocalization, ...localization }
48
+
49
+ const { onSuccess, isPending: transitionPending } = useOnSuccessTransition({
50
+ redirectTo
51
+ })
52
+
53
+ const formSchema = z.object({
54
+ code: z.string().min(1, { message: localization.BACKUP_CODE_REQUIRED })
55
+ })
56
+
57
+ const form = useForm({
58
+ resolver: zodResolver(formSchema),
59
+ defaultValues: {
60
+ code: ""
61
+ }
62
+ })
63
+
64
+ isSubmitting =
65
+ isSubmitting || form.formState.isSubmitting || transitionPending
66
+
67
+ useEffect(() => {
68
+ setIsSubmitting?.(form.formState.isSubmitting || transitionPending)
69
+ }, [form.formState.isSubmitting, transitionPending, setIsSubmitting])
70
+
71
+ async function verifyBackupCode({ code }: z.infer<typeof formSchema>) {
72
+ try {
73
+ await authClient.twoFactor.verifyBackupCode({
74
+ code,
75
+ fetchOptions: { throw: true }
76
+ })
77
+
78
+ await onSuccess()
79
+ } catch (error) {
80
+ toast({
81
+ variant: "error",
82
+ message: getLocalizedError({ error, localization })
83
+ })
84
+
85
+ form.reset()
86
+ }
87
+ }
88
+
89
+ return (
90
+ <Form {...form}>
91
+ <form
92
+ onSubmit={form.handleSubmit(verifyBackupCode)}
93
+ className={cn("grid gap-6", className, classNames?.base)}
94
+ >
95
+ <FormField
96
+ control={form.control}
97
+ name="code"
98
+ render={({ field }) => (
99
+ <FormItem>
100
+ <FormLabel className={classNames?.label}>
101
+ {localization.BACKUP_CODE}
102
+ </FormLabel>
103
+
104
+ <FormControl>
105
+ <Input
106
+ placeholder={
107
+ localization.BACKUP_CODE_PLACEHOLDER
108
+ }
109
+ autoComplete="off"
110
+ className={classNames?.input}
111
+ disabled={isSubmitting}
112
+ {...field}
113
+ />
114
+ </FormControl>
115
+
116
+ <FormMessage className={classNames?.error} />
117
+ </FormItem>
118
+ )}
119
+ />
120
+
121
+ <Button
122
+ type="submit"
123
+ disabled={isSubmitting}
124
+ className={cn(
125
+ classNames?.button,
126
+ classNames?.primaryButton
127
+ )}
128
+ >
129
+ {isSubmitting ? (
130
+ <Loader2 className="animate-spin" />
131
+ ) : (
132
+ localization.RECOVER_ACCOUNT_ACTION
133
+ )}
134
+ </Button>
135
+ </form>
136
+ </Form>
137
+ )
138
+ }
@@ -0,0 +1,215 @@
1
+ "use client"
2
+
3
+ import { zodResolver } from "@hookform/resolvers/zod"
4
+ import { Loader2 } from "lucide-react"
5
+ import { useContext, useEffect, useRef } from "react"
6
+ import { useForm } from "react-hook-form"
7
+ import * as z from "zod"
8
+
9
+ import { AuthUIContext } from "../../../lib/auth-ui-provider"
10
+ import { cn, getLocalizedError, getPasswordSchema } from "../../../lib/utils"
11
+ import type { AuthLocalization } from "../../../localization/auth-localization"
12
+ import type { PasswordValidation } from "../../../types/password-validation"
13
+ import { PasswordInput } from "../../password-input"
14
+ import { Button } from "../../ui/button"
15
+ import {
16
+ Form,
17
+ FormControl,
18
+ FormField,
19
+ FormItem,
20
+ FormLabel,
21
+ FormMessage
22
+ } from "../../ui/form"
23
+ import type { AuthFormClassNames } from "../auth-form"
24
+
25
+ export interface ResetPasswordFormProps {
26
+ className?: string
27
+ classNames?: AuthFormClassNames
28
+ localization: Partial<AuthLocalization>
29
+ passwordValidation?: PasswordValidation
30
+ }
31
+
32
+ export function ResetPasswordForm({
33
+ className,
34
+ classNames,
35
+ localization,
36
+ passwordValidation
37
+ }: ResetPasswordFormProps) {
38
+ const tokenChecked = useRef(false)
39
+
40
+ const {
41
+ authClient,
42
+ basePath,
43
+ credentials,
44
+ localization: contextLocalization,
45
+ viewPaths,
46
+ navigate,
47
+ toast
48
+ } = useContext(AuthUIContext)
49
+
50
+ const confirmPasswordEnabled = credentials?.confirmPassword
51
+ const contextPasswordValidation = credentials?.passwordValidation
52
+
53
+ localization = { ...contextLocalization, ...localization }
54
+ passwordValidation = { ...contextPasswordValidation, ...passwordValidation }
55
+
56
+ const formSchema = z
57
+ .object({
58
+ newPassword: getPasswordSchema(passwordValidation, {
59
+ PASSWORD_REQUIRED: localization.NEW_PASSWORD_REQUIRED,
60
+ PASSWORD_TOO_SHORT: localization.PASSWORD_TOO_SHORT,
61
+ PASSWORD_TOO_LONG: localization.PASSWORD_TOO_LONG,
62
+ INVALID_PASSWORD: localization.INVALID_PASSWORD
63
+ }),
64
+ confirmPassword: confirmPasswordEnabled
65
+ ? getPasswordSchema(passwordValidation, {
66
+ PASSWORD_REQUIRED: localization.CONFIRM_PASSWORD_REQUIRED,
67
+ PASSWORD_TOO_SHORT: localization.PASSWORD_TOO_SHORT,
68
+ PASSWORD_TOO_LONG: localization.PASSWORD_TOO_LONG,
69
+ INVALID_PASSWORD: localization.INVALID_PASSWORD
70
+ })
71
+ : z.string().optional()
72
+ })
73
+ .refine(
74
+ (data) =>
75
+ !confirmPasswordEnabled ||
76
+ data.newPassword === data.confirmPassword,
77
+ {
78
+ message: localization.PASSWORDS_DO_NOT_MATCH,
79
+ path: ["confirmPassword"]
80
+ }
81
+ )
82
+
83
+ const form = useForm({
84
+ resolver: zodResolver(formSchema),
85
+ defaultValues: {
86
+ newPassword: "",
87
+ confirmPassword: ""
88
+ }
89
+ })
90
+
91
+ const isSubmitting = form.formState.isSubmitting
92
+
93
+ useEffect(() => {
94
+ if (tokenChecked.current) return
95
+ tokenChecked.current = true
96
+
97
+ const searchParams = new URLSearchParams(window.location.search)
98
+ const token = searchParams.get("token")
99
+
100
+ if (!token || token === "INVALID_TOKEN") {
101
+ navigate(
102
+ `${basePath}/${viewPaths.SIGN_IN}${window.location.search}`
103
+ )
104
+ toast({ variant: "error", message: localization.INVALID_TOKEN })
105
+ }
106
+ }, [basePath, navigate, toast, viewPaths, localization])
107
+
108
+ async function resetPassword({ newPassword }: z.infer<typeof formSchema>) {
109
+ try {
110
+ const searchParams = new URLSearchParams(window.location.search)
111
+ const token = searchParams.get("token") as string
112
+
113
+ await authClient.resetPassword({
114
+ newPassword,
115
+ token,
116
+ fetchOptions: { throw: true }
117
+ })
118
+
119
+ toast({
120
+ variant: "success",
121
+ message: localization.RESET_PASSWORD_SUCCESS
122
+ })
123
+
124
+ navigate(
125
+ `${basePath}/${viewPaths.SIGN_IN}${window.location.search}`
126
+ )
127
+ } catch (error) {
128
+ toast({
129
+ variant: "error",
130
+ message: getLocalizedError({ error, localization })
131
+ })
132
+
133
+ form.reset()
134
+ }
135
+ }
136
+
137
+ return (
138
+ <Form {...form}>
139
+ <form
140
+ onSubmit={form.handleSubmit(resetPassword)}
141
+ className={cn("grid w-full gap-6", className, classNames?.base)}
142
+ >
143
+ <FormField
144
+ control={form.control}
145
+ name="newPassword"
146
+ render={({ field }) => (
147
+ <FormItem>
148
+ <FormLabel className={classNames?.label}>
149
+ {localization.NEW_PASSWORD}
150
+ </FormLabel>
151
+
152
+ <FormControl>
153
+ <PasswordInput
154
+ autoComplete="new-password"
155
+ className={classNames?.input}
156
+ placeholder={
157
+ localization.NEW_PASSWORD_PLACEHOLDER
158
+ }
159
+ disabled={isSubmitting}
160
+ {...field}
161
+ />
162
+ </FormControl>
163
+
164
+ <FormMessage className={classNames?.error} />
165
+ </FormItem>
166
+ )}
167
+ />
168
+
169
+ {confirmPasswordEnabled && (
170
+ <FormField
171
+ control={form.control}
172
+ name="confirmPassword"
173
+ render={({ field }) => (
174
+ <FormItem>
175
+ <FormLabel className={classNames?.label}>
176
+ {localization.CONFIRM_PASSWORD}
177
+ </FormLabel>
178
+
179
+ <FormControl>
180
+ <PasswordInput
181
+ autoComplete="new-password"
182
+ className={classNames?.input}
183
+ placeholder={
184
+ localization.CONFIRM_PASSWORD_PLACEHOLDER
185
+ }
186
+ disabled={isSubmitting}
187
+ {...field}
188
+ />
189
+ </FormControl>
190
+
191
+ <FormMessage className={classNames?.error} />
192
+ </FormItem>
193
+ )}
194
+ />
195
+ )}
196
+
197
+ <Button
198
+ type="submit"
199
+ disabled={isSubmitting}
200
+ className={cn(
201
+ "w-full",
202
+ classNames?.button,
203
+ classNames?.primaryButton
204
+ )}
205
+ >
206
+ {isSubmitting ? (
207
+ <Loader2 className="animate-spin" />
208
+ ) : (
209
+ localization.RESET_PASSWORD_ACTION
210
+ )}
211
+ </Button>
212
+ </form>
213
+ </Form>
214
+ )
215
+ }
@@ -0,0 +1,289 @@
1
+ "use client"
2
+
3
+ import type { BetterFetchOption } from "@better-fetch/fetch"
4
+ import { zodResolver } from "@hookform/resolvers/zod"
5
+ import { Loader2 } from "lucide-react"
6
+ import { useContext, useEffect } 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 {
15
+ cn,
16
+ getLocalizedError,
17
+ getPasswordSchema,
18
+ isValidEmail
19
+ } from "../../../lib/utils"
20
+ import type { AuthLocalization } from "../../../localization/auth-localization"
21
+ import type { PasswordValidation } from "../../../types/password-validation"
22
+ import { Captcha } from "../../captcha/captcha"
23
+ import { PasswordInput } from "../../password-input"
24
+ import { Button } from "../../ui/button"
25
+ import { Checkbox } from "../../ui/checkbox"
26
+ import {
27
+ Form,
28
+ FormControl,
29
+ FormField,
30
+ FormItem,
31
+ FormLabel,
32
+ FormMessage
33
+ } from "../../ui/form"
34
+ import { Input } from "../../ui/input"
35
+ import type { AuthFormClassNames } from "../auth-form"
36
+
37
+ export interface SignInFormProps {
38
+ className?: string
39
+ classNames?: AuthFormClassNames
40
+ isSubmitting?: boolean
41
+ localization: Partial<AuthLocalization>
42
+ redirectTo?: string
43
+ setIsSubmitting?: (isSubmitting: boolean) => void
44
+ passwordValidation?: PasswordValidation
45
+ }
46
+
47
+ export function SignInForm({
48
+ className,
49
+ classNames,
50
+ isSubmitting,
51
+ localization,
52
+ redirectTo,
53
+ setIsSubmitting,
54
+ passwordValidation
55
+ }: SignInFormProps) {
56
+ const isHydrated = useIsHydrated()
57
+ const { captchaRef, getCaptchaHeaders, resetCaptcha } = useCaptcha({
58
+ localization
59
+ })
60
+
61
+ const {
62
+ authClient,
63
+ basePath,
64
+ credentials,
65
+ localization: contextLocalization,
66
+ viewPaths,
67
+ navigate,
68
+ toast,
69
+ Link
70
+ } = useContext(AuthUIContext)
71
+
72
+ const rememberMeEnabled = credentials?.rememberMe
73
+ const usernameEnabled = credentials?.username
74
+ const contextPasswordValidation = credentials?.passwordValidation
75
+
76
+ localization = { ...contextLocalization, ...localization }
77
+ passwordValidation = { ...contextPasswordValidation, ...passwordValidation }
78
+
79
+ const { onSuccess, isPending: transitionPending } = useOnSuccessTransition({
80
+ redirectTo
81
+ })
82
+
83
+ const formSchema = z.object({
84
+ email: usernameEnabled
85
+ ? z.string().min(1, {
86
+ message: `${localization.USERNAME} ${localization.IS_REQUIRED}`
87
+ })
88
+ : z.string().email({
89
+ message: `${localization.EMAIL} ${localization.IS_INVALID}`
90
+ }),
91
+ password: getPasswordSchema(passwordValidation, localization),
92
+ rememberMe: z.boolean().optional()
93
+ })
94
+
95
+ const form = useForm({
96
+ resolver: zodResolver(formSchema),
97
+ defaultValues: {
98
+ email: "",
99
+ password: "",
100
+ rememberMe: !rememberMeEnabled
101
+ }
102
+ })
103
+
104
+ isSubmitting =
105
+ isSubmitting || form.formState.isSubmitting || transitionPending
106
+
107
+ useEffect(() => {
108
+ setIsSubmitting?.(form.formState.isSubmitting || transitionPending)
109
+ }, [form.formState.isSubmitting, transitionPending, setIsSubmitting])
110
+
111
+ async function signIn({
112
+ email,
113
+ password,
114
+ rememberMe
115
+ }: z.infer<typeof formSchema>) {
116
+ try {
117
+ let response: Record<string, unknown> = {}
118
+
119
+ if (usernameEnabled && !isValidEmail(email)) {
120
+ const fetchOptions: BetterFetchOption = {
121
+ throw: true,
122
+ headers: await getCaptchaHeaders("/sign-in/username")
123
+ }
124
+
125
+ response = await authClient.signIn.username({
126
+ username: email,
127
+ password,
128
+ rememberMe,
129
+ fetchOptions
130
+ })
131
+ } else {
132
+ const fetchOptions: BetterFetchOption = {
133
+ throw: true,
134
+ headers: await getCaptchaHeaders("/sign-in/email")
135
+ }
136
+
137
+ response = await authClient.signIn.email({
138
+ email,
139
+ password,
140
+ rememberMe,
141
+ fetchOptions
142
+ })
143
+ }
144
+
145
+ if (response.twoFactorRedirect) {
146
+ navigate(
147
+ `${basePath}/${viewPaths.TWO_FACTOR}${window.location.search}`
148
+ )
149
+ } else {
150
+ await onSuccess()
151
+ }
152
+ } catch (error) {
153
+ form.resetField("password")
154
+ resetCaptcha()
155
+
156
+ toast({
157
+ variant: "error",
158
+ message: getLocalizedError({ error, localization })
159
+ })
160
+ }
161
+ }
162
+
163
+ return (
164
+ <Form {...form}>
165
+ <form
166
+ onSubmit={form.handleSubmit(signIn)}
167
+ noValidate={isHydrated}
168
+ className={cn("grid w-full gap-6", className, classNames?.base)}
169
+ >
170
+ <FormField
171
+ control={form.control}
172
+ name="email"
173
+ render={({ field }) => (
174
+ <FormItem>
175
+ <FormLabel className={classNames?.label}>
176
+ {usernameEnabled
177
+ ? localization.USERNAME
178
+ : localization.EMAIL}
179
+ </FormLabel>
180
+
181
+ <FormControl>
182
+ <Input
183
+ autoComplete={
184
+ usernameEnabled ? "username" : "email"
185
+ }
186
+ className={classNames?.input}
187
+ type={usernameEnabled ? "text" : "email"}
188
+ placeholder={
189
+ usernameEnabled
190
+ ? localization.SIGN_IN_USERNAME_PLACEHOLDER
191
+ : localization.EMAIL_PLACEHOLDER
192
+ }
193
+ disabled={isSubmitting}
194
+ {...field}
195
+ />
196
+ </FormControl>
197
+
198
+ <FormMessage className={classNames?.error} />
199
+ </FormItem>
200
+ )}
201
+ />
202
+
203
+ <FormField
204
+ control={form.control}
205
+ name="password"
206
+ render={({ field }) => (
207
+ <FormItem>
208
+ <div className="flex items-center justify-between">
209
+ <FormLabel className={classNames?.label}>
210
+ {localization.PASSWORD}
211
+ </FormLabel>
212
+
213
+ {credentials?.forgotPassword && (
214
+ <Link
215
+ className={cn(
216
+ "text-sm hover:underline",
217
+ classNames?.forgotPasswordLink
218
+ )}
219
+ href={`${basePath}/${viewPaths.FORGOT_PASSWORD}${isHydrated ? window.location.search : ""}`}
220
+ >
221
+ {localization.FORGOT_PASSWORD_LINK}
222
+ </Link>
223
+ )}
224
+ </div>
225
+
226
+ <FormControl>
227
+ <PasswordInput
228
+ autoComplete="current-password"
229
+ className={classNames?.input}
230
+ placeholder={
231
+ localization.PASSWORD_PLACEHOLDER
232
+ }
233
+ disabled={isSubmitting}
234
+ {...field}
235
+ />
236
+ </FormControl>
237
+
238
+ <FormMessage className={classNames?.error} />
239
+ </FormItem>
240
+ )}
241
+ />
242
+
243
+ {rememberMeEnabled && (
244
+ <FormField
245
+ control={form.control}
246
+ name="rememberMe"
247
+ render={({ field }) => (
248
+ <FormItem className="flex">
249
+ <FormControl>
250
+ <Checkbox
251
+ checked={field.value}
252
+ onCheckedChange={field.onChange}
253
+ disabled={isSubmitting}
254
+ />
255
+ </FormControl>
256
+
257
+ <FormLabel>
258
+ {localization.REMEMBER_ME}
259
+ </FormLabel>
260
+ </FormItem>
261
+ )}
262
+ />
263
+ )}
264
+
265
+ <Captcha
266
+ ref={captchaRef}
267
+ localization={localization}
268
+ action="/sign-in/email"
269
+ />
270
+
271
+ <Button
272
+ type="submit"
273
+ disabled={isSubmitting}
274
+ className={cn(
275
+ "w-full",
276
+ classNames?.button,
277
+ classNames?.primaryButton
278
+ )}
279
+ >
280
+ {isSubmitting ? (
281
+ <Loader2 className="animate-spin" />
282
+ ) : (
283
+ localization.SIGN_IN_ACTION
284
+ )}
285
+ </Button>
286
+ </form>
287
+ </Form>
288
+ )
289
+ }