@better-auth-ui/heroui 1.6.3 → 1.6.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 (204) hide show
  1. package/dist/components/auth/additional-field.d.ts +4 -0
  2. package/dist/components/auth/additional-field.js +270 -0
  3. package/dist/components/auth/auth-provider.d.ts +4 -6
  4. package/dist/components/auth/auth.d.ts +1 -1
  5. package/dist/components/auth/auth.js +54 -28
  6. package/dist/components/{settings/security → auth/delete-user}/danger-zone.d.ts +1 -1
  7. package/dist/components/{settings/security → auth/delete-user}/danger-zone.js +1 -1
  8. package/dist/components/auth/delete-user/delete-user.js +95 -0
  9. package/dist/components/{email → auth/email}/email-changed-email.d.ts +2 -2
  10. package/dist/components/{email → auth/email}/email-changed-email.js +7 -7
  11. package/dist/components/{email → auth/email}/email-verification-email.d.ts +2 -2
  12. package/dist/components/{email → auth/email}/email-verification-email.js +7 -7
  13. package/dist/components/auth/email/magic-link-email.d.ts +3 -0
  14. package/dist/components/{email → auth/email}/magic-link-email.js +7 -7
  15. package/dist/components/auth/email/new-device-email.d.ts +3 -0
  16. package/dist/components/{email → auth/email}/new-device-email.js +7 -7
  17. package/dist/components/auth/email/otp-email.d.ts +3 -0
  18. package/dist/components/{email → auth/email}/otp-email.js +6 -6
  19. package/dist/components/{email → auth/email}/password-changed-email.d.ts +2 -2
  20. package/dist/components/{email → auth/email}/password-changed-email.js +7 -7
  21. package/dist/components/{email → auth/email}/reset-password-email.d.ts +2 -2
  22. package/dist/components/{email → auth/email}/reset-password-email.js +7 -7
  23. package/dist/components/auth/forgot-password.d.ts +1 -1
  24. package/dist/components/auth/forgot-password.js +70 -48
  25. package/dist/components/auth/magic-link/magic-link-button.d.ts +11 -0
  26. package/dist/components/auth/magic-link/magic-link-button.js +20 -0
  27. package/dist/components/auth/magic-link/magic-link.d.ts +16 -0
  28. package/dist/components/auth/magic-link/magic-link.js +87 -0
  29. package/dist/components/{settings/account → auth/multi-session}/manage-account.d.ts +2 -6
  30. package/dist/components/auth/multi-session/manage-account.js +49 -0
  31. package/dist/components/auth/multi-session/manage-accounts.js +34 -0
  32. package/dist/components/{user/switch-account-menu.d.ts → auth/multi-session/switch-account-submenu-content.d.ts} +3 -3
  33. package/dist/components/auth/multi-session/switch-account-submenu-content.js +29 -0
  34. package/dist/components/{user/switch-account-item.d.ts → auth/multi-session/switch-account-submenu-item.d.ts} +2 -2
  35. package/dist/components/auth/multi-session/switch-account-submenu-item.js +20 -0
  36. package/dist/components/auth/multi-session/switch-account-submenu.d.ts +13 -0
  37. package/dist/components/auth/multi-session/switch-account-submenu.js +24 -0
  38. package/dist/components/auth/passkey/passkey-button.d.ts +6 -0
  39. package/dist/components/auth/passkey/passkey-button.js +24 -0
  40. package/dist/components/auth/passkey/passkey.js +45 -0
  41. package/dist/components/auth/passkey/passkeys.js +106 -0
  42. package/dist/components/auth/provider-button.d.ts +6 -8
  43. package/dist/components/auth/provider-button.js +16 -20
  44. package/dist/components/auth/provider-buttons.d.ts +3 -4
  45. package/dist/components/auth/provider-buttons.js +9 -10
  46. package/dist/components/auth/reset-password.js +110 -107
  47. package/dist/components/{settings → auth/settings}/account/account-settings.d.ts +5 -2
  48. package/dist/components/auth/settings/account/account-settings.js +20 -0
  49. package/dist/components/auth/settings/account/change-avatar.js +89 -0
  50. package/dist/components/{settings → auth/settings}/account/change-email.js +17 -17
  51. package/dist/components/{settings → auth/settings}/account/user-profile.d.ts +2 -3
  52. package/dist/components/auth/settings/account/user-profile.js +86 -0
  53. package/dist/components/auth/settings/security/active-session.js +59 -0
  54. package/dist/components/{settings → auth/settings}/security/active-sessions.js +4 -4
  55. package/dist/components/auth/settings/security/change-password.js +189 -0
  56. package/dist/components/auth/settings/security/linked-account.js +56 -0
  57. package/dist/components/{settings → auth/settings}/security/linked-accounts.js +5 -5
  58. package/dist/components/{settings → auth/settings}/security/security-settings.d.ts +1 -1
  59. package/dist/components/auth/settings/security/security-settings.js +22 -0
  60. package/dist/components/auth/settings/settings.js +45 -0
  61. package/dist/components/auth/sign-in.js +66 -85
  62. package/dist/components/auth/sign-out.js +11 -11
  63. package/dist/components/auth/sign-up.js +128 -148
  64. package/dist/components/{settings/account → auth/theme}/appearance.d.ts +3 -6
  65. package/dist/components/auth/theme/appearance.js +72 -0
  66. package/dist/components/auth/theme/theme-toggle-item.d.ts +5 -0
  67. package/dist/components/auth/theme/theme-toggle-item.js +58 -0
  68. package/dist/components/auth/user/user-avatar.js +29 -0
  69. package/dist/components/{user → auth/user}/user-button.d.ts +2 -6
  70. package/dist/components/auth/user/user-button.js +55 -0
  71. package/dist/components/auth/user/user-view.js +37 -0
  72. package/dist/components/auth/username/sign-in-username.d.ts +14 -0
  73. package/dist/components/auth/username/sign-in-username.js +148 -0
  74. package/dist/components/auth/username/username-field.d.ts +8 -0
  75. package/dist/components/auth/username/username-field.js +53 -0
  76. package/dist/email.d.ts +7 -0
  77. package/dist/email.js +9 -0
  78. package/dist/index.d.ts +12 -27
  79. package/dist/index.js +20 -35
  80. package/dist/lib/auth/auth-plugin.d.ts +44 -0
  81. package/dist/lib/auth/delete-user-plugin.d.ts +17 -0
  82. package/dist/lib/auth/delete-user-plugin.js +10 -0
  83. package/dist/lib/auth/magic-link-plugin.d.ts +31 -0
  84. package/dist/lib/auth/magic-link-plugin.js +13 -0
  85. package/dist/lib/auth/multi-session-plugin.d.ts +18 -0
  86. package/dist/lib/auth/multi-session-plugin.js +12 -0
  87. package/dist/lib/auth/passkey-plugin.d.ts +20 -0
  88. package/dist/lib/auth/passkey-plugin.js +12 -0
  89. package/dist/lib/auth/theme-plugin.d.ts +68 -0
  90. package/dist/lib/auth/theme-plugin.js +23 -0
  91. package/dist/lib/auth/username-plugin.d.ts +63 -0
  92. package/dist/lib/auth/username-plugin.js +18 -0
  93. package/dist/plugins.d.ts +20 -0
  94. package/dist/plugins.js +21 -0
  95. package/package.json +19 -21
  96. package/src/components/auth/additional-field.tsx +554 -0
  97. package/src/components/auth/auth-provider.tsx +5 -6
  98. package/src/components/auth/auth.tsx +99 -35
  99. package/src/components/{settings/security → auth/delete-user}/danger-zone.tsx +1 -1
  100. package/src/components/{settings/security → auth/delete-user}/delete-user.tsx +30 -20
  101. package/src/components/{email → auth/email}/email-changed-email.tsx +2 -2
  102. package/src/components/{email → auth/email}/email-verification-email.tsx +2 -2
  103. package/src/components/{email → auth/email}/magic-link-email.tsx +2 -2
  104. package/src/components/{email → auth/email}/new-device-email.tsx +2 -2
  105. package/src/components/{email → auth/email}/otp-email.tsx +2 -2
  106. package/src/components/{email → auth/email}/password-changed-email.tsx +2 -2
  107. package/src/components/{email → auth/email}/reset-password-email.tsx +2 -2
  108. package/src/components/auth/forgot-password.tsx +42 -17
  109. package/src/components/auth/magic-link/magic-link-button.tsx +61 -0
  110. package/src/components/auth/{magic-link.tsx → magic-link/magic-link.tsx} +55 -40
  111. package/src/components/{settings/account → auth/multi-session}/manage-account.tsx +16 -13
  112. package/src/components/{settings/account → auth/multi-session}/manage-accounts.tsx +14 -5
  113. package/src/components/{user/switch-account-menu.tsx → auth/multi-session/switch-account-submenu-content.tsx} +18 -11
  114. package/src/components/{user/switch-account-item.tsx → auth/multi-session/switch-account-submenu-item.tsx} +17 -5
  115. package/src/components/auth/multi-session/switch-account-submenu.tsx +50 -0
  116. package/src/components/auth/passkey/passkey-button.tsx +56 -0
  117. package/src/components/{settings/security → auth/passkey}/passkey.tsx +20 -6
  118. package/src/components/auth/passkey/passkeys.tsx +196 -0
  119. package/src/components/auth/provider-button.tsx +27 -29
  120. package/src/components/auth/provider-buttons.tsx +6 -11
  121. package/src/components/auth/reset-password.tsx +13 -7
  122. package/src/components/{settings → auth/settings}/account/account-settings.tsx +15 -13
  123. package/src/components/{settings → auth/settings}/account/change-avatar.tsx +4 -3
  124. package/src/components/{settings → auth/settings}/account/change-email.tsx +3 -3
  125. package/src/components/auth/settings/account/user-profile.tsx +171 -0
  126. package/src/components/{settings → auth/settings}/security/active-session.tsx +8 -5
  127. package/src/components/{settings → auth/settings}/security/active-sessions.tsx +3 -3
  128. package/src/components/{settings → auth/settings}/security/change-password.tsx +14 -9
  129. package/src/components/{settings → auth/settings}/security/linked-account.tsx +12 -8
  130. package/src/components/{settings → auth/settings}/security/linked-accounts.tsx +2 -2
  131. package/src/components/{settings → auth/settings}/security/security-settings.tsx +7 -6
  132. package/src/components/{settings → auth/settings}/settings.tsx +2 -2
  133. package/src/components/auth/sign-in.tsx +50 -61
  134. package/src/components/auth/sign-out.tsx +2 -2
  135. package/src/components/auth/sign-up.tsx +119 -131
  136. package/src/components/{settings/account → auth/theme}/appearance.tsx +17 -24
  137. package/src/components/auth/theme/theme-toggle-item.tsx +110 -0
  138. package/src/components/{user → auth/user}/user-avatar.tsx +12 -4
  139. package/src/components/{user → auth/user}/user-button.tsx +15 -88
  140. package/src/components/{user → auth/user}/user-view.tsx +12 -4
  141. package/src/components/auth/username/sign-in-username.tsx +299 -0
  142. package/src/components/auth/username/username-field.tsx +124 -0
  143. package/src/email.ts +9 -0
  144. package/src/index.tsx +12 -27
  145. package/src/lib/auth/auth-plugin.ts +67 -0
  146. package/src/lib/auth/delete-user-plugin.ts +15 -0
  147. package/src/lib/auth/magic-link-plugin.ts +26 -0
  148. package/src/lib/auth/multi-session-plugin.ts +17 -0
  149. package/src/lib/auth/passkey-plugin.ts +17 -0
  150. package/src/lib/auth/theme-plugin.ts +86 -0
  151. package/src/lib/auth/username-plugin.tsx +30 -0
  152. package/src/plugins.ts +23 -0
  153. package/dist/components/auth/magic-link-button.d.ts +0 -13
  154. package/dist/components/auth/magic-link-button.js +0 -16
  155. package/dist/components/auth/magic-link.d.ts +0 -17
  156. package/dist/components/auth/magic-link.js +0 -99
  157. package/dist/components/auth/passkey-button.d.ts +0 -4
  158. package/dist/components/auth/passkey-button.js +0 -21
  159. package/dist/components/email/magic-link-email.d.ts +0 -3
  160. package/dist/components/email/new-device-email.d.ts +0 -3
  161. package/dist/components/email/otp-email.d.ts +0 -3
  162. package/dist/components/settings/account/account-settings.js +0 -23
  163. package/dist/components/settings/account/appearance.js +0 -71
  164. package/dist/components/settings/account/change-avatar.js +0 -89
  165. package/dist/components/settings/account/manage-account.js +0 -48
  166. package/dist/components/settings/account/manage-accounts.js +0 -33
  167. package/dist/components/settings/account/user-profile.js +0 -103
  168. package/dist/components/settings/security/active-session.js +0 -59
  169. package/dist/components/settings/security/change-password.js +0 -189
  170. package/dist/components/settings/security/delete-user.js +0 -92
  171. package/dist/components/settings/security/linked-account.js +0 -56
  172. package/dist/components/settings/security/passkey.js +0 -43
  173. package/dist/components/settings/security/passkeys.js +0 -53
  174. package/dist/components/settings/security/security-settings.js +0 -25
  175. package/dist/components/settings/settings.js +0 -45
  176. package/dist/components/user/switch-account-item.js +0 -20
  177. package/dist/components/user/switch-account-menu.js +0 -28
  178. package/dist/components/user/user-avatar.js +0 -29
  179. package/dist/components/user/user-button.js +0 -91
  180. package/dist/components/user/user-view.js +0 -37
  181. package/dist/core.d.ts +0 -1
  182. package/dist/core.js +0 -1
  183. package/dist/react-exports.d.ts +0 -1
  184. package/dist/react-exports.js +0 -1
  185. package/src/components/auth/magic-link-button.tsx +0 -43
  186. package/src/components/auth/passkey-button.tsx +0 -35
  187. package/src/components/settings/account/user-profile.tsx +0 -217
  188. package/src/components/settings/security/passkeys.tsx +0 -96
  189. package/src/core.ts +0 -1
  190. package/src/react-exports.ts +0 -1
  191. /package/dist/components/{settings/security → auth/delete-user}/delete-user.d.ts +0 -0
  192. /package/dist/components/{settings/account → auth/multi-session}/manage-accounts.d.ts +0 -0
  193. /package/dist/components/{settings/security → auth/passkey}/passkey.d.ts +0 -0
  194. /package/dist/components/{settings/security → auth/passkey}/passkeys.d.ts +0 -0
  195. /package/dist/components/{settings → auth/settings}/account/change-avatar.d.ts +0 -0
  196. /package/dist/components/{settings → auth/settings}/account/change-email.d.ts +0 -0
  197. /package/dist/components/{settings → auth/settings}/security/active-session.d.ts +0 -0
  198. /package/dist/components/{settings → auth/settings}/security/active-sessions.d.ts +0 -0
  199. /package/dist/components/{settings → auth/settings}/security/change-password.d.ts +0 -0
  200. /package/dist/components/{settings → auth/settings}/security/linked-account.d.ts +0 -0
  201. /package/dist/components/{settings → auth/settings}/security/linked-accounts.d.ts +0 -0
  202. /package/dist/components/{settings → auth/settings}/settings.d.ts +0 -0
  203. /package/dist/components/{user → auth/user}/user-avatar.d.ts +0 -0
  204. /package/dist/components/{user → auth/user}/user-view.d.ts +0 -0
