@better-auth-ui/heroui 1.6.2 → 1.6.4

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 (213) 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-provider.js +12 -11
  5. package/dist/components/auth/auth.d.ts +1 -1
  6. package/dist/components/auth/auth.js +54 -28
  7. package/dist/components/{settings/security → auth/delete-user}/danger-zone.d.ts +3 -4
  8. package/dist/components/{settings/security → auth/delete-user}/danger-zone.js +1 -1
  9. package/dist/components/{settings/security → auth/delete-user}/delete-user.d.ts +1 -1
  10. package/dist/components/auth/delete-user/delete-user.js +95 -0
  11. package/dist/components/{email → auth/email}/email-changed-email.d.ts +2 -2
  12. package/dist/components/{email → auth/email}/email-changed-email.js +7 -7
  13. package/dist/components/{email → auth/email}/email-verification-email.d.ts +2 -2
  14. package/dist/components/{email → auth/email}/email-verification-email.js +7 -7
  15. package/dist/components/auth/email/magic-link-email.d.ts +3 -0
  16. package/dist/components/{email → auth/email}/magic-link-email.js +7 -7
  17. package/dist/components/auth/email/new-device-email.d.ts +3 -0
  18. package/dist/components/{email → auth/email}/new-device-email.js +7 -7
  19. package/dist/components/auth/email/otp-email.d.ts +3 -0
  20. package/dist/components/{email → auth/email}/otp-email.js +6 -6
  21. package/dist/components/{email → auth/email}/password-changed-email.d.ts +2 -2
  22. package/dist/components/{email → auth/email}/password-changed-email.js +7 -7
  23. package/dist/components/{email → auth/email}/reset-password-email.d.ts +2 -2
  24. package/dist/components/{email → auth/email}/reset-password-email.js +7 -7
  25. package/dist/components/auth/error-toaster.d.ts +1 -0
  26. package/dist/components/auth/error-toaster.js +17 -0
  27. package/dist/components/auth/forgot-password.d.ts +2 -2
  28. package/dist/components/auth/forgot-password.js +42 -42
  29. package/dist/components/auth/magic-link/magic-link-button.d.ts +11 -0
  30. package/dist/components/auth/magic-link/magic-link-button.js +20 -0
  31. package/dist/components/auth/magic-link/magic-link.d.ts +16 -0
  32. package/dist/components/auth/magic-link/magic-link.js +87 -0
  33. package/dist/components/{settings/account → auth/multi-session}/manage-account.d.ts +2 -6
  34. package/dist/components/auth/multi-session/manage-account.js +49 -0
  35. package/dist/components/{settings/account → auth/multi-session}/manage-accounts.d.ts +1 -1
  36. package/dist/components/auth/multi-session/manage-accounts.js +34 -0
  37. package/dist/components/{user/switch-account-menu.d.ts → auth/multi-session/switch-account-submenu-content.d.ts} +3 -3
  38. package/dist/components/auth/multi-session/switch-account-submenu-content.js +29 -0
  39. package/dist/components/{user/switch-account-item.d.ts → auth/multi-session/switch-account-submenu-item.d.ts} +2 -2
  40. package/dist/components/auth/multi-session/switch-account-submenu-item.js +20 -0
  41. package/dist/components/auth/multi-session/switch-account-submenu.d.ts +13 -0
  42. package/dist/components/auth/multi-session/switch-account-submenu.js +24 -0
  43. package/dist/components/auth/passkey/passkey-button.d.ts +6 -0
  44. package/dist/components/auth/passkey/passkey-button.js +24 -0
  45. package/dist/components/auth/passkey/passkey.js +45 -0
  46. package/dist/components/{settings/security → auth/passkey}/passkeys.d.ts +1 -1
  47. package/dist/components/auth/passkey/passkeys.js +106 -0
  48. package/dist/components/auth/provider-button.d.ts +13 -0
  49. package/dist/components/auth/provider-button.js +25 -0
  50. package/dist/components/auth/provider-buttons.d.ts +3 -10
  51. package/dist/components/auth/provider-buttons.js +14 -22
  52. package/dist/components/auth/reset-password.d.ts +1 -1
  53. package/dist/components/auth/reset-password.js +110 -110
  54. package/dist/components/{settings → auth/settings}/account/account-settings.d.ts +5 -2
  55. package/dist/components/auth/settings/account/account-settings.js +20 -0
  56. package/dist/components/auth/settings/account/change-avatar.js +89 -0
  57. package/dist/components/{settings → auth/settings}/account/change-email.d.ts +1 -1
  58. package/dist/components/{settings → auth/settings}/account/change-email.js +17 -20
  59. package/dist/components/{settings → auth/settings}/account/user-profile.d.ts +3 -4
  60. package/dist/components/auth/settings/account/user-profile.js +86 -0
  61. package/dist/components/auth/settings/security/active-session.js +59 -0
  62. package/dist/components/{settings → auth/settings}/security/active-sessions.d.ts +1 -1
  63. package/dist/components/auth/settings/security/active-sessions.js +34 -0
  64. package/dist/components/{settings → auth/settings}/security/change-password.d.ts +1 -1
  65. package/dist/components/auth/settings/security/change-password.js +189 -0
  66. package/dist/components/auth/settings/security/linked-account.js +56 -0
  67. package/dist/components/{settings → auth/settings}/security/linked-accounts.d.ts +1 -1
  68. package/dist/components/auth/settings/security/linked-accounts.js +45 -0
  69. package/dist/components/{settings → auth/settings}/security/security-settings.d.ts +1 -1
  70. package/dist/components/auth/settings/security/security-settings.js +22 -0
  71. package/dist/components/auth/settings/settings.js +45 -0
  72. package/dist/components/auth/sign-in.d.ts +1 -1
  73. package/dist/components/auth/sign-in.js +64 -100
  74. package/dist/components/auth/sign-out.js +11 -11
  75. package/dist/components/auth/sign-up.d.ts +1 -1
  76. package/dist/components/auth/sign-up.js +123 -157
  77. package/dist/components/{settings/account → auth/theme}/appearance.d.ts +3 -6
  78. package/dist/components/auth/theme/appearance.js +72 -0
  79. package/dist/components/auth/theme/theme-toggle-item.d.ts +5 -0
  80. package/dist/components/auth/theme/theme-toggle-item.js +58 -0
  81. package/dist/components/auth/user/user-avatar.js +29 -0
  82. package/dist/components/{user → auth/user}/user-button.d.ts +2 -6
  83. package/dist/components/auth/user/user-button.js +55 -0
  84. package/dist/components/auth/user/user-view.js +37 -0
  85. package/dist/components/auth/username/sign-in-username.d.ts +14 -0
  86. package/dist/components/auth/username/sign-in-username.js +142 -0
  87. package/dist/components/auth/username/username-field.d.ts +8 -0
  88. package/dist/components/auth/username/username-field.js +51 -0
  89. package/dist/email.d.ts +7 -0
  90. package/dist/email.js +9 -0
  91. package/dist/index.d.ts +13 -27
  92. package/dist/index.js +22 -36
  93. package/dist/lib/auth/auth-plugin.d.ts +44 -0
  94. package/dist/lib/auth/delete-user-plugin.d.ts +17 -0
  95. package/dist/lib/auth/delete-user-plugin.js +10 -0
  96. package/dist/lib/auth/magic-link-plugin.d.ts +31 -0
  97. package/dist/lib/auth/magic-link-plugin.js +13 -0
  98. package/dist/lib/auth/multi-session-plugin.d.ts +18 -0
  99. package/dist/lib/auth/multi-session-plugin.js +12 -0
  100. package/dist/lib/auth/passkey-plugin.d.ts +20 -0
  101. package/dist/lib/auth/passkey-plugin.js +12 -0
  102. package/dist/lib/auth/theme-plugin.d.ts +68 -0
  103. package/dist/lib/auth/theme-plugin.js +23 -0
  104. package/dist/lib/auth/username-plugin.d.ts +63 -0
  105. package/dist/lib/auth/username-plugin.js +18 -0
  106. package/dist/plugins.d.ts +20 -0
  107. package/dist/plugins.js +21 -0
  108. package/package.json +22 -22
  109. package/src/components/auth/additional-field.tsx +554 -0
  110. package/src/components/auth/auth-provider.tsx +8 -5
  111. package/src/components/auth/auth.tsx +100 -36
  112. package/src/components/{settings/security → auth/delete-user}/danger-zone.tsx +3 -10
  113. package/src/components/{settings/security → auth/delete-user}/delete-user.tsx +31 -24
  114. package/src/components/{email → auth/email}/email-changed-email.tsx +2 -2
  115. package/src/components/{email → auth/email}/email-verification-email.tsx +2 -2
  116. package/src/components/{email → auth/email}/magic-link-email.tsx +2 -2
  117. package/src/components/{email → auth/email}/new-device-email.tsx +2 -2
  118. package/src/components/{email → auth/email}/otp-email.tsx +2 -2
  119. package/src/components/{email → auth/email}/password-changed-email.tsx +2 -2
  120. package/src/components/{email → auth/email}/reset-password-email.tsx +2 -2
  121. package/src/components/auth/error-toaster.tsx +25 -0
  122. package/src/components/auth/forgot-password.tsx +15 -13
  123. package/src/components/auth/magic-link/magic-link-button.tsx +61 -0
  124. package/src/components/auth/{magic-link.tsx → magic-link/magic-link.tsx} +52 -57
  125. package/src/components/{settings/account → auth/multi-session}/manage-account.tsx +15 -15
  126. package/src/components/{settings/account → auth/multi-session}/manage-accounts.tsx +16 -12
  127. package/src/components/{user/switch-account-menu.tsx → auth/multi-session/switch-account-submenu-content.tsx} +19 -17
  128. package/src/components/{user/switch-account-item.tsx → auth/multi-session/switch-account-submenu-item.tsx} +18 -8
  129. package/src/components/auth/multi-session/switch-account-submenu.tsx +50 -0
  130. package/src/components/auth/passkey/passkey-button.tsx +56 -0
  131. package/src/components/{settings/security → auth/passkey}/passkey.tsx +21 -9
  132. package/src/components/auth/passkey/passkeys.tsx +196 -0
  133. package/src/components/auth/provider-button.tsx +65 -0
  134. package/src/components/auth/provider-buttons.tsx +23 -45
  135. package/src/components/auth/reset-password.tsx +14 -9
  136. package/src/components/{settings → auth/settings}/account/account-settings.tsx +15 -13
  137. package/src/components/{settings → auth/settings}/account/change-avatar.tsx +4 -5
  138. package/src/components/{settings → auth/settings}/account/change-email.tsx +5 -6
  139. package/src/components/auth/settings/account/user-profile.tsx +171 -0
  140. package/src/components/{settings → auth/settings}/security/active-session.tsx +8 -6
  141. package/src/components/{settings → auth/settings}/security/active-sessions.tsx +5 -10
  142. package/src/components/{settings → auth/settings}/security/change-password.tsx +22 -14
  143. package/src/components/{settings → auth/settings}/security/linked-account.tsx +10 -16
  144. package/src/components/{settings → auth/settings}/security/linked-accounts.tsx +4 -9
  145. package/src/components/{settings → auth/settings}/security/security-settings.tsx +7 -6
  146. package/src/components/{settings → auth/settings}/settings.tsx +2 -2
  147. package/src/components/auth/sign-in.tsx +39 -80
  148. package/src/components/auth/sign-out.tsx +2 -2
  149. package/src/components/auth/sign-up.tsx +110 -147
  150. package/src/components/{settings/account → auth/theme}/appearance.tsx +18 -25
  151. package/src/components/auth/theme/theme-toggle-item.tsx +110 -0
  152. package/src/components/{user → auth/user}/user-avatar.tsx +12 -4
  153. package/src/components/{user → auth/user}/user-button.tsx +15 -88
  154. package/src/components/{user → auth/user}/user-view.tsx +12 -4
  155. package/src/components/auth/username/sign-in-username.tsx +283 -0
  156. package/src/components/auth/username/username-field.tsx +122 -0
  157. package/src/email.ts +9 -0
  158. package/src/index.tsx +13 -27
  159. package/src/lib/auth/auth-plugin.ts +67 -0
  160. package/src/lib/auth/delete-user-plugin.ts +15 -0
  161. package/src/lib/auth/magic-link-plugin.ts +26 -0
  162. package/src/lib/auth/multi-session-plugin.ts +17 -0
  163. package/src/lib/auth/passkey-plugin.ts +17 -0
  164. package/src/lib/auth/theme-plugin.ts +86 -0
  165. package/src/lib/auth/username-plugin.tsx +30 -0
  166. package/src/plugins.ts +23 -0
  167. package/dist/components/auth/magic-link-button.d.ts +0 -13
  168. package/dist/components/auth/magic-link-button.js +0 -16
  169. package/dist/components/auth/magic-link.d.ts +0 -17
  170. package/dist/components/auth/magic-link.js +0 -111
  171. package/dist/components/auth/passkey-button.d.ts +0 -4
  172. package/dist/components/auth/passkey-button.js +0 -24
  173. package/dist/components/email/magic-link-email.d.ts +0 -3
  174. package/dist/components/email/new-device-email.d.ts +0 -3
  175. package/dist/components/email/otp-email.d.ts +0 -3
  176. package/dist/components/settings/account/account-settings.js +0 -23
  177. package/dist/components/settings/account/appearance.js +0 -71
  178. package/dist/components/settings/account/change-avatar.js +0 -89
  179. package/dist/components/settings/account/manage-account.js +0 -51
  180. package/dist/components/settings/account/manage-accounts.js +0 -33
  181. package/dist/components/settings/account/user-profile.js +0 -106
  182. package/dist/components/settings/security/active-session.js +0 -62
  183. package/dist/components/settings/security/active-sessions.js +0 -34
  184. package/dist/components/settings/security/change-password.js +0 -192
  185. package/dist/components/settings/security/delete-user.js +0 -97
  186. package/dist/components/settings/security/linked-account.js +0 -62
  187. package/dist/components/settings/security/linked-accounts.js +0 -45
  188. package/dist/components/settings/security/passkey.js +0 -43
  189. package/dist/components/settings/security/passkeys.js +0 -53
  190. package/dist/components/settings/security/security-settings.js +0 -25
  191. package/dist/components/settings/settings.js +0 -45
  192. package/dist/components/user/switch-account-item.js +0 -20
  193. package/dist/components/user/switch-account-menu.js +0 -28
  194. package/dist/components/user/user-avatar.js +0 -29
  195. package/dist/components/user/user-button.js +0 -91
  196. package/dist/components/user/user-view.js +0 -37
  197. package/dist/core.d.ts +0 -1
  198. package/dist/core.js +0 -1
  199. package/dist/react-exports.d.ts +0 -1
  200. package/dist/react-exports.js +0 -1
  201. package/src/components/auth/magic-link-button.tsx +0 -43
  202. package/src/components/auth/passkey-button.tsx +0 -36
  203. package/src/components/settings/account/user-profile.tsx +0 -218
  204. package/src/components/settings/security/passkeys.tsx +0 -104
  205. package/src/core.ts +0 -1
  206. package/src/react-exports.ts +0 -1
  207. /package/dist/components/{settings/security → auth/passkey}/passkey.d.ts +0 -0
  208. /package/dist/components/{settings → auth/settings}/account/change-avatar.d.ts +0 -0
  209. /package/dist/components/{settings → auth/settings}/security/active-session.d.ts +0 -0
  210. /package/dist/components/{settings → auth/settings}/security/linked-account.d.ts +0 -0
  211. /package/dist/components/{settings → auth/settings}/settings.d.ts +0 -0
  212. /package/dist/components/{user → auth/user}/user-avatar.d.ts +0 -0
  213. /package/dist/components/{user → auth/user}/user-view.d.ts +0 -0
