@erikey/react 0.4.26 → 0.4.27

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 (143) hide show
  1. package/package.json +2 -1
  2. package/src/__tests__/auth-client.test.ts +105 -0
  3. package/src/__tests__/security/localStorage-encryption.test.ts +171 -0
  4. package/src/auth-client.ts +158 -0
  5. package/src/dashboard-client.ts +60 -0
  6. package/src/index.ts +88 -0
  7. package/src/kv-client.ts +316 -0
  8. package/src/lib/cross-origin-auth.ts +99 -0
  9. package/src/stubs/captcha.ts +24 -0
  10. package/src/stubs/hashes.ts +16 -0
  11. package/src/stubs/index.ts +17 -0
  12. package/src/stubs/passkey.ts +12 -0
  13. package/src/stubs/qr-code.ts +10 -0
  14. package/src/stubs/query.ts +16 -0
  15. package/src/stubs/realtime.ts +17 -0
  16. package/src/stubs/use-sync-external-store.ts +12 -0
  17. package/src/styles.css +141 -0
  18. package/src/types.ts +14 -0
  19. package/src/ui/components/auth/auth-callback.tsx +36 -0
  20. package/src/ui/components/auth/auth-form.tsx +310 -0
  21. package/src/ui/components/auth/auth-view.tsx +435 -0
  22. package/src/ui/components/auth/email-otp-button.tsx +53 -0
  23. package/src/ui/components/auth/forms/email-otp-form.tsx +312 -0
  24. package/src/ui/components/auth/forms/email-verification-form.tsx +271 -0
  25. package/src/ui/components/auth/forms/forgot-password-form.tsx +173 -0
  26. package/src/ui/components/auth/forms/magic-link-form.tsx +196 -0
  27. package/src/ui/components/auth/forms/recover-account-form.tsx +143 -0
  28. package/src/ui/components/auth/forms/reset-password-form.tsx +220 -0
  29. package/src/ui/components/auth/forms/sign-in-form.tsx +323 -0
  30. package/src/ui/components/auth/forms/sign-up-form.tsx +820 -0
  31. package/src/ui/components/auth/forms/two-factor-form.tsx +381 -0
  32. package/src/ui/components/auth/magic-link-button.tsx +54 -0
  33. package/src/ui/components/auth/one-tap.tsx +53 -0
  34. package/src/ui/components/auth/otp-input-group.tsx +65 -0
  35. package/src/ui/components/auth/passkey-button.tsx +91 -0
  36. package/src/ui/components/auth/provider-button.tsx +155 -0
  37. package/src/ui/components/auth/sign-out.tsx +25 -0
  38. package/src/ui/components/auth/wallet-button.tsx +192 -0
  39. package/src/ui/components/auth-loading.tsx +21 -0
  40. package/src/ui/components/captcha/captcha.tsx +91 -0
  41. package/src/ui/components/captcha/recaptcha-badge.tsx +61 -0
  42. package/src/ui/components/captcha/recaptcha-v2.tsx +58 -0
  43. package/src/ui/components/captcha/recaptcha-v3.tsx +73 -0
  44. package/src/ui/components/email/email-template.tsx +216 -0
  45. package/src/ui/components/form-error.tsx +27 -0
  46. package/src/ui/components/password-input.tsx +56 -0
  47. package/src/ui/components/provider-icons.tsx +404 -0
  48. package/src/ui/components/redirect-to-sign-in.tsx +16 -0
  49. package/src/ui/components/redirect-to-sign-up.tsx +16 -0
  50. package/src/ui/components/signed-in.tsx +20 -0
  51. package/src/ui/components/signed-out.tsx +20 -0
  52. package/src/ui/components/ui/alert.tsx +66 -0
  53. package/src/ui/components/ui/button.tsx +70 -0
  54. package/src/ui/components/ui/card.tsx +92 -0
  55. package/src/ui/components/ui/checkbox.tsx +66 -0
  56. package/src/ui/components/ui/field.tsx +248 -0
  57. package/src/ui/components/ui/form.tsx +165 -0
  58. package/src/ui/components/ui/input-otp.tsx +77 -0
  59. package/src/ui/components/ui/input.tsx +21 -0
  60. package/src/ui/components/ui/label.tsx +23 -0
  61. package/src/ui/components/ui/separator.tsx +34 -0
  62. package/src/ui/components/ui/skeleton.tsx +13 -0
  63. package/src/ui/components/ui/textarea.tsx +18 -0
  64. package/src/ui/components/user-avatar.tsx +151 -0
  65. package/src/ui/hooks/use-auth-data.ts +193 -0
  66. package/src/ui/hooks/use-authenticate.ts +64 -0
  67. package/src/ui/hooks/use-captcha.tsx +151 -0
  68. package/src/ui/hooks/use-hydrated.ts +13 -0
  69. package/src/ui/hooks/use-lang.ts +32 -0
  70. package/src/ui/hooks/use-success-transition.ts +41 -0
  71. package/src/ui/hooks/use-theme.ts +39 -0
  72. package/src/ui/index.ts +46 -0
  73. package/src/ui/instantdb.ts +1 -0
  74. package/src/ui/lib/auth-data-cache.ts +90 -0
  75. package/src/ui/lib/auth-ui-provider.tsx +769 -0
  76. package/src/ui/lib/gravatar-utils.ts +58 -0
  77. package/src/ui/lib/image-utils.ts +55 -0
  78. package/src/ui/lib/instantdb/model-names.ts +24 -0
  79. package/src/ui/lib/instantdb/use-instant-options.ts +98 -0
  80. package/src/ui/lib/instantdb/use-list-accounts.ts +38 -0
  81. package/src/ui/lib/instantdb/use-list-sessions.ts +53 -0
  82. package/src/ui/lib/instantdb/use-session.ts +55 -0
  83. package/src/ui/lib/social-providers.ts +150 -0
  84. package/src/ui/lib/tanstack/auth-ui-provider-tanstack.tsx +49 -0
  85. package/src/ui/lib/tanstack/use-tanstack-options.ts +112 -0
  86. package/src/ui/lib/triplit/model-names.ts +24 -0
  87. package/src/ui/lib/triplit/use-conditional-query.ts +82 -0
  88. package/src/ui/lib/triplit/use-list-accounts.ts +31 -0
  89. package/src/ui/lib/triplit/use-list-sessions.ts +33 -0
  90. package/src/ui/lib/triplit/use-session.ts +42 -0
  91. package/src/ui/lib/triplit/use-triplit-hooks.ts +68 -0
  92. package/src/ui/lib/triplit/use-triplit-token.ts +44 -0
  93. package/src/ui/lib/utils.ts +119 -0
  94. package/src/ui/lib/view-paths.ts +61 -0
  95. package/src/ui/lib/wallet.ts +129 -0
  96. package/src/ui/localization/admin-error-codes.ts +20 -0
  97. package/src/ui/localization/anonymous-error-codes.ts +6 -0
  98. package/src/ui/localization/api-key-error-codes.ts +32 -0
  99. package/src/ui/localization/auth-localization.ts +865 -0
  100. package/src/ui/localization/base-error-codes.ts +27 -0
  101. package/src/ui/localization/captcha-error-codes.ts +17 -0
  102. package/src/ui/localization/email-otp-error-codes.ts +7 -0
  103. package/src/ui/localization/generic-oauth-error-codes.ts +3 -0
  104. package/src/ui/localization/haveibeenpwned-error-codes.ts +4 -0
  105. package/src/ui/localization/multi-session-error-codes.ts +3 -0
  106. package/src/ui/localization/organization-error-codes.ts +57 -0
  107. package/src/ui/localization/passkey-error-codes.ts +10 -0
  108. package/src/ui/localization/phone-number-error-codes.ts +10 -0
  109. package/src/ui/localization/stripe-localization.ts +12 -0
  110. package/src/ui/localization/team-error-codes.ts +12 -0
  111. package/src/ui/localization/two-factor-error-codes.ts +12 -0
  112. package/src/ui/localization/username-error-codes.ts +9 -0
  113. package/src/ui/server.ts +4 -0
  114. package/src/ui/style.css +146 -0
  115. package/src/ui/tanstack.ts +1 -0
  116. package/src/ui/triplit.ts +1 -0
  117. package/src/ui/types/account-options.ts +35 -0
  118. package/src/ui/types/additional-fields.ts +21 -0
  119. package/src/ui/types/any-auth-client.ts +6 -0
  120. package/src/ui/types/api-key.ts +9 -0
  121. package/src/ui/types/auth-client.ts +41 -0
  122. package/src/ui/types/auth-hooks.ts +81 -0
  123. package/src/ui/types/auth-mutators.ts +21 -0
  124. package/src/ui/types/avatar-options.ts +29 -0
  125. package/src/ui/types/captcha-options.ts +32 -0
  126. package/src/ui/types/captcha-provider.ts +7 -0
  127. package/src/ui/types/credentials-options.ts +38 -0
  128. package/src/ui/types/delete-user-options.ts +7 -0
  129. package/src/ui/types/email-verification-options.ts +7 -0
  130. package/src/ui/types/fetch-error.ts +6 -0
  131. package/src/ui/types/generic-oauth-options.ts +16 -0
  132. package/src/ui/types/gravatar-options.ts +21 -0
  133. package/src/ui/types/image.ts +7 -0
  134. package/src/ui/types/invitation.ts +10 -0
  135. package/src/ui/types/link.ts +7 -0
  136. package/src/ui/types/organization-options.ts +106 -0
  137. package/src/ui/types/password-validation.ts +16 -0
  138. package/src/ui/types/profile.ts +15 -0
  139. package/src/ui/types/refetch.ts +1 -0
  140. package/src/ui/types/render-toast.ts +9 -0
  141. package/src/ui/types/sign-up-options.ts +7 -0
  142. package/src/ui/types/social-options.ts +16 -0
  143. package/src/ui/types/team-options.ts +47 -0
