@better-auth-ui/heroui 1.6.3 → 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 (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 +3 -4
  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 +41 -38
  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 +75 -99
  62. package/dist/components/auth/sign-out.js +11 -11
  63. package/dist/components/auth/sign-up.js +123 -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 +142 -0
  74. package/dist/components/auth/username/username-field.d.ts +8 -0
  75. package/dist/components/auth/username/username-field.js +51 -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 +3 -10
  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 +14 -11
  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 +38 -61
  134. package/src/components/auth/sign-out.tsx +2 -2
  135. package/src/components/auth/sign-up.tsx +109 -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 +283 -0
  142. package/src/components/auth/username/username-field.tsx +122 -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
@@ -3,12 +3,8 @@ import {
3
3
  ArrowRightFromSquare,
4
4
  ArrowRightToSquare,
5
5
  ChevronsExpandVertical,
6
- Display,
7
6
  Gear,
8
- Moon,
9
- PersonPlus,
10
- Persons,
11
- Sun
7
+ PersonPlus
12
8
  } from "@gravity-ui/icons"
13
9
  import {
14
10
  Button,
@@ -17,11 +13,9 @@ import {
17
13
  Dropdown,
18
14
  type DropdownPopoverProps,
19
15
  Label,
20
- Separator,
21
- Tabs
16
+ Separator
22
17
  } from "@heroui/react"
23
18
 
24
- import { SwitchAccountMenu } from "./switch-account-menu"
25
19
  import { UserAvatar } from "./user-avatar"
26
20
  import { UserView } from "./user-view"
27
21
 
@@ -33,39 +27,34 @@ export type UserButtonProps = {
33
27
  * @default "bottom"
34
28
  */
35
29
  placement?: DropdownPopoverProps["placement"]
36
- themeToggle?: boolean
37
30
  variant?: ButtonProps["variant"]
38
31
  }
39
32
 
40
33
  /**
41
- * Render a user account dropdown button that shows account actions, session switching, and theme controls.
42
- *
43
- * Renders either an icon-only trigger or a full button showing the current user state; its dropdown contains account settings, multi-session switching (when enabled), theme selection (when enabled), and sign-in/sign-out flows depending on authentication state.
34
+ * Render a user account dropdown button that shows account actions.
44
35
  *
45
36
  * @param className - Additional CSS classes applied to the trigger element
46
37
  * @param placement - Dropdown popover placement (e.g., "bottom", "top-start", "bottom-end")
47
38
  * @param size - "icon" renders an avatar-only trigger; "default" renders a button with label and chevron
48
39
  * @param variant - Button visual variant passed to the underlying Button component
49
- * @param themeToggle - When true and theming is available, show theme selection controls in the menu
50
40
  * @returns The user button and its dropdown menu as a JSX element
51
41
  */
52
42
  export function UserButton({
53
43
  className,
54
44
  placement = "bottom",
55
45
  size = "default",
56
- variant = "ghost",
57
- themeToggle = true
46
+ variant = "ghost"
58
47
  }: UserButtonProps) {
59
- const {
60
- basePaths,
61
- viewPaths,
62
- localization,
63
- multiSession,
64
- appearance: { theme, setTheme, themes }
65
- } = useAuth()
48
+ const { authClient, basePaths, viewPaths, localization, plugins } = useAuth()
66
49
 
67
- const { data: session, isPending: sessionPending } = useSession()
50
+ const { data: session, isPending: sessionPending } = useSession(authClient)
68
51
 
52
+ const userMenuItems = plugins.flatMap(
53
+ (plugin) =>
54
+ plugin.userMenuItems?.map((Item, index) => (
55
+ <Item key={`${plugin.id}-${index.toString()}`} />
56
+ )) ?? []
57
+ )
69
58
  return (
70
59
  <Dropdown>
71
60
  {size === "icon" ? (
@@ -116,71 +105,7 @@ export function UserButton({
116
105
  <Label>{localization.settings.settings}</Label>
117
106
  </Dropdown.Item>
118
107
 
119
- {multiSession && (
120
- <Dropdown.SubmenuTrigger>
121
- <Dropdown.Item textValue={localization.auth.switchAccount}>
122
- <Persons className="text-muted" />
123
-
124
- <Label>{localization.auth.switchAccount}</Label>
125
-
126
- <Dropdown.SubmenuIndicator />
127
- </Dropdown.Item>
128
-
129
- <Dropdown.Popover className="min-w-40 md:min-w-56 max-w-[48svw]">
130
- <SwitchAccountMenu />
131
- </Dropdown.Popover>
132
- </Dropdown.SubmenuTrigger>
133
- )}
134
-
135
- {themeToggle && theme && setTheme && !!themes?.length && (
136
- <Dropdown.Item className="py-1 pe-2">
137
- <Label>{localization.settings.theme}</Label>
138
-
139
- <Tabs
140
- className="ml-auto"
141
- selectedKey={theme}
142
- onSelectionChange={(key) => setTheme(key as string)}
143
- >
144
- <Tabs.ListContainer>
145
- <Tabs.List
146
- aria-label={localization.settings.theme}
147
- className="*:h-5 *:w-5 *:p-0"
148
- >
149
- {themes.includes("system") && (
150
- <Tabs.Tab
151
- id="system"
152
- aria-label={localization.settings.system}
153
- >
154
- <Display className="size-3" />
155
-
156
- <Tabs.Indicator />
157
- </Tabs.Tab>
158
- )}
159
- {themes.includes("light") && (
160
- <Tabs.Tab
161
- id="light"
162
- aria-label={localization.settings.light}
163
- >
164
- <Sun className="size-3" />
165
-
166
- <Tabs.Indicator />
167
- </Tabs.Tab>
168
- )}
169
- {themes.includes("dark") && (
170
- <Tabs.Tab
171
- id="dark"
172
- aria-label={localization.settings.dark}
173
- >
174
- <Moon className="size-3" />
175
-
176
- <Tabs.Indicator />
177
- </Tabs.Tab>
178
- )}
179
- </Tabs.List>
180
- </Tabs.ListContainer>
181
- </Tabs>
182
- </Dropdown.Item>
183
- )}
108
+ {userMenuItems}
184
109
 
185
110
  <Separator />
186
111
 
@@ -212,6 +137,8 @@ export function UserButton({
212
137
 
213
138
  <Label>{localization.auth.signUp}</Label>
214
139
  </Dropdown.Item>
140
+
141
+ {userMenuItems}
215
142
  </>
216
143
  )}
217
144
  </Dropdown.Menu>
@@ -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 { type AvatarProps, cn, Skeleton } from "@heroui/react"
3
7
  import type { User } from "better-auth"
4
8
  import type { ComponentProps } from "react"
@@ -27,9 +31,13 @@ export function UserView({
27
31
  user,
28
32
  ...props
29
33
  }: UserViewProps & ComponentProps<"div">) {
30
- const { data: session, isPending: sessionPending } = useSession({
31
- enabled: !user && !isPending
32
- })
34
+ const { authClient } = useAuth()
35
+ const { data: session, isPending: sessionPending } = useSession(
36
+ authClient as UsernameAuthClient,
37
+ {
38
+ enabled: !user && !isPending
39
+ }
40
+ )
33
41
 
34
42
  const resolvedUser = user ?? session?.user
35
43
 
@@ -0,0 +1,283 @@
1
+ import { authMutationKeys } from "@better-auth-ui/core"
2
+ import {
3
+ type UsernameAuthClient,
4
+ useAuth,
5
+ useAuthPlugin,
6
+ useSendVerificationEmail,
7
+ useSignInEmail,
8
+ useSignInUsername
9
+ } from "@better-auth-ui/react"
10
+ import {
11
+ Button,
12
+ Card,
13
+ type CardProps,
14
+ Checkbox,
15
+ cn,
16
+ Description,
17
+ FieldError,
18
+ Form,
19
+ Input,
20
+ Label,
21
+ Link,
22
+ Spinner,
23
+ TextField,
24
+ toast
25
+ } from "@heroui/react"
26
+ import { useIsMutating } from "@tanstack/react-query"
27
+ import { type SyntheticEvent, useState } from "react"
28
+ import { usernamePlugin } from "../../../lib/auth/username-plugin"
29
+ import { FieldSeparator } from "../field-separator"
30
+ import { ProviderButtons, type SocialLayout } from "../provider-buttons"
31
+
32
+ export interface SignInUsernameProps {
33
+ className?: string
34
+ socialLayout?: SocialLayout
35
+ socialPosition?: "top" | "bottom"
36
+ variant?: CardProps["variant"]
37
+ }
38
+
39
+ /**
40
+ * Render the username-based sign-in UI.
41
+ *
42
+ * @returns The sign-in JSX element containing username/password fields, optional magic-link button, and social provider buttons.
43
+ */
44
+ export function SignInUsername({
45
+ className,
46
+ socialLayout,
47
+ socialPosition = "bottom",
48
+ variant,
49
+ ...props
50
+ }: SignInUsernameProps & Omit<CardProps, "children">) {
51
+ const {
52
+ authClient,
53
+ basePaths,
54
+ baseURL,
55
+ emailAndPassword,
56
+ localization,
57
+ plugins,
58
+ redirectTo,
59
+ socialProviders,
60
+ viewPaths,
61
+ navigate
62
+ } = useAuth()
63
+
64
+ const { localization: usernameLocalization } = useAuthPlugin(usernamePlugin)
65
+
66
+ const [password, setPassword] = useState("")
67
+
68
+ const { mutate: sendVerificationEmail } = useSendVerificationEmail(
69
+ authClient,
70
+ {
71
+ onSuccess: () => toast.success(localization.auth.verificationEmailSent)
72
+ }
73
+ )
74
+
75
+ function isEmail(value: string): boolean {
76
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
77
+ }
78
+
79
+ const { mutate: signInEmail } = useSignInEmail(authClient, {
80
+ onError: (error, { email }) => {
81
+ setPassword("")
82
+
83
+ if (error.error?.code === "EMAIL_NOT_VERIFIED") {
84
+ toast.danger(error.error?.message || error.message, {
85
+ actionProps: {
86
+ children: localization.auth.resend,
87
+ onClick: () =>
88
+ sendVerificationEmail({
89
+ email,
90
+ callbackURL: `${baseURL}${redirectTo}`
91
+ })
92
+ }
93
+ })
94
+ } else {
95
+ toast.danger(error.error?.message || error.message)
96
+ }
97
+ },
98
+ onSuccess: () => navigate({ to: redirectTo })
99
+ })
100
+
101
+ const { mutate: signInUsername, isPending: isSignInPending } =
102
+ useSignInUsername(authClient as UsernameAuthClient, {
103
+ onError: (error) => {
104
+ setPassword("")
105
+ toast.danger(error.error?.message || error.message)
106
+ },
107
+ onSuccess: () => navigate({ to: redirectTo })
108
+ })
109
+
110
+ const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
111
+ e.preventDefault()
112
+
113
+ const formData = new FormData(e.currentTarget)
114
+ const email = formData.get("email") as string
115
+ const rememberMe = formData.get("rememberMe") === "on"
116
+
117
+ if (isEmail(email)) {
118
+ signInEmail({
119
+ email,
120
+ password,
121
+ ...(emailAndPassword?.rememberMe ? { rememberMe } : {})
122
+ })
123
+ } else {
124
+ signInUsername({
125
+ username: email,
126
+ password,
127
+ ...(emailAndPassword?.rememberMe ? { rememberMe } : {})
128
+ })
129
+ }
130
+ }
131
+
132
+ const signInMutating = useIsMutating({
133
+ mutationKey: authMutationKeys.signIn.all
134
+ })
135
+ const signUpMutating = useIsMutating({
136
+ mutationKey: authMutationKeys.signUp.all
137
+ })
138
+ const isPending = signInMutating + signUpMutating > 0
139
+
140
+ const showSeparator = emailAndPassword?.enabled && !!socialProviders?.length
141
+
142
+ return (
143
+ <Card
144
+ className={cn("w-full max-w-sm gap-4 md:p-6", className)}
145
+ variant={variant}
146
+ {...props}
147
+ >
148
+ <Card.Header>
149
+ <Card.Title className="text-xl font-semibold mb-1">
150
+ {localization.auth.signIn}
151
+ </Card.Title>
152
+ </Card.Header>
153
+
154
+ <Card.Content className="gap-4">
155
+ {socialPosition === "top" && (
156
+ <>
157
+ {!!socialProviders?.length && (
158
+ <ProviderButtons socialLayout={socialLayout} />
159
+ )}
160
+
161
+ {showSeparator && (
162
+ <FieldSeparator>{localization.auth.or}</FieldSeparator>
163
+ )}
164
+ </>
165
+ )}
166
+
167
+ {emailAndPassword?.enabled && (
168
+ <Form onSubmit={handleSubmit} className="flex flex-col gap-4">
169
+ <TextField
170
+ name="email"
171
+ type="text"
172
+ autoComplete="username email"
173
+ isDisabled={isPending}
174
+ >
175
+ <Label>{usernameLocalization.username}</Label>
176
+
177
+ <Input
178
+ placeholder={usernameLocalization.usernameOrEmailPlaceholder}
179
+ variant={variant === "transparent" ? "primary" : "secondary"}
180
+ required
181
+ />
182
+
183
+ <FieldError />
184
+ </TextField>
185
+
186
+ <TextField
187
+ minLength={emailAndPassword?.minPasswordLength}
188
+ maxLength={emailAndPassword?.maxPasswordLength}
189
+ name="password"
190
+ type="password"
191
+ autoComplete="current-password"
192
+ isDisabled={isPending}
193
+ value={password}
194
+ onChange={setPassword}
195
+ >
196
+ <Label>{localization.auth.password}</Label>
197
+
198
+ <Input
199
+ placeholder={localization.auth.passwordPlaceholder}
200
+ variant={variant === "transparent" ? "primary" : "secondary"}
201
+ required
202
+ />
203
+
204
+ <FieldError />
205
+ </TextField>
206
+
207
+ {emailAndPassword?.rememberMe && (
208
+ <Checkbox
209
+ name="rememberMe"
210
+ isDisabled={isPending}
211
+ variant={variant === "transparent" ? "primary" : "secondary"}
212
+ >
213
+ <Checkbox.Control>
214
+ <Checkbox.Indicator />
215
+ </Checkbox.Control>
216
+
217
+ <Checkbox.Content>
218
+ <Label>{localization.auth.rememberMe}</Label>
219
+ </Checkbox.Content>
220
+ </Checkbox>
221
+ )}
222
+
223
+ <div className="flex flex-col gap-3">
224
+ <Button
225
+ type="submit"
226
+ className="w-full"
227
+ isPending={isSignInPending || isPending}
228
+ >
229
+ {isSignInPending && <Spinner color="current" size="sm" />}
230
+
231
+ {localization.auth.signIn}
232
+ </Button>
233
+
234
+ {plugins.flatMap((plugin) =>
235
+ plugin.authButtons?.map((AuthButton, index) => (
236
+ <AuthButton
237
+ key={`${plugin.id}-${index.toString()}`}
238
+ view="signIn"
239
+ />
240
+ ))
241
+ )}
242
+ </div>
243
+ </Form>
244
+ )}
245
+
246
+ {socialPosition === "bottom" && (
247
+ <>
248
+ {showSeparator && (
249
+ <FieldSeparator>{localization.auth.or}</FieldSeparator>
250
+ )}
251
+
252
+ {!!socialProviders?.length && (
253
+ <ProviderButtons socialLayout={socialLayout} />
254
+ )}
255
+ </>
256
+ )}
257
+ </Card.Content>
258
+
259
+ <Card.Footer className="flex-col gap-3">
260
+ {emailAndPassword?.forgotPassword && (
261
+ <Link
262
+ href={`${basePaths.auth}/${viewPaths.auth.forgotPassword}`}
263
+ className="no-underline hover:underline"
264
+ >
265
+ {localization.auth.forgotPasswordLink}
266
+ </Link>
267
+ )}
268
+
269
+ {emailAndPassword?.enabled && (
270
+ <Description className="text-sm">
271
+ {localization.auth.needToCreateAnAccount}{" "}
272
+ <Link
273
+ href={`${basePaths.auth}/${viewPaths.auth.signUp}`}
274
+ className="text-accent no-underline hover:underline decoration-accent-hover"
275
+ >
276
+ {localization.auth.signUp}
277
+ </Link>
278
+ </Description>
279
+ )}
280
+ </Card.Footer>
281
+ </Card>
282
+ )
283
+ }
@@ -0,0 +1,122 @@
1
+ import {
2
+ type UsernameAuthClient,
3
+ useAuth,
4
+ useAuthPlugin,
5
+ useIsUsernameAvailable
6
+ } from "@better-auth-ui/react"
7
+ import { Check, Xmark } from "@gravity-ui/icons"
8
+ import {
9
+ FieldError,
10
+ InputGroup,
11
+ Label,
12
+ Spinner,
13
+ TextField
14
+ } from "@heroui/react"
15
+ import { useDebouncer } from "@tanstack/react-pacer"
16
+ import { useState } from "react"
17
+ import { usernamePlugin } from "../../../lib/auth/username-plugin"
18
+ import type { AdditionalFieldProps } from "../additional-field"
19
+
20
+ /**
21
+ * Renderer for the `username` additional field. Owns availability checking,
22
+ * length limits, and visual indicators. `FieldError` automatically surfaces
23
+ * native validation messages. Availability feedback is shown via the icon and
24
+ * aria-label without affecting the field's invalid state.
25
+ */
26
+ export function UsernameField({
27
+ name,
28
+ field,
29
+ isPending,
30
+ variant
31
+ }: AdditionalFieldProps) {
32
+ const { authClient } = useAuth()
33
+ const {
34
+ localization,
35
+ minUsernameLength,
36
+ maxUsernameLength,
37
+ isUsernameAvailable: checkAvailability
38
+ } = useAuthPlugin(usernamePlugin)
39
+
40
+ const currentUsername = String(field.defaultValue ?? "")
41
+ const [value, setValue] = useState(currentUsername)
42
+
43
+ const {
44
+ mutate: requestAvailability,
45
+ data: availability,
46
+ error: availabilityError,
47
+ reset: resetAvailability
48
+ } = useIsUsernameAvailable(authClient as UsernameAuthClient, {
49
+ // Bypass global error toast
50
+ onError: () => {}
51
+ })
52
+
53
+ const debouncer = useDebouncer(
54
+ (next: string) => {
55
+ const trimmed = next.trim()
56
+ // Skip blank input and the user's own current username (profile view).
57
+ if (!trimmed || trimmed === currentUsername) {
58
+ resetAvailability()
59
+ return
60
+ }
61
+
62
+ requestAvailability({ username: trimmed })
63
+ },
64
+ { wait: 500 }
65
+ )
66
+
67
+ function handleChange(next: string) {
68
+ setValue(next)
69
+ resetAvailability()
70
+
71
+ if (checkAvailability) {
72
+ debouncer.maybeExecute(next)
73
+ }
74
+ }
75
+
76
+ const isCheckingAvailability =
77
+ !!checkAvailability && !!value.trim() && value.trim() !== currentUsername
78
+
79
+ return (
80
+ <TextField
81
+ name={name}
82
+ type="text"
83
+ autoComplete="username"
84
+ minLength={minUsernameLength}
85
+ maxLength={maxUsernameLength}
86
+ isDisabled={isPending}
87
+ isRequired={field.required}
88
+ isReadOnly={field.readOnly}
89
+ value={value}
90
+ onChange={handleChange}
91
+ >
92
+ <Label>{field.label}</Label>
93
+
94
+ <InputGroup variant={variant === "transparent" ? "primary" : "secondary"}>
95
+ <InputGroup.Input placeholder={field.placeholder} />
96
+
97
+ {isCheckingAvailability && (
98
+ <InputGroup.Suffix
99
+ aria-label={
100
+ availability?.available
101
+ ? localization.usernameAvailable
102
+ : availability?.available === false
103
+ ? localization.usernameTaken
104
+ : undefined
105
+ }
106
+ className="px-2"
107
+ >
108
+ {availability?.available ? (
109
+ <Check className="text-success" />
110
+ ) : availabilityError || availability?.available === false ? (
111
+ <Xmark className="text-danger" />
112
+ ) : (
113
+ <Spinner size="sm" color="current" />
114
+ )}
115
+ </InputGroup.Suffix>
116
+ )}
117
+ </InputGroup>
118
+
119
+ <FieldError />
120
+ </TextField>
121
+ )
122
+ }
package/src/email.ts ADDED
@@ -0,0 +1,9 @@
1
+ "use client"
2
+
3
+ export * from "./components/auth/email/email-changed-email"
4
+ export * from "./components/auth/email/email-verification-email"
5
+ export * from "./components/auth/email/magic-link-email"
6
+ export * from "./components/auth/email/new-device-email"
7
+ export * from "./components/auth/email/otp-email"
8
+ export * from "./components/auth/email/password-changed-email"
9
+ export * from "./components/auth/email/reset-password-email"
package/src/index.tsx CHANGED
@@ -1,38 +1,23 @@
1
1
  "use client"
2
2
 
3
+ export * from "./components/auth/additional-field"
3
4
  export * from "./components/auth/auth"
4
5
  export * from "./components/auth/auth-provider"
5
6
  export * from "./components/auth/forgot-password"
6
- export * from "./components/auth/magic-link"
7
- export * from "./components/auth/magic-link-button"
8
- export * from "./components/auth/passkey-button"
9
7
  export * from "./components/auth/provider-button"
10
8
  export * from "./components/auth/provider-buttons"
11
9
  export * from "./components/auth/reset-password"
10
+ export * from "./components/auth/settings/account/account-settings"
11
+ export * from "./components/auth/settings/account/change-email"
12
+ export * from "./components/auth/settings/account/user-profile"
13
+ export * from "./components/auth/settings/security/active-sessions"
14
+ export * from "./components/auth/settings/security/change-password"
15
+ export * from "./components/auth/settings/security/linked-accounts"
16
+ export * from "./components/auth/settings/security/security-settings"
17
+ export * from "./components/auth/settings/settings"
12
18
  export * from "./components/auth/sign-in"
13
19
  export * from "./components/auth/sign-out"
14
20
  export * from "./components/auth/sign-up"
15
- export * from "./components/email/email-changed-email"
16
- export * from "./components/email/email-verification-email"
17
- export * from "./components/email/magic-link-email"
18
- export * from "./components/email/new-device-email"
19
- export * from "./components/email/otp-email"
20
- export * from "./components/email/password-changed-email"
21
- export * from "./components/email/reset-password-email"
22
- export * from "./components/settings/account/account-settings"
23
- export * from "./components/settings/account/appearance"
24
- export * from "./components/settings/account/change-email"
25
- export * from "./components/settings/account/manage-account"
26
- export * from "./components/settings/account/manage-accounts"
27
- export * from "./components/settings/account/user-profile"
28
- export * from "./components/settings/security/active-sessions"
29
- export * from "./components/settings/security/change-password"
30
- export * from "./components/settings/security/danger-zone"
31
- export * from "./components/settings/security/delete-user"
32
- export * from "./components/settings/security/linked-accounts"
33
- export * from "./components/settings/security/passkeys"
34
- export * from "./components/settings/security/security-settings"
35
- export * from "./components/settings/settings"
36
- export * from "./components/user/user-avatar"
37
- export * from "./components/user/user-button"
38
- export * from "./components/user/user-view"
21
+ export * from "./components/auth/user/user-avatar"
22
+ export * from "./components/auth/user/user-button"
23
+ export * from "./components/auth/user/user-view"