@@ -1,10 +1,9 @@
1
1
  import {
2
- useAuth,
3
- useIsUsernameAvailable,
4
- useSignInSocial,
5
- useSignUpEmail
6
- } from "@better-auth-ui/react"
7
- import { Check, Eye, EyeSlash, Xmark } from "@gravity-ui/icons"
2
+ authMutationKeys,
3
+ parseAdditionalFieldValue
4
+ } from "@better-auth-ui/core"
5
+ import { useAuth, useSignUpEmail } from "@better-auth-ui/react"
6
+ import { Eye, EyeSlash } from "@gravity-ui/icons"
8
7
  import {
9
8
  Button,
10
9
  Card,
@@ -21,11 +20,10 @@ import {
21
20
  TextField,
22
21
  toast
23
22
  } from "@heroui/react"
24
- import { useDebouncer } from "@tanstack/react-pacer"
23
+ import { useIsMutating } from "@tanstack/react-query"
25
24
  import { type SyntheticEvent, useState } from "react"
26
-
25
+ import { AdditionalField } from "./additional-field"
27
26
  import { FieldSeparator } from "./field-separator"
28
- import { MagicLinkButton } from "./magic-link-button"
29
27
  import { ProviderButtons, type SocialLayout } from "./provider-buttons"
30
28
 
31
29
  export type SignUpProps = {
@@ -51,91 +49,60 @@ export function SignUp({
51
49
  socialPosition = "bottom",
52
50
  variant,
53
51
  ...props
54
- }: SignUpProps & CardProps) {
52
+ }: SignUpProps & Omit<CardProps, "children">) {
55
53
  const {
54
+ additionalFields,
55
+ authClient,
56
56
  basePaths,
57
57
  emailAndPassword,
58
58
  localization,
59
- magicLink,
59
+ plugins,
60
60
  redirectTo,
61
61
  socialProviders,
62
- username: usernameConfig,
63
62
  viewPaths,
64
63
  navigate
65
64
  } = useAuth()
66
65
 
67
66
  const [password, setPassword] = useState("")
68
67
  const [confirmPassword, setConfirmPassword] = useState("")
69
- const [username, setUsername] = useState("")
70
68
 
71
- const {
72
- mutate: isUsernameAvailable,
73
- data: usernameData,
74
- error: usernameError,
75
- reset: resetUsername
76
- } = useIsUsernameAvailable()
77
-
78
- const usernameDebouncer = useDebouncer(
79
- (value: string) => {
80
- if (!value.trim()) {
81
- resetUsername()
82
- return
69
+ const { mutate: signUpEmail, isPending: signUpEmailPending } = useSignUpEmail(
70
+ authClient,
71
+ {
72
+ onError: (error) => {
73
+ setPassword("")
74
+ setConfirmPassword("")
75
+ toast.danger(error.error?.message || error.message)
76
+ },
77
+ onSuccess: () => {
78
+ if (emailAndPassword?.requireEmailVerification) {
79
+ toast.success(localization.auth.verifyYourEmail)
80
+ navigate({ to: `${basePaths.auth}/${viewPaths.auth.signIn}` })
81
+ } else {
82
+ navigate({ to: redirectTo })
83
+ }
83
84
  }
84
-
85
- isUsernameAvailable({ username: value.trim() })
86
- },
87
- { wait: 500 }
88
- )
89
-
90
- function handleUsernameChange(value: string) {
91
- setUsername(value)
92
- resetUsername()
93
-
94
- if (usernameConfig?.isUsernameAvailable) {
95
- usernameDebouncer.maybeExecute(value)
96
85
  }
97
- }
98
-
99
- const { mutate: signUpEmail, isPending: signUpPending } = useSignUpEmail({
100
- onError: (error) => {
101
- setPassword("")
102
- setConfirmPassword("")
103
- toast.danger(error.error?.message || error.message)
104
- },
105
- onSuccess: () => {
106
- if (emailAndPassword?.requireEmailVerification) {
107
- toast.success(localization.auth.verifyYourEmail)
108
- navigate({ to: `${basePaths.auth}/${viewPaths.auth.signIn}` })
109
- } else {
110
- navigate({ to: redirectTo })
111
- }
112
- }
113
- })
114
-
115
- const [socialRedirecting, setSocialRedirecting] = useState(false)
116
-
117
- const { mutate: signInSocial, isPending: socialPending } = useSignInSocial({
118
- onError: (error) => toast.danger(error.error?.message || error.message),
119
- onSuccess: () => {
120
- setSocialRedirecting(true)
121
-
122
- setTimeout(() => {
123
- setSocialRedirecting(false)
124
- }, 5000)
125
- }
126
- })
86
+ )
127
87
 
128
88
  const [isPasswordVisible, setIsPasswordVisible] = useState(false)
129
89
  const [isConfirmPasswordVisible, setIsConfirmPasswordVisible] =
130
90
  useState(false)
131
91
 
132
- const isPending = signUpPending || socialPending || socialRedirecting
92
+ const signInMutating = useIsMutating({
93
+ mutationKey: authMutationKeys.signIn.all
94
+ })
95
+ const signUpMutating = useIsMutating({
96
+ mutationKey: authMutationKeys.signUp.all
97
+ })
98
+ const isPending = signInMutating + signUpMutating > 0
133
99
 
134
- const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
100
+ const handleSubmit = async (e: SyntheticEvent<HTMLFormElement>) => {
135
101
  e.preventDefault()
136
102
 
137
103
  const formData = new FormData(e.currentTarget)
138
- const name = formData.get("name") as string
104
+ // `emailAndPassword.name === false` hides the name field and submits "".
105
+ const name = (formData.get("name") as string | null) ?? ""
139
106
  const email = formData.get("email") as string
140
107
 
141
108
  if (emailAndPassword?.confirmPassword && password !== confirmPassword) {
@@ -145,18 +112,34 @@ export function SignUp({
145
112
  return
146
113
  }
147
114
 
115
+ const additionalFieldValues: Record<string, unknown> = {}
116
+
117
+ for (const field of additionalFields ?? []) {
118
+ if (!field.signUp || field.readOnly) continue
119
+ const value = parseAdditionalFieldValue(
120
+ field,
121
+ formData.get(field.name) as string | null
122
+ )
123
+
124
+ if (field.validate) {
125
+ try {
126
+ await field.validate(value)
127
+ } catch (error) {
128
+ toast.danger(error instanceof Error ? error.message : String(error))
129
+ return
130
+ }
131
+ }
132
+
133
+ if (value !== undefined) {
134
+ additionalFieldValues[field.name] = value
135
+ }
136
+ }
137
+
148
138
  signUpEmail({
149
139
  name,
150
140
  email,
151
141
  password,
152
- ...(usernameConfig?.enabled
153
- ? {
154
- username: username.trim(),
155
- ...(usernameConfig.displayUsername
156
- ? { displayUsername: username.trim() }
157
- : {})
158
- }
159
- : {})
142
+ ...additionalFieldValues
160
143
  })
161
144
  }
162
145
 
@@ -164,7 +147,7 @@ export function SignUp({
164
147
 
165
148
  return (
166
149
  <Card
167
- className={cn("w-full max-w-sm p-4 md:p-6", className)}
150
+ className={cn("w-full max-w-sm gap-4 md:p-6", className)}
168
151
  variant={variant}
169
152
  {...props}
170
153
  >
@@ -178,11 +161,7 @@ export function SignUp({
178
161
  {socialPosition === "top" && (
179
162
  <>
180
163
  {!!socialProviders?.length && (
181
- <ProviderButtons
182
- isPending={isPending}
183
- socialLayout={socialLayout}
184
- signInSocial={signInSocial}
185
- />
164
+ <ProviderButtons socialLayout={socialLayout} />
186
165
  )}
187
166
 
188
167
  {showSeparator && (
@@ -193,66 +172,22 @@ export function SignUp({
193
172
 
194
173
  {emailAndPassword?.enabled && (
195
174
  <Form onSubmit={handleSubmit} className="flex flex-col gap-4">
196
- <TextField
197
- name="name"
198
- type="text"
199
- autoComplete="name"
200
- isDisabled={isPending}
201
- >
202
- <Label>{localization.auth.name}</Label>
203
-
204
- <Input
205
- placeholder={localization.auth.namePlaceholder}
206
- required
207
- variant={variant === "transparent" ? "primary" : "secondary"}
208
- />
209
-
210
- <FieldError />
211
- </TextField>
212
-
213
- {usernameConfig?.enabled && (
175
+ {emailAndPassword.name !== false && (
214
176
  <TextField
215
- name="username"
177
+ name="name"
216
178
  type="text"
217
- autoComplete="username"
218
- minLength={usernameConfig.minUsernameLength}
219
- maxLength={usernameConfig.maxUsernameLength}
179
+ autoComplete="name"
220
180
  isDisabled={isPending}
221
- value={username}
222
- onChange={handleUsernameChange}
223
- isInvalid={
224
- !!usernameError || (usernameData && !usernameData.available)
225
- }
226
181
  >
227
- <Label>{localization.auth.username}</Label>
182
+ <Label>{localization.auth.name}</Label>
228
183
 
229
- <InputGroup
184
+ <Input
185
+ placeholder={localization.auth.namePlaceholder}
186
+ required
230
187
  variant={variant === "transparent" ? "primary" : "secondary"}
231
- >
232
- <InputGroup.Input
233
- placeholder={localization.auth.usernamePlaceholder}
234
- required
235
- />
236
-
237
- {usernameConfig.isUsernameAvailable && username.trim() && (
238
- <InputGroup.Suffix className="px-2">
239
- {usernameData?.available ? (
240
- <Check className="text-success" />
241
- ) : usernameError || usernameData?.available === false ? (
242
- <Xmark className="text-danger" />
243
- ) : (
244
- <Spinner size="sm" color="current" />
245
- )}
246
- </InputGroup.Suffix>
247
- )}
248
- </InputGroup>
188
+ />
249
189
 
250
- <FieldError>
251
- {usernameError?.error?.message ||
252
- usernameError?.message ||
253
- (usernameData?.available === false &&
254
- localization.auth.usernameTaken)}
255
- </FieldError>
190
+ <FieldError />
256
191
  </TextField>
257
192
  )}
258
193
 
@@ -273,6 +208,19 @@ export function SignUp({
273
208
  <FieldError />
274
209
  </TextField>
275
210
 
211
+ {additionalFields?.map(
212
+ (field) =>
213
+ field.signUp === "above" && (
214
+ <AdditionalField
215
+ key={field.name}
216
+ name={field.name}
217
+ field={field}
218
+ isPending={isPending}
219
+ variant={variant}
220
+ />
221
+ )
222
+ )}
223
+
276
224
  <TextField
277
225
  minLength={emailAndPassword?.minPasswordLength}
278
226
  maxLength={emailAndPassword?.maxPasswordLength}
@@ -361,15 +309,34 @@ export function SignUp({
361
309
  </TextField>
362
310
  )}
363
311
 
312
+ {additionalFields?.map(
313
+ (field) =>
314
+ field.signUp &&
315
+ field.signUp !== "above" && (
316
+ <AdditionalField
317
+ key={field.name}
318
+ name={field.name}
319
+ field={field}
320
+ isPending={isPending}
321
+ variant={variant}
322
+ />
323
+ )
324
+ )}
325
+
364
326
  <div className="flex flex-col gap-3">
365
327
  <Button type="submit" className="w-full" isPending={isPending}>
366
- {isPending && <Spinner color="current" size="sm" />}
328
+ {signUpEmailPending && <Spinner color="current" size="sm" />}
367
329
 
368
330
  {localization.auth.signUp}
369
331
  </Button>
370
332
 
371
- {magicLink && (
372
- <MagicLinkButton view="signUp" isPending={isPending} />
333
+ {plugins.flatMap((plugin) =>
334
+ plugin.authButtons?.map((AuthButton, index) => (
335
+ <AuthButton
336
+ key={`${plugin.id}-${index.toString()}`}
337
+ view="signUp"
338
+ />
339
+ ))
373
340
  )}
374
341
  </div>
375
342
  </Form>
@@ -382,22 +349,18 @@ export function SignUp({
382
349
  )}
383
350
 
384
351
  {!!socialProviders?.length && (
385
- <ProviderButtons
386
- socialLayout={socialLayout}
387
- signInSocial={signInSocial}
388
- isPending={isPending}
389
- />
352
+ <ProviderButtons socialLayout={socialLayout} />
390
353
  )}
391
354
  </>
392
355
  )}
393
356
  </Card.Content>
394
357
 
395
- <Card.Footer className="flex-col">
358
+ <Card.Footer className="flex-col gap-3">
396
359
  <Description className="text-sm">
397
360
  {localization.auth.alreadyHaveAnAccount}{" "}
398
361
  <Link
399
362
  href={`${basePaths.auth}/${viewPaths.auth.signIn}`}
400
- className="text-accent decoration-accent no-underline hover:underline"
363
+ className="text-accent no-underline hover:underline decoration-accent-hover"
401
364
  >
402
365
  {localization.auth.signIn}
403
366
  </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
- }: AppearanceProps & CardProps) {
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
- }
36
+ }: AppearanceProps & Omit<CardProps, "children">) {
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 (