@@ -1,9 +1,9 @@
1
1
  import {
2
- useAuth,
3
- useIsUsernameAvailable,
4
- useSignUpEmail
5
- } from "@better-auth-ui/react"
6
- import { Check, Eye, EyeSlash, Xmark } from "@gravity-ui/icons"
2
+ authMutationKeys,
3
+ parseAdditionalFieldValue
4
+ } from "@better-auth-ui/core"
5
+ import { useAuth, useFetchOptions, useSignUpEmail } from "@better-auth-ui/react"
6
+ import { Eye, EyeSlash } from "@gravity-ui/icons"
7
7
  import {
8
8
  Button,
9
9
  Card,
@@ -20,11 +20,10 @@ import {
20
20
  TextField,
21
21
  toast
22
22
  } from "@heroui/react"
23
- import { useDebouncer } from "@tanstack/react-pacer"
23
+ import { useIsMutating } from "@tanstack/react-query"
24
24
  import { type SyntheticEvent, useState } from "react"
25
-
25
+ import { AdditionalField } from "./additional-field"
26
26
  import { FieldSeparator } from "./field-separator"
27
- import { MagicLinkButton } from "./magic-link-button"
28
27
  import { ProviderButtons, type SocialLayout } from "./provider-buttons"
29
28
 
30
29
  export type SignUpProps = {
@@ -52,76 +51,65 @@ export function SignUp({
52
51
  ...props
53
52
  }: SignUpProps & Omit<CardProps, "children">) {
54
53
  const {
54
+ additionalFields,
55
+ authClient,
55
56
  basePaths,
56
57
  emailAndPassword,
57
58
  localization,
58
- magicLink,
59
+ plugins,
59
60
  redirectTo,
60
61
  socialProviders,
61
- username: usernameConfig,
62
62
  viewPaths,
63
63
  navigate
64
64
  } = useAuth()
65
65
 
66
+ const { fetchOptions, resetFetchOptions } = useFetchOptions()
67
+
66
68
  const [password, setPassword] = useState("")
67
69
  const [confirmPassword, setConfirmPassword] = useState("")
68
- const [username, setUsername] = useState("")
69
-
70
- const {
71
- mutate: isUsernameAvailable,
72
- data: usernameData,
73
- error: usernameError,
74
- reset: resetUsername
75
- } = useIsUsernameAvailable()
76
-
77
- const usernameDebouncer = useDebouncer(
78
- (value: string) => {
79
- if (!value.trim()) {
80
- resetUsername()
81
- return
82
- }
83
-
84
- isUsernameAvailable({ username: value.trim() })
85
- },
86
- { wait: 500 }
87
- )
88
70
 
89
- function handleUsernameChange(value: string) {
90
- setUsername(value)
91
- resetUsername()
92
-
93
- if (usernameConfig?.isUsernameAvailable) {
94
- usernameDebouncer.maybeExecute(value)
95
- }
96
- }
97
-
98
- const { mutate: signUpEmail, isPending: signUpPending } = useSignUpEmail({
99
- onError: (error) => {
100
- setPassword("")
101
- setConfirmPassword("")
102
- toast.danger(error.error?.message || error.message)
103
- },
104
- onSuccess: () => {
105
- if (emailAndPassword?.requireEmailVerification) {
106
- toast.success(localization.auth.verifyYourEmail)
107
- navigate({ to: `${basePaths.auth}/${viewPaths.auth.signIn}` })
108
- } else {
109
- navigate({ to: redirectTo })
71
+ const { mutate: signUpEmail, isPending: signUpEmailPending } = useSignUpEmail(
72
+ authClient,
73
+ {
74
+ onError: (error) => {
75
+ setPassword("")
76
+ setConfirmPassword("")
77
+ toast.danger(error.error?.message || error.message)
78
+ resetFetchOptions()
79
+ },
80
+ onSuccess: () => {
81
+ if (emailAndPassword?.requireEmailVerification) {
82
+ toast.success(localization.auth.verifyYourEmail)
83
+ navigate({ to: `${basePaths.auth}/${viewPaths.auth.signIn}` })
84
+ } else {
85
+ navigate({ to: redirectTo })
86
+ }
110
87
  }
111
88
  }
112
- })
89
+ )
113
90
 
114
91
  const [isPasswordVisible, setIsPasswordVisible] = useState(false)
115
92
  const [isConfirmPasswordVisible, setIsConfirmPasswordVisible] =
116
93
  useState(false)
117
94
 
118
- const isPending = signUpPending
95
+ const signInMutating = useIsMutating({
96
+ mutationKey: authMutationKeys.signIn.all
97
+ })
98
+ const signUpMutating = useIsMutating({
99
+ mutationKey: authMutationKeys.signUp.all
100
+ })
101
+ const isPending = signInMutating + signUpMutating > 0
102
+
103
+ const Captcha = plugins.find(
104
+ (plugin) => plugin.captchaComponent
105
+ )?.captchaComponent
119
106
 
120
- const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
107
+ const handleSubmit = async (e: SyntheticEvent<HTMLFormElement>) => {
121
108
  e.preventDefault()
122
109
 
123
110
  const formData = new FormData(e.currentTarget)
124
- const name = formData.get("name") as string
111
+ // `emailAndPassword.name === false` hides the name field and submits "".
112
+ const name = (formData.get("name") as string | null) ?? ""
125
113
  const email = formData.get("email") as string
126
114
 
127
115
  if (emailAndPassword?.confirmPassword && password !== confirmPassword) {
@@ -131,18 +119,35 @@ export function SignUp({
131
119
  return
132
120
  }
133
121
 
122
+ const additionalFieldValues: Record<string, unknown> = {}
123
+
124
+ for (const field of additionalFields ?? []) {
125
+ if (!field.signUp || field.readOnly) continue
126
+ const value = parseAdditionalFieldValue(
127
+ field,
128
+ formData.get(field.name) as string | null
129
+ )
130
+
131
+ if (field.validate) {
132
+ try {
133
+ await field.validate(value)
134
+ } catch (error) {
135
+ toast.danger(error instanceof Error ? error.message : String(error))
136
+ return
137
+ }
138
+ }
139
+
140
+ if (value !== undefined) {
141
+ additionalFieldValues[field.name] = value
142
+ }
143
+ }
144
+
134
145
  signUpEmail({
135
146
  name,
136
147
  email,
137
148
  password,
138
- ...(usernameConfig?.enabled
139
- ? {
140
- username: username.trim(),
141
- ...(usernameConfig.displayUsername
142
- ? { displayUsername: username.trim() }
143
- : {})
144
- }
145
- : {})
149
+ ...additionalFieldValues,
150
+ fetchOptions
146
151
  })
147
152
  }
148
153
 
@@ -150,7 +155,7 @@ export function SignUp({
150
155
 
151
156
  return (
152
157
  <Card
153
- className={cn("w-full max-w-sm p-4 md:p-6", className)}
158
+ className={cn("w-full max-w-sm gap-4 md:p-6", className)}
154
159
  variant={variant}
155
160
  {...props}
156
161
  >
@@ -164,10 +169,7 @@ export function SignUp({
164
169
  {socialPosition === "top" && (
165
170
  <>
166
171
  {!!socialProviders?.length && (
167
- <ProviderButtons
168
- isPending={isPending}
169
- socialLayout={socialLayout}
170
- />
172
+ <ProviderButtons socialLayout={socialLayout} />
171
173
  )}
172
174
 
173
175
  {showSeparator && (
@@ -178,67 +180,22 @@ export function SignUp({
178
180
 
179
181
  {emailAndPassword?.enabled && (
180
182
  <Form onSubmit={handleSubmit} className="flex flex-col gap-4">
181
- <TextField
182
- name="name"
183
- type="text"
184
- autoComplete="name"
185
- isDisabled={isPending}
186
- >
187
- <Label>{localization.auth.name}</Label>
188
-
189
- <Input
190
- placeholder={localization.auth.namePlaceholder}
191
- required
192
- variant={variant === "transparent" ? "primary" : "secondary"}
193
- />
194
-
195
- <FieldError />
196
- </TextField>
197
-
198
- {usernameConfig?.enabled && (
183
+ {emailAndPassword.name !== false && (
199
184
  <TextField
200
- name="username"
185
+ name="name"
201
186
  type="text"
202
- autoComplete="username"
203
- minLength={usernameConfig.minUsernameLength}
204
- maxLength={usernameConfig.maxUsernameLength}
187
+ autoComplete="name"
205
188
  isDisabled={isPending}
206
- value={username}
207
- onChange={handleUsernameChange}
208
- isInvalid={
209
- !!usernameError || (usernameData && !usernameData.available)
210
- }
211
189
  >
212
- <Label>{localization.auth.username}</Label>
190
+ <Label>{localization.auth.name}</Label>
213
191
 
214
- <InputGroup
192
+ <Input
193
+ placeholder={localization.auth.namePlaceholder}
194
+ required
215
195
  variant={variant === "transparent" ? "primary" : "secondary"}
216
- >
217
- <InputGroup.Input
218
- placeholder={localization.auth.usernamePlaceholder}
219
- required
220
- />
221
-
222
- {usernameConfig.isUsernameAvailable && username.trim() && (
223
- <InputGroup.Suffix className="px-2">
224
- {usernameData?.available ? (
225
- <Check className="text-success" />
226
- ) : usernameError || usernameData?.available === false ? (
227
- <Xmark className="text-danger" />
228
- ) : (
229
- <Spinner size="sm" color="current" />
230
- )}
231
- </InputGroup.Suffix>
232
- )}
233
- </InputGroup>
196
+ />
234
197
 
235
- <FieldError>
236
- {usernameError?.error?.message ||
237
- usernameError?.message ||
238
- (usernameData?.available === false
239
- ? localization.auth.usernameTaken
240
- : null)}
241
- </FieldError>
198
+ <FieldError />
242
199
  </TextField>
243
200
  )}
244
201
 
@@ -259,6 +216,19 @@ export function SignUp({
259
216
  <FieldError />
260
217
  </TextField>
261
218
 
219
+ {additionalFields?.map(
220
+ (field) =>
221
+ field.signUp === "above" && (
222
+ <AdditionalField
223
+ key={field.name}
224
+ name={field.name}
225
+ field={field}
226
+ isPending={isPending}
227
+ variant={variant}
228
+ />
229
+ )
230
+ )}
231
+
262
232
  <TextField
263
233
  minLength={emailAndPassword?.minPasswordLength}
264
234
  maxLength={emailAndPassword?.maxPasswordLength}
@@ -347,15 +317,36 @@ export function SignUp({
347
317
  </TextField>
348
318
  )}
349
319
 
320
+ {additionalFields?.map(
321
+ (field) =>
322
+ field.signUp &&
323
+ field.signUp !== "above" && (
324
+ <AdditionalField
325
+ key={field.name}
326
+ name={field.name}
327
+ field={field}
328
+ isPending={isPending}
329
+ variant={variant}
330
+ />
331
+ )
332
+ )}
333
+
334
+ {Captcha && <div className="flex justify-center">{Captcha}</div>}
335
+
350
336
  <div className="flex flex-col gap-3">
351
337
  <Button type="submit" className="w-full" isPending={isPending}>
352
- {isPending && <Spinner color="current" size="sm" />}
338
+ {signUpEmailPending && <Spinner color="current" size="sm" />}
353
339
 
354
340
  {localization.auth.signUp}
355
341
  </Button>
356
342
 
357
- {magicLink && (
358
- <MagicLinkButton view="signUp" isPending={isPending} />
343
+ {plugins.flatMap((plugin) =>
344
+ plugin.authButtons?.map((AuthButton, index) => (
345
+ <AuthButton
346
+ key={`${plugin.id}-${index.toString()}`}
347
+ view="signUp"
348
+ />
349
+ ))
359
350
  )}
360
351
  </div>
361
352
  </Form>
@@ -368,21 +359,18 @@ export function SignUp({
368
359
  )}
369
360
 
370
361
  {!!socialProviders?.length && (
371
- <ProviderButtons
372
- socialLayout={socialLayout}
373
- isPending={isPending}
374
- />
362
+ <ProviderButtons socialLayout={socialLayout} />
375
363
  )}
376
364
  </>
377
365
  )}
378
366
  </Card.Content>
379
367
 
380
- <Card.Footer className="flex-col">
368
+ <Card.Footer className="flex-col gap-3">
381
369
  <Description className="text-sm">
382
370
  {localization.auth.alreadyHaveAnAccount}{" "}
383
371
  <Link
384
372
  href={`${basePaths.auth}/${viewPaths.auth.signIn}`}
385
- className="text-accent decoration-accent no-underline hover:underline"
373
+ className="text-accent no-underline hover:underline decoration-accent-hover"
386
374
  >
387
375
  {localization.auth.signIn}
388
376
  </Link>
@@ -2,8 +2,7 @@ import {
2
2
  ThemePreviewDark,
3
3
  ThemePreviewLight,
4
4
  ThemePreviewSystem,
5
- useAuth,
6
- useSession
5
+ useAuthPlugin
7
6
  } from "@better-auth-ui/react"
8
7
  import { Display, Moon, Sun } from "@gravity-ui/icons"
9
8
  import {
@@ -12,9 +11,12 @@ import {
12
11
  cn,
13
12
  Label,
14
13
  Radio,
15
- RadioGroup
14
+ RadioGroup,
15
+ useIsHydrated
16
16
  } from "@heroui/react"
17
17
 
18
+ import { themePlugin } from "../../../lib/auth/theme-plugin"
19
+
18
20
  export type AppearanceProps = {
19
21
  className?: string
20
22
  variant?: CardProps["variant"]
@@ -24,42 +26,33 @@ export type AppearanceProps = {
24
26
  * Renders a theme selector card with visual theme previews.
25
27
  *
26
28
  * Displays a card containing radio buttons for selecting between system, light,
27
- * and dark themes. Each option shows a visual preview of the theme. Only renders
28
- * if theme settings are configured (theme, setTheme, and themes are provided).
29
- *
30
- * @param className - Optional additional CSS class names for the card container.
31
- * @returns A JSX element containing the theme selector card, or null if theme settings are not configured.
29
+ * and dark themes. Each option shows a visual preview of the theme. Theme options
30
+ * are conditionally shown based on the `themes` array provided via the theme plugin.
32
31
  */
33
32
  export function Appearance({
34
33
  className,
35
34
  variant,
36
35
  ...props
37
36
  }: AppearanceProps & Omit<CardProps, "children">) {
38
- const {
39
- localization,
40
- appearance: { theme, setTheme, themes }
41
- } = useAuth()
42
- const { data: session } = useSession()
43
-
44
- if (!setTheme || !themes?.length) {
45
- return null
46
- }
37
+ const { useTheme, localization } = useAuthPlugin(themePlugin)
38
+ const { theme, setTheme, themes = [] } = useTheme()
39
+ const hydrated = useIsHydrated()
47
40
 
48
41
  return (
49
42
  <div>
50
43
  <h2 className={cn("text-sm font-semibold mb-3")}>
51
- {localization.settings.appearance}
44
+ {localization.appearance}
52
45
  </h2>
53
46
 
54
47
  <Card className={cn("p-4 gap-4", className)} variant={variant} {...props}>
55
48
  <Card.Content>
56
- <Label>{localization.settings.theme}</Label>
49
+ <Label>{localization.theme}</Label>
57
50
 
58
51
  <RadioGroup
59
52
  variant={variant === "transparent" ? "secondary" : "primary"}
60
- value={session ? theme : ""}
53
+ value={hydrated ? theme : ""}
61
54
  onChange={setTheme}
62
- isDisabled={!session || !theme}
55
+ isDisabled={!hydrated || !theme}
63
56
  >
64
57
  <div className="grid gap-3 grid-cols-2 sm:grid-cols-3">
65
58
  {themes.includes("system") && (
@@ -78,7 +71,7 @@ export function Appearance({
78
71
  <div className="flex gap-2 justify-between">
79
72
  <Label className="flex gap-2 items-center">
80
73
  <Display className="text-muted" />
81
- {localization.settings.system}
74
+ {localization.system}
82
75
  </Label>
83
76
 
84
77
  <Radio.Control>
@@ -107,7 +100,7 @@ export function Appearance({
107
100
  <div className="flex gap-2 justify-between">
108
101
  <Label className="flex gap-2 items-center">
109
102
  <Sun className="text-muted" />
110
- {localization.settings.light}
103
+ {localization.light}
111
104
  </Label>
112
105
 
113
106
  <Radio.Control>
@@ -136,7 +129,7 @@ export function Appearance({
136
129
  <div className="flex gap-2 justify-between">
137
130
  <Label className="flex gap-2 items-center">
138
131
  <Moon className="text-muted" />
139
- {localization.settings.dark}
132
+ {localization.dark}
140
133
  </Label>
141
134
 
142
135
  <Radio.Control>
@@ -0,0 +1,110 @@
1
+ import { useAuthPlugin } from "@better-auth-ui/react"
2
+ import { Display, Moon, Sun } from "@gravity-ui/icons"
3
+ import { Dropdown, Label, Tabs } from "@heroui/react"
4
+
5
+ import { themePlugin } from "../../../lib/auth/theme-plugin"
6
+
7
+ const ACTIVE_TAB_SELECTOR = '[role="tab"][aria-selected="true"]'
8
+
9
+ /**
10
+ * Theme toggle dropdown item used inside `UserButton`. Callers are responsible
11
+ * for ensuring theming is configured before rendering this component.
12
+ */
13
+ export function ThemeToggleItem() {
14
+ const { useTheme, localization } = useAuthPlugin(themePlugin)
15
+ const { theme, setTheme, themes = [] } = useTheme()
16
+
17
+ // The Tabs aren't part of the menu's keyboard-navigation list, so arrow-key
18
+ // nav can't reach them on its own. When the wrapper menu item receives focus
19
+ // we delegate focus to the active Tab so the user can switch themes with the
20
+ // Left/Right arrows. React's synthetic `onFocus` bubbles (it's wired to
21
+ // `focusin`), so we must guard against child-originated events: without the
22
+ // `target === currentTarget` check, an arrow-key focus move between tabs
23
+ // bubbles up here and the still-stale `aria-selected="true"` lookup yanks
24
+ // focus back to the previously selected tab.
25
+ const focusActiveTab = (event: React.FocusEvent<Element>) => {
26
+ if (event.target !== event.currentTarget) return
27
+ const activeTab =
28
+ event.currentTarget.querySelector<HTMLElement>(ACTIVE_TAB_SELECTOR)
29
+ activeTab?.focus({ preventScroll: true })
30
+ }
31
+
32
+ // Up/Down on a Tab escapes back to the previous/next sibling menu item so
33
+ // users can keep navigating the menu with the arrow keys after they've
34
+ // entered the Tabs.
35
+ const handleTabsKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
36
+ if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return
37
+
38
+ const target = event.target as HTMLElement
39
+ if (target.getAttribute("role") !== "tab") return
40
+
41
+ const wrapper = target.closest<HTMLElement>('[role="menuitem"]')
42
+ const menu = wrapper?.closest<HTMLElement>('[role="menu"]')
43
+ if (!wrapper || !menu) return
44
+
45
+ const items = Array.from(
46
+ menu.querySelectorAll<HTMLElement>(
47
+ '[role="menuitem"]:not([data-disabled])'
48
+ )
49
+ )
50
+ const currentIndex = items.indexOf(wrapper)
51
+ if (currentIndex === -1) return
52
+ const nextIndex =
53
+ event.key === "ArrowDown" ? currentIndex + 1 : currentIndex - 1
54
+ const next = items[nextIndex]
55
+ if (!next) return
56
+
57
+ event.preventDefault()
58
+ next.focus()
59
+ }
60
+
61
+ return (
62
+ <Dropdown.Item
63
+ className="py-1 pe-2"
64
+ onFocus={focusActiveTab}
65
+ // Without `shouldCloseOnSelect={false}` the menu auto-closes after the
66
+ // user changes themes, because clicking on a Tab bubbles up as a
67
+ // menu-item activation.
68
+ shouldCloseOnSelect={false}
69
+ >
70
+ <Label>{localization.theme}</Label>
71
+
72
+ <Tabs
73
+ className="ml-auto"
74
+ selectedKey={theme}
75
+ onSelectionChange={(key) => setTheme(key as string)}
76
+ >
77
+ <Tabs.ListContainer onKeyDown={handleTabsKeyDown}>
78
+ <Tabs.List
79
+ aria-label={localization.theme}
80
+ className="*:h-5 *:w-5 *:p-0"
81
+ >
82
+ {themes.includes("system") && (
83
+ <Tabs.Tab id="system" aria-label={localization.system}>
84
+ <Display className="size-3" />
85
+
86
+ <Tabs.Indicator />
87
+ </Tabs.Tab>
88
+ )}
89
+
90
+ {themes.includes("light") && (
91
+ <Tabs.Tab id="light" aria-label={localization.light}>
92
+ <Sun className="size-3" />
93
+
94
+ <Tabs.Indicator />
95
+ </Tabs.Tab>
96
+ )}
97
+
98
+ {themes.includes("dark") && (
99
+ <Tabs.Tab id="dark" aria-label={localization.dark}>
100
+ <Moon className="size-3" />
101
+
102
+ <Tabs.Indicator />
103
+ </Tabs.Tab>
104
+ )}
105
+ </Tabs.List>
106
+ </Tabs.ListContainer>
107
+ </Tabs>
108
+ </Dropdown.Item>
109
+ )
110
+ }
@@ -1,4 +1,8 @@
1
- import { useSession } from "@better-auth-ui/react"
1
+ import {
2
+ type UsernameAuthClient,
3
+ useAuth,
4
+ useSession
5
+ } from "@better-auth-ui/react"
2
6
  import { Person } from "@gravity-ui/icons"
3
7
  import { Avatar, type AvatarProps, cn, Skeleton } from "@heroui/react"
4
8
  import type { User } from "better-auth"
@@ -32,9 +36,13 @@ export function UserAvatar({
32
36
  style,
33
37
  ...props
34
38
  }: UserAvatarProps & AvatarProps) {
35
- const { data: session, isPending: sessionPending } = useSession({
36
- enabled: !user && !isPending
37
- })
39
+ const { authClient } = useAuth()
40
+ const { data: session, isPending: sessionPending } = useSession(
41
+ authClient as UsernameAuthClient,
42
+ {
43
+ enabled: !user && !isPending
44
+ }
45
+ )
38
46
 
39
47
  if ((isPending || sessionPending) && !user) {
40
48
  return (