@@ -0,0 +1,220 @@
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
+ localizeErrors
49
+ } = useContext(AuthUIContext)
50
+
51
+ const confirmPasswordEnabled = credentials?.confirmPassword
52
+ const contextPasswordValidation = credentials?.passwordValidation
53
+
54
+ localization = { ...contextLocalization, ...localization }
55
+ passwordValidation = { ...contextPasswordValidation, ...passwordValidation }
56
+
57
+ const formSchema = z
58
+ .object({
59
+ newPassword: getPasswordSchema(passwordValidation, {
60
+ PASSWORD_REQUIRED: localization.NEW_PASSWORD_REQUIRED,
61
+ PASSWORD_TOO_SHORT: localization.PASSWORD_TOO_SHORT,
62
+ PASSWORD_TOO_LONG: localization.PASSWORD_TOO_LONG,
63
+ INVALID_PASSWORD: localization.INVALID_PASSWORD
64
+ }),
65
+ confirmPassword: confirmPasswordEnabled
66
+ ? getPasswordSchema(passwordValidation, {
67
+ PASSWORD_REQUIRED: localization.CONFIRM_PASSWORD_REQUIRED,
68
+ PASSWORD_TOO_SHORT: localization.PASSWORD_TOO_SHORT,
69
+ PASSWORD_TOO_LONG: localization.PASSWORD_TOO_LONG,
70
+ INVALID_PASSWORD: localization.INVALID_PASSWORD
71
+ })
72
+ : z.string().optional()
73
+ })
74
+ .refine(
75
+ (data) =>
76
+ !confirmPasswordEnabled ||
77
+ data.newPassword === data.confirmPassword,
78
+ {
79
+ message: localization.PASSWORDS_DO_NOT_MATCH,
80
+ path: ["confirmPassword"]
81
+ }
82
+ )
83
+
84
+ const form = useForm({
85
+ resolver: zodResolver(formSchema),
86
+ defaultValues: {
87
+ newPassword: "",
88
+ confirmPassword: ""
89
+ }
90
+ })
91
+
92
+ const isSubmitting = form.formState.isSubmitting
93
+
94
+ useEffect(() => {
95
+ if (tokenChecked.current) return
96
+ tokenChecked.current = true
97
+
98
+ const searchParams = new URLSearchParams(window.location.search)
99
+ const token = searchParams.get("token")
100
+
101
+ if (!token || token === "INVALID_TOKEN") {
102
+ navigate(
103
+ `${basePath}/${viewPaths.SIGN_IN}${window.location.search}`
104
+ )
105
+ toast({ variant: "error", message: localization.INVALID_TOKEN })
106
+ }
107
+ }, [basePath, navigate, toast, viewPaths, localization])
108
+
109
+ async function resetPassword({ newPassword }: z.infer<typeof formSchema>) {
110
+ try {
111
+ const searchParams = new URLSearchParams(window.location.search)
112
+ const token = searchParams.get("token") as string
113
+
114
+ await authClient.resetPassword({
115
+ newPassword,
116
+ token,
117
+ fetchOptions: { throw: true }
118
+ })
119
+
120
+ toast({
121
+ variant: "success",
122
+ message: localization.RESET_PASSWORD_SUCCESS
123
+ })
124
+
125
+ navigate(
126
+ `${basePath}/${viewPaths.SIGN_IN}${window.location.search}`
127
+ )
128
+ } catch (error) {
129
+ toast({
130
+ variant: "error",
131
+ message: getLocalizedError({
132
+ error,
133
+ localization,
134
+ localizeErrors
135
+ })
136
+ })
137
+
138
+ form.reset()
139
+ }
140
+ }
141
+
142
+ return (
143
+ <Form {...form}>
144
+ <form
145
+ onSubmit={form.handleSubmit(resetPassword)}
146
+ className={cn("grid w-full gap-6", className, classNames?.base)}
147
+ >
148
+ <FormField
149
+ control={form.control}
150
+ name="newPassword"
151
+ render={({ field }) => (
152
+ <FormItem>
153
+ <FormLabel className={classNames?.label}>
154
+ {localization.NEW_PASSWORD}
155
+ </FormLabel>
156
+
157
+ <FormControl>
158
+ <PasswordInput
159
+ autoComplete="new-password"
160
+ className={classNames?.input}
161
+ placeholder={
162
+ localization.NEW_PASSWORD_PLACEHOLDER
163
+ }
164
+ disabled={isSubmitting}
165
+ {...field}
166
+ />
167
+ </FormControl>
168
+
169
+ <FormMessage className={classNames?.error} />
170
+ </FormItem>
171
+ )}
172
+ />
173
+
174
+ {confirmPasswordEnabled && (
175
+ <FormField
176
+ control={form.control}
177
+ name="confirmPassword"
178
+ render={({ field }) => (
179
+ <FormItem>
180
+ <FormLabel className={classNames?.label}>
181
+ {localization.CONFIRM_PASSWORD}
182
+ </FormLabel>
183
+
184
+ <FormControl>
185
+ <PasswordInput
186
+ autoComplete="new-password"
187
+ className={classNames?.input}
188
+ placeholder={
189
+ localization.CONFIRM_PASSWORD_PLACEHOLDER
190
+ }
191
+ disabled={isSubmitting}
192
+ {...field}
193
+ />
194
+ </FormControl>
195
+
196
+ <FormMessage className={classNames?.error} />
197
+ </FormItem>
198
+ )}
199
+ />
200
+ )}
201
+
202
+ <Button
203
+ type="submit"
204
+ disabled={isSubmitting}
205
+ className={cn(
206
+ "w-full",
207
+ classNames?.button,
208
+ classNames?.primaryButton
209
+ )}
210
+ >
211
+ {isSubmitting ? (
212
+ <Loader2 className="animate-spin" />
213
+ ) : (
214
+ localization.RESET_PASSWORD_ACTION
215
+ )}
216
+ </Button>
217
+ </form>
218
+ </Form>
219
+ )
220
+ }
@@ -0,0 +1,323 @@
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
+ localizeErrors,
71
+ emailVerification
72
+ } = useContext(AuthUIContext)
73
+
74
+ const rememberMeEnabled = credentials?.rememberMe
75
+ const usernameEnabled = credentials?.username
76
+ const contextPasswordValidation = credentials?.passwordValidation
77
+
78
+ localization = { ...contextLocalization, ...localization }
79
+ passwordValidation = { ...contextPasswordValidation, ...passwordValidation }
80
+
81
+ const { onSuccess, isPending: transitionPending } = useOnSuccessTransition({
82
+ redirectTo
83
+ })
84
+
85
+ const formSchema = z.object({
86
+ email: usernameEnabled
87
+ ? z.string().min(1, {
88
+ message: `${localization.USERNAME} ${localization.IS_REQUIRED}`
89
+ })
90
+ : z.string().email({
91
+ message: `${localization.EMAIL} ${localization.IS_INVALID}`
92
+ }),
93
+ password: getPasswordSchema(passwordValidation, localization),
94
+ rememberMe: z.boolean().optional()
95
+ })
96
+
97
+ const form = useForm({
98
+ resolver: zodResolver(formSchema),
99
+ defaultValues: {
100
+ email: "",
101
+ password: "",
102
+ rememberMe: !rememberMeEnabled
103
+ }
104
+ })
105
+
106
+ isSubmitting =
107
+ isSubmitting || form.formState.isSubmitting || transitionPending
108
+
109
+ useEffect(() => {
110
+ setIsSubmitting?.(form.formState.isSubmitting || transitionPending)
111
+ }, [form.formState.isSubmitting, transitionPending, setIsSubmitting])
112
+
113
+ async function signIn({
114
+ email,
115
+ password,
116
+ rememberMe
117
+ }: z.infer<typeof formSchema>) {
118
+ try {
119
+ let response: Record<string, unknown> = {}
120
+
121
+ if (usernameEnabled && !isValidEmail(email)) {
122
+ const fetchOptions: BetterFetchOption = {
123
+ throw: true,
124
+ headers: await getCaptchaHeaders("/sign-in/username")
125
+ }
126
+
127
+ response = await authClient.signIn.username({
128
+ username: email,
129
+ password,
130
+ rememberMe,
131
+ fetchOptions
132
+ })
133
+ } else {
134
+ const fetchOptions: BetterFetchOption = {
135
+ throw: true,
136
+ headers: await getCaptchaHeaders("/sign-in/email")
137
+ }
138
+
139
+ response = await authClient.signIn.email({
140
+ email,
141
+ password,
142
+ rememberMe,
143
+ fetchOptions
144
+ })
145
+ }
146
+
147
+ if (response.twoFactorRedirect) {
148
+ navigate(
149
+ `${basePath}/${viewPaths.TWO_FACTOR}${window.location.search}`
150
+ )
151
+ } else {
152
+ await onSuccess()
153
+ }
154
+ } catch (error) {
155
+ form.resetField("password")
156
+ resetCaptcha()
157
+
158
+ const errorMessage = getLocalizedError({
159
+ error,
160
+ localization,
161
+ localizeErrors
162
+ })
163
+
164
+ // Set inline error for display
165
+ form.setError("root", { message: errorMessage })
166
+
167
+ // Also call toast for users who provide custom toast handler
168
+ toast({
169
+ variant: "error",
170
+ message: errorMessage
171
+ })
172
+
173
+ if (
174
+ emailVerification?.otp &&
175
+ (error as { error?: { code?: string; message?: string } })
176
+ ?.error?.code === "EMAIL_NOT_VERIFIED"
177
+ ) {
178
+ navigate(
179
+ `${basePath}/${
180
+ viewPaths.EMAIL_VERIFICATION
181
+ }?email=${encodeURIComponent(email)}`
182
+ )
183
+ }
184
+ }
185
+ }
186
+
187
+ return (
188
+ <Form {...form}>
189
+ <form
190
+ onSubmit={form.handleSubmit(signIn)}
191
+ noValidate={isHydrated}
192
+ className={cn("grid w-full gap-6", className, classNames?.base)}
193
+ >
194
+ <FormField
195
+ control={form.control}
196
+ name="email"
197
+ render={({ field }) => (
198
+ <FormItem>
199
+ <FormLabel className={classNames?.label}>
200
+ {usernameEnabled
201
+ ? localization.USERNAME
202
+ : localization.EMAIL}
203
+ </FormLabel>
204
+
205
+ <FormControl>
206
+ <Input
207
+ autoComplete={
208
+ usernameEnabled ? "username" : "email"
209
+ }
210
+ className={classNames?.input}
211
+ type={usernameEnabled ? "text" : "email"}
212
+ placeholder={
213
+ usernameEnabled
214
+ ? localization.SIGN_IN_USERNAME_PLACEHOLDER
215
+ : localization.EMAIL_PLACEHOLDER
216
+ }
217
+ disabled={isSubmitting}
218
+ {...field}
219
+ />
220
+ </FormControl>
221
+
222
+ <FormMessage className={classNames?.error} />
223
+ </FormItem>
224
+ )}
225
+ />
226
+
227
+ <FormField
228
+ control={form.control}
229
+ name="password"
230
+ render={({ field }) => (
231
+ <FormItem>
232
+ <div className="flex items-center justify-between">
233
+ <FormLabel className={classNames?.label}>
234
+ {localization.PASSWORD}
235
+ </FormLabel>
236
+
237
+ {credentials?.forgotPassword && (
238
+ <Link
239
+ className={cn(
240
+ "text-sm hover:underline",
241
+ classNames?.forgotPasswordLink
242
+ )}
243
+ href={`${basePath}/${viewPaths.FORGOT_PASSWORD}${
244
+ isHydrated
245
+ ? window.location.search
246
+ : ""
247
+ }`}
248
+ >
249
+ {localization.FORGOT_PASSWORD_LINK}
250
+ </Link>
251
+ )}
252
+ </div>
253
+
254
+ <FormControl>
255
+ <PasswordInput
256
+ autoComplete="current-password"
257
+ className={classNames?.input}
258
+ placeholder={
259
+ localization.PASSWORD_PLACEHOLDER
260
+ }
261
+ disabled={isSubmitting}
262
+ {...field}
263
+ />
264
+ </FormControl>
265
+
266
+ <FormMessage className={classNames?.error} />
267
+ </FormItem>
268
+ )}
269
+ />
270
+
271
+ {rememberMeEnabled && (
272
+ <FormField
273
+ control={form.control}
274
+ name="rememberMe"
275
+ render={({ field }) => (
276
+ <FormItem className="flex">
277
+ <FormControl>
278
+ <Checkbox
279
+ checked={field.value}
280
+ onCheckedChange={field.onChange}
281
+ disabled={isSubmitting}
282
+ />
283
+ </FormControl>
284
+
285
+ <FormLabel>
286
+ {localization.REMEMBER_ME}
287
+ </FormLabel>
288
+ </FormItem>
289
+ )}
290
+ />
291
+ )}
292
+
293
+ <Captcha
294
+ ref={captchaRef}
295
+ localization={localization}
296
+ action="/sign-in/email"
297
+ />
298
+
299
+ {form.formState.errors.root && (
300
+ <div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
301
+ {form.formState.errors.root.message}
302
+ </div>
303
+ )}
304
+
305
+ <Button
306
+ type="submit"
307
+ disabled={isSubmitting}
308
+ className={cn(
309
+ "w-full",
310
+ classNames?.button,
311
+ classNames?.primaryButton
312
+ )}
313
+ >
314
+ {isSubmitting ? (
315
+ <Loader2 className="animate-spin" />
316
+ ) : (
317
+ localization.SIGN_IN_ACTION
318
+ )}
319
+ </Button>
320
+ </form>
321
+ </Form>
322
+ )
323
+ }