@better-auth-ui/heroui 1.6.0

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 (253) hide show
  1. package/dist/components/account/account.d.ts +18 -0
  2. package/dist/components/account/account.js +24 -0
  3. package/dist/components/account/settings/account-settings.d.ts +5 -0
  4. package/dist/components/account/settings/account-settings.js +6 -0
  5. package/dist/components/account/settings/settings.d.ts +5 -0
  6. package/dist/components/account/settings/settings.js +5 -0
  7. package/dist/components/account/settings/user-profile.d.ts +5 -0
  8. package/dist/components/account/settings/user-profile.js +20 -0
  9. package/dist/components/auth/auth-provider.d.ts +9 -0
  10. package/dist/components/auth/auth-provider.js +12 -0
  11. package/dist/components/auth/auth.d.ts +23 -0
  12. package/dist/components/auth/auth.js +42 -0
  13. package/dist/components/auth/field-separator.d.ts +4 -0
  14. package/dist/components/auth/field-separator.js +5 -0
  15. package/dist/components/auth/forgot-password.d.ts +15 -0
  16. package/dist/components/auth/forgot-password.js +28 -0
  17. package/dist/components/auth/magic-link-button.d.ts +13 -0
  18. package/dist/components/auth/magic-link-button.js +19 -0
  19. package/dist/components/auth/magic-link.d.ts +17 -0
  20. package/dist/components/auth/magic-link.js +43 -0
  21. package/dist/components/auth/provider-buttons.d.ts +19 -0
  22. package/dist/components/auth/provider-buttons.js +34 -0
  23. package/dist/components/auth/reset-password.d.ts +11 -0
  24. package/dist/components/auth/reset-password.js +58 -0
  25. package/dist/components/auth/sign-in.d.ts +14 -0
  26. package/dist/components/auth/sign-in.js +64 -0
  27. package/dist/components/auth/sign-out.d.ts +9 -0
  28. package/dist/components/auth/sign-out.js +33 -0
  29. package/dist/components/auth/sign-up.d.ts +19 -0
  30. package/dist/components/auth/sign-up.js +71 -0
  31. package/dist/components/email/email-changed-email.d.ts +3 -0
  32. package/dist/components/email/email-changed-email.js +23 -0
  33. package/dist/components/email/email-verification-email.d.ts +3 -0
  34. package/dist/components/email/email-verification-email.js +23 -0
  35. package/dist/components/email/magic-link-email.d.ts +3 -0
  36. package/dist/components/email/magic-link-email.js +23 -0
  37. package/dist/components/email/new-device-email.d.ts +3 -0
  38. package/dist/components/email/new-device-email.js +23 -0
  39. package/dist/components/email/otp-email.d.ts +3 -0
  40. package/dist/components/email/otp-email.js +22 -0
  41. package/dist/components/email/password-changed-email.d.ts +3 -0
  42. package/dist/components/email/password-changed-email.js +23 -0
  43. package/dist/components/email/reset-password-email.d.ts +3 -0
  44. package/dist/components/email/reset-password-email.js +23 -0
  45. package/dist/components/settings/account/account-settings.d.ts +16 -0
  46. package/dist/components/settings/account/account-settings.js +20 -0
  47. package/dist/components/settings/account/accounts.d.ts +14 -0
  48. package/dist/components/settings/account/accounts.js +26 -0
  49. package/dist/components/settings/account/appearance.d.ts +16 -0
  50. package/dist/components/settings/account/appearance.js +28 -0
  51. package/dist/components/settings/account/change-avatar.d.ts +4 -0
  52. package/dist/components/settings/account/change-avatar.js +56 -0
  53. package/dist/components/settings/account/change-email.d.ts +15 -0
  54. package/dist/components/settings/account/change-email.js +29 -0
  55. package/dist/components/settings/account/manage-account.d.ts +19 -0
  56. package/dist/components/settings/account/manage-account.js +32 -0
  57. package/dist/components/settings/account/manage-accounts.d.ts +14 -0
  58. package/dist/components/settings/account/manage-accounts.js +37 -0
  59. package/dist/components/settings/account/theme-selector.d.ts +16 -0
  60. package/dist/components/settings/account/theme-selector.js +33 -0
  61. package/dist/components/settings/account/theme-settings.d.ts +16 -0
  62. package/dist/components/settings/account/theme-settings.js +31 -0
  63. package/dist/components/settings/account/user-profile.d.ts +11 -0
  64. package/dist/components/settings/account/user-profile.js +23 -0
  65. package/dist/components/settings/account-settings.d.ts +5 -0
  66. package/dist/components/settings/account-settings.js +6 -0
  67. package/dist/components/settings/account.d.ts +18 -0
  68. package/dist/components/settings/account.js +24 -0
  69. package/dist/components/settings/security/active-session.d.ts +14 -0
  70. package/dist/components/settings/security/active-session.js +53 -0
  71. package/dist/components/settings/security/active-sessions.d.ts +14 -0
  72. package/dist/components/settings/security/active-sessions.js +28 -0
  73. package/dist/components/settings/security/change-password.d.ts +15 -0
  74. package/dist/components/settings/security/change-password.js +79 -0
  75. package/dist/components/settings/security/connected-account.d.ts +16 -0
  76. package/dist/components/settings/security/connected-account.js +46 -0
  77. package/dist/components/settings/security/connected-accounts.d.ts +15 -0
  78. package/dist/components/settings/security/connected-accounts.js +31 -0
  79. package/dist/components/settings/security/danger-zone.d.ts +11 -0
  80. package/dist/components/settings/security/danger-zone.js +12 -0
  81. package/dist/components/settings/security/delete-user.d.ts +9 -0
  82. package/dist/components/settings/security/delete-user.js +47 -0
  83. package/dist/components/settings/security/linked-account.d.ts +16 -0
  84. package/dist/components/settings/security/linked-account.js +48 -0
  85. package/dist/components/settings/security/linked-accounts.d.ts +15 -0
  86. package/dist/components/settings/security/linked-accounts.js +42 -0
  87. package/dist/components/settings/security/security-settings.d.ts +17 -0
  88. package/dist/components/settings/security/security-settings.js +21 -0
  89. package/dist/components/settings/security/sessions.d.ts +14 -0
  90. package/dist/components/settings/security/sessions.js +40 -0
  91. package/dist/components/settings/settings/account-settings.d.ts +5 -0
  92. package/dist/components/settings/settings/account-settings.js +6 -0
  93. package/dist/components/settings/settings/user-profile.d.ts +5 -0
  94. package/dist/components/settings/settings/user-profile.js +20 -0
  95. package/dist/components/settings/settings.d.ts +24 -0
  96. package/dist/components/settings/settings.js +28 -0
  97. package/dist/components/settings/user-profile.d.ts +5 -0
  98. package/dist/components/settings/user-profile.js +20 -0
  99. package/dist/components/user/switch-account-item.d.ts +16 -0
  100. package/dist/components/user/switch-account-item.js +16 -0
  101. package/dist/components/user/switch-account-menu.d.ts +10 -0
  102. package/dist/components/user/switch-account-menu.js +29 -0
  103. package/dist/components/user/switch-account-popover.d.ts +10 -0
  104. package/dist/components/user/switch-account-popover.js +25 -0
  105. package/dist/components/user/switch-account-submenu.d.ts +10 -0
  106. package/dist/components/user/switch-account-submenu.js +25 -0
  107. package/dist/components/user/user-avatar.d.ts +25 -0
  108. package/dist/components/user/user-avatar.js +31 -0
  109. package/dist/components/user/user-button.d.ts +25 -0
  110. package/dist/components/user/user-button.js +24 -0
  111. package/dist/components/user/user-view.d.ts +22 -0
  112. package/dist/components/user/user-view.js +24 -0
  113. package/dist/core.d.ts +1 -0
  114. package/dist/core.js +1 -0
  115. package/dist/hooks/use-auth.d.ts +2 -0
  116. package/dist/hooks/use-auth.js +8 -0
  117. package/dist/hooks/use-redirect-to.d.ts +8 -0
  118. package/dist/hooks/use-redirect-to.js +19 -0
  119. package/dist/hooks/use-sign-in.d.ts +8 -0
  120. package/dist/hooks/use-sign-in.js +53 -0
  121. package/dist/index.d.ts +33 -0
  122. package/dist/index.js +34 -0
  123. package/dist/lib/utils.d.ts +2 -0
  124. package/dist/lib/utils.js +5 -0
  125. package/dist/react-exports.d.ts +1 -0
  126. package/dist/react-exports.js +1 -0
  127. package/dist/server.d.ts +1 -0
  128. package/dist/server.js +1 -0
  129. package/dist/src/components/auth/auth-provider.d.ts +8 -0
  130. package/dist/src/components/auth/auth-provider.js +13 -0
  131. package/dist/src/components/auth/auth.d.ts +22 -0
  132. package/dist/src/components/auth/auth.js +42 -0
  133. package/dist/src/components/auth/field-separator.d.ts +4 -0
  134. package/dist/src/components/auth/field-separator.js +5 -0
  135. package/dist/src/components/auth/forgot-password.d.ts +15 -0
  136. package/dist/src/components/auth/forgot-password.js +29 -0
  137. package/dist/src/components/auth/magic-link-button.d.ts +13 -0
  138. package/dist/src/components/auth/magic-link-button.js +20 -0
  139. package/dist/src/components/auth/magic-link.d.ts +17 -0
  140. package/dist/src/components/auth/magic-link.js +37 -0
  141. package/dist/src/components/auth/provider-buttons.d.ts +19 -0
  142. package/dist/src/components/auth/provider-buttons.js +35 -0
  143. package/dist/src/components/auth/reset-password.d.ts +11 -0
  144. package/dist/src/components/auth/reset-password.js +59 -0
  145. package/dist/src/components/auth/sign-in.d.ts +14 -0
  146. package/dist/src/components/auth/sign-in.js +58 -0
  147. package/dist/src/components/auth/sign-out.d.ts +11 -0
  148. package/dist/src/components/auth/sign-out.js +34 -0
  149. package/dist/src/components/auth/sign-up.d.ts +19 -0
  150. package/dist/src/components/auth/sign-up.js +65 -0
  151. package/dist/src/components/email/email-changed-email.d.ts +3 -0
  152. package/dist/src/components/email/email-changed-email.js +23 -0
  153. package/dist/src/components/email/email-verification-email.d.ts +3 -0
  154. package/dist/src/components/email/email-verification-email.js +23 -0
  155. package/dist/src/components/email/magic-link-email.d.ts +3 -0
  156. package/dist/src/components/email/magic-link-email.js +23 -0
  157. package/dist/src/components/email/new-device-email.d.ts +3 -0
  158. package/dist/src/components/email/new-device-email.js +23 -0
  159. package/dist/src/components/email/otp-email.d.ts +3 -0
  160. package/dist/src/components/email/otp-email.js +22 -0
  161. package/dist/src/components/email/password-changed-email.d.ts +3 -0
  162. package/dist/src/components/email/password-changed-email.js +23 -0
  163. package/dist/src/components/email/reset-password-email.d.ts +3 -0
  164. package/dist/src/components/email/reset-password-email.js +23 -0
  165. package/dist/src/components/settings/account/account-settings.d.ts +13 -0
  166. package/dist/src/components/settings/account/account-settings.js +19 -0
  167. package/dist/src/components/settings/account/appearance.d.ts +16 -0
  168. package/dist/src/components/settings/account/appearance.js +28 -0
  169. package/dist/src/components/settings/account/change-email.d.ts +15 -0
  170. package/dist/src/components/settings/account/change-email.js +35 -0
  171. package/dist/src/components/settings/account/manage-account.d.ts +19 -0
  172. package/dist/src/components/settings/account/manage-account.js +27 -0
  173. package/dist/src/components/settings/account/manage-accounts.d.ts +14 -0
  174. package/dist/src/components/settings/account/manage-accounts.js +27 -0
  175. package/dist/src/components/settings/account/user-profile.d.ts +13 -0
  176. package/dist/src/components/settings/account/user-profile.js +29 -0
  177. package/dist/src/components/settings/security/active-session.d.ts +14 -0
  178. package/dist/src/components/settings/security/active-session.js +35 -0
  179. package/dist/src/components/settings/security/active-sessions.d.ts +14 -0
  180. package/dist/src/components/settings/security/active-sessions.js +29 -0
  181. package/dist/src/components/settings/security/change-password.d.ts +15 -0
  182. package/dist/src/components/settings/security/change-password.js +57 -0
  183. package/dist/src/components/settings/security/linked-account.d.ts +16 -0
  184. package/dist/src/components/settings/security/linked-account.js +46 -0
  185. package/dist/src/components/settings/security/linked-accounts.d.ts +15 -0
  186. package/dist/src/components/settings/security/linked-accounts.js +31 -0
  187. package/dist/src/components/settings/security/security-settings.d.ts +15 -0
  188. package/dist/src/components/settings/security/security-settings.js +18 -0
  189. package/dist/src/components/settings/settings.d.ts +29 -0
  190. package/dist/src/components/settings/settings.js +39 -0
  191. package/dist/src/components/user/switch-account-item.d.ts +16 -0
  192. package/dist/src/components/user/switch-account-item.js +14 -0
  193. package/dist/src/components/user/switch-account-menu.d.ts +10 -0
  194. package/dist/src/components/user/switch-account-menu.js +23 -0
  195. package/dist/src/components/user/user-avatar.d.ts +24 -0
  196. package/dist/src/components/user/user-avatar.js +31 -0
  197. package/dist/src/components/user/user-button.d.ts +21 -0
  198. package/dist/src/components/user/user-button.js +24 -0
  199. package/dist/src/components/user/user-view.d.ts +21 -0
  200. package/dist/src/components/user/user-view.js +24 -0
  201. package/dist/src/core.d.ts +1 -0
  202. package/dist/src/core.js +1 -0
  203. package/dist/src/index.d.ts +31 -0
  204. package/dist/src/index.js +32 -0
  205. package/dist/src/lib/utils.d.ts +2 -0
  206. package/dist/src/lib/utils.js +5 -0
  207. package/dist/src/react-exports.d.ts +1 -0
  208. package/dist/src/react-exports.js +1 -0
  209. package/dist/styles.css +1 -0
  210. package/package.json +63 -0
  211. package/src/components/auth/auth-provider.tsx +27 -0
  212. package/src/components/auth/auth.tsx +87 -0
  213. package/src/components/auth/field-separator.tsx +14 -0
  214. package/src/components/auth/forgot-password.tsx +107 -0
  215. package/src/components/auth/magic-link-button.tsx +43 -0
  216. package/src/components/auth/magic-link.tsx +181 -0
  217. package/src/components/auth/provider-buttons.tsx +85 -0
  218. package/src/components/auth/reset-password.tsx +210 -0
  219. package/src/components/auth/sign-in.tsx +260 -0
  220. package/src/components/auth/sign-out.tsx +44 -0
  221. package/src/components/auth/sign-up.tsx +314 -0
  222. package/src/components/email/email-changed-email.tsx +38 -0
  223. package/src/components/email/email-verification-email.tsx +38 -0
  224. package/src/components/email/magic-link-email.tsx +38 -0
  225. package/src/components/email/new-device-email.tsx +38 -0
  226. package/src/components/email/otp-email.tsx +33 -0
  227. package/src/components/email/password-changed-email.tsx +38 -0
  228. package/src/components/email/reset-password-email.tsx +38 -0
  229. package/src/components/settings/account/account-settings.tsx +48 -0
  230. package/src/components/settings/account/appearance.tsx +157 -0
  231. package/src/components/settings/account/change-avatar.tsx +142 -0
  232. package/src/components/settings/account/change-email.tsx +111 -0
  233. package/src/components/settings/account/manage-account.tsx +123 -0
  234. package/src/components/settings/account/manage-accounts.tsx +78 -0
  235. package/src/components/settings/account/user-profile.tsx +103 -0
  236. package/src/components/settings/security/active-session.tsx +123 -0
  237. package/src/components/settings/security/active-sessions.tsx +77 -0
  238. package/src/components/settings/security/change-password.tsx +335 -0
  239. package/src/components/settings/security/danger-zone.tsx +32 -0
  240. package/src/components/settings/security/delete-user.tsx +187 -0
  241. package/src/components/settings/security/linked-account.tsx +136 -0
  242. package/src/components/settings/security/linked-accounts.tsx +98 -0
  243. package/src/components/settings/security/security-settings.tsx +43 -0
  244. package/src/components/settings/settings.tsx +96 -0
  245. package/src/components/user/switch-account-item.tsx +39 -0
  246. package/src/components/user/switch-account-menu.tsx +64 -0
  247. package/src/components/user/user-avatar.tsx +88 -0
  248. package/src/components/user/user-button.tsx +221 -0
  249. package/src/components/user/user-view.tsx +74 -0
  250. package/src/core.ts +1 -0
  251. package/src/index.tsx +35 -0
  252. package/src/react-exports.ts +1 -0
  253. package/src/styles.css +1 -0
@@ -0,0 +1,98 @@
1
+ import { useAuth, useListAccounts } from "@better-auth-ui/react"
2
+ import { Card, type CardProps, cn, Skeleton, toast } from "@heroui/react"
3
+ import { LinkedAccount } from "./linked-account"
4
+
5
+ export type LinkedAccountsProps = {
6
+ className?: string
7
+ variant?: CardProps["variant"]
8
+ }
9
+
10
+ /**
11
+ * Render a card showing linked social accounts and available social providers to link.
12
+ *
13
+ * Linked accounts (excluding the "credential" provider) are shown with an unlink control;
14
+ * available providers are shown with a link control. Button states and labels reflect
15
+ * ongoing link/unlink activity and use localization for provider-specific text.
16
+ *
17
+ * @returns A JSX element containing the linked accounts card
18
+ */
19
+ export function LinkedAccounts({
20
+ className,
21
+ variant,
22
+ ...props
23
+ }: LinkedAccountsProps & CardProps) {
24
+ const { localization, socialProviders } = useAuth()
25
+
26
+ const { data: accounts, isPending } = useListAccounts({
27
+ throwOnError: (error) => {
28
+ if (error.error) toast.danger(error.error.message)
29
+ return false
30
+ }
31
+ })
32
+
33
+ const linkedAccounts = accounts?.filter(
34
+ (account) => account.providerId !== "credential"
35
+ )
36
+
37
+ const allRows = [
38
+ ...(linkedAccounts?.map((account) => ({
39
+ key: account.id,
40
+ account,
41
+ provider: account.providerId
42
+ })) ?? []),
43
+ ...(socialProviders?.map((provider) => ({
44
+ key: provider,
45
+ account: undefined,
46
+ provider
47
+ })) ?? [])
48
+ ]
49
+
50
+ return (
51
+ <div>
52
+ <h2 className={cn("text-sm font-semibold mb-3")}>
53
+ {localization.settings.linkedAccounts}
54
+ </h2>
55
+
56
+ <Card className={cn(className)} variant={variant} {...props}>
57
+ <Card.Content className="gap-0">
58
+ {isPending
59
+ ? socialProviders?.map((provider, index) => (
60
+ <div key={provider}>
61
+ {index > 0 && (
62
+ <div className="border-b border-dashed -mx-4 md:-mx-6 my-4" />
63
+ )}
64
+ <AccountRowSkeleton />
65
+ </div>
66
+ ))
67
+ : allRows.map((row, index) => (
68
+ <div key={row.key}>
69
+ {index > 0 && (
70
+ <div className="border-b border-dashed -mx-4 md:-mx-6 my-4" />
71
+ )}
72
+
73
+ <LinkedAccount
74
+ account={row.account}
75
+ provider={row.provider}
76
+ />
77
+ </div>
78
+ ))}
79
+ </Card.Content>
80
+ </Card>
81
+ </div>
82
+ )
83
+ }
84
+
85
+ function AccountRowSkeleton() {
86
+ return (
87
+ <div className="flex items-center justify-between">
88
+ <div className="flex items-center gap-3">
89
+ <Skeleton className="size-10 rounded-xl" />
90
+
91
+ <div className="flex flex-col gap-1">
92
+ <Skeleton className="h-4 w-20 rounded-lg" />
93
+ <Skeleton className="h-3 w-32 rounded-lg" />
94
+ </div>
95
+ </div>
96
+ </div>
97
+ )
98
+ }
@@ -0,0 +1,43 @@
1
+ import { useAuth } from "@better-auth-ui/react"
2
+ import { type CardProps, cn } from "@heroui/react"
3
+ import type { ComponentProps } from "react"
4
+
5
+ import { ActiveSessions } from "./active-sessions"
6
+ import { ChangePassword } from "./change-password"
7
+ import { DangerZone } from "./danger-zone"
8
+ import { LinkedAccounts } from "./linked-accounts"
9
+
10
+ export type SecuritySettingsProps = {
11
+ className?: string
12
+ variant?: CardProps["variant"]
13
+ }
14
+
15
+ /**
16
+ * Renders the security settings layout including password management, linked accounts, and active sessions.
17
+ *
18
+ * ChangePassword is rendered when password authentication is enabled; LinkedAccounts is rendered when social providers are present.
19
+ * DangerZone is rendered when `deleteUser.enabled` is true in auth config.
20
+ *
21
+ * @param className - Optional additional CSS class names for the outer container.
22
+ * @param variant - Card variant forwarded to each security settings card.
23
+ * @returns The security settings container as a JSX element.
24
+ */
25
+ export function SecuritySettings({
26
+ className,
27
+ variant,
28
+ ...props
29
+ }: SecuritySettingsProps & ComponentProps<"div">) {
30
+ const { deleteUser, emailAndPassword, socialProviders } = useAuth()
31
+
32
+ return (
33
+ <div
34
+ className={cn("flex w-full flex-col gap-4 md:gap-6", className)}
35
+ {...props}
36
+ >
37
+ {emailAndPassword?.enabled && <ChangePassword variant={variant} />}
38
+ {!!socialProviders?.length && <LinkedAccounts variant={variant} />}
39
+ <ActiveSessions variant={variant} />
40
+ {deleteUser?.enabled && <DangerZone variant={variant} />}
41
+ </div>
42
+ )
43
+ }
@@ -0,0 +1,96 @@
1
+ import { useAuth, useAuthenticate } from "@better-auth-ui/react"
2
+ import type { SettingsView } from "@better-auth-ui/react/core"
3
+ import { type CardProps, cn, Tabs } from "@heroui/react"
4
+ import { type ComponentProps, useMemo } from "react"
5
+
6
+ import { AccountSettings } from "./account/account-settings"
7
+ import { SecuritySettings } from "./security/security-settings"
8
+
9
+ export type SettingsProps = {
10
+ className?: string
11
+ hideNav?: boolean
12
+ path?: string
13
+ variant?: CardProps["variant"]
14
+ /** @remarks `SettingsView` */
15
+ view?: SettingsView
16
+ }
17
+
18
+ /**
19
+ * Renders the settings UI and activates the appropriate settings view based on `view` or `path`.
20
+ *
21
+ * @param className - Additional CSS class names applied to the root container
22
+ * @param path - Route path used to resolve which settings view to activate when `view` is not provided
23
+ * @param view - Explicit settings view to activate, e.g. `"account"` or `"security"`
24
+ * @param hideNav - When `true`, hide the navigation tabs
25
+ * @param variant - Card variant forwarded to each settings section card
26
+ * @returns A DOM tree containing the responsive settings tabs and the currently selected settings panel
27
+ *
28
+ * @throws Error if neither `view` nor `path` is provided
29
+ */
30
+ export function Settings({
31
+ className,
32
+ hideNav,
33
+ path,
34
+ variant,
35
+ view,
36
+ ...props
37
+ }: SettingsProps & ComponentProps<"div">) {
38
+ const { basePaths, localization, viewPaths } = useAuth()
39
+ useAuthenticate()
40
+
41
+ if (!view && !path) {
42
+ throw new Error("[Better Auth UI] Either `view` or `path` must be provided")
43
+ }
44
+
45
+ const settingsPathViews = useMemo(
46
+ () =>
47
+ Object.fromEntries(
48
+ Object.entries(viewPaths.settings).map(([k, v]) => [v, k])
49
+ ) as Record<string, SettingsView>,
50
+ [viewPaths.settings]
51
+ )
52
+
53
+ const currentView = view || (path ? settingsPathViews[path] : undefined)
54
+
55
+ return (
56
+ <Tabs
57
+ className={cn(className)}
58
+ orientation="horizontal"
59
+ selectedKey={currentView}
60
+ {...props}
61
+ >
62
+ <Tabs.ListContainer>
63
+ <Tabs.List
64
+ aria-label={localization.settings.settings}
65
+ className="overflow-auto w-fit"
66
+ >
67
+ <Tabs.Tab
68
+ id="account"
69
+ href={`${basePaths.settings}/${viewPaths.settings.account}`}
70
+ >
71
+ {localization.settings.account}
72
+
73
+ <Tabs.Indicator />
74
+ </Tabs.Tab>
75
+
76
+ <Tabs.Tab
77
+ id="security"
78
+ href={`${basePaths.settings}/${viewPaths.settings.security}`}
79
+ >
80
+ {localization.settings.security}
81
+
82
+ <Tabs.Indicator />
83
+ </Tabs.Tab>
84
+ </Tabs.List>
85
+ </Tabs.ListContainer>
86
+
87
+ <Tabs.Panel id="account" className="px-0">
88
+ <AccountSettings variant={variant} />
89
+ </Tabs.Panel>
90
+
91
+ <Tabs.Panel id="security" className="px-0">
92
+ <SecuritySettings variant={variant} />
93
+ </Tabs.Panel>
94
+ </Tabs>
95
+ )
96
+ }
@@ -0,0 +1,39 @@
1
+ import { useSetActiveSession } from "@better-auth-ui/react"
2
+ import { Dropdown, Spinner, toast } from "@heroui/react"
3
+ import type { Session, User } from "better-auth"
4
+ import { UserView } from "./user-view"
5
+
6
+ type DeviceSession = {
7
+ session: Session
8
+ user: User
9
+ }
10
+
11
+ export type SwitchAccountItemProps = {
12
+ deviceSession: DeviceSession
13
+ }
14
+
15
+ /**
16
+ * Render a dropdown item for switching to a different authenticated session.
17
+ *
18
+ * @param deviceSession - The device session to display and switch to when pressed
19
+ * @returns The switch account dropdown item as a JSX element
20
+ */
21
+ export function SwitchAccountItem({ deviceSession }: SwitchAccountItemProps) {
22
+ const { mutate: setActiveSession, isPending } = useSetActiveSession({
23
+ onError: (error) => toast.danger(error.error?.message || error.message)
24
+ })
25
+
26
+ return (
27
+ <Dropdown.Item
28
+ className="px-2"
29
+ isDisabled={isPending}
30
+ onPress={() =>
31
+ setActiveSession({ sessionToken: deviceSession.session.token })
32
+ }
33
+ >
34
+ <UserView user={deviceSession.user} />
35
+
36
+ {isPending && <Spinner color="current" size="sm" className="ml-auto" />}
37
+ </Dropdown.Item>
38
+ )
39
+ }
@@ -0,0 +1,64 @@
1
+ import {
2
+ useAuth,
3
+ useListDeviceSessions,
4
+ useSession
5
+ } from "@better-auth-ui/react"
6
+ import { Check, CirclePlus } from "@gravity-ui/icons"
7
+ import { Dropdown, Label, Separator, toast } from "@heroui/react"
8
+
9
+ import { SwitchAccountItem } from "./switch-account-item"
10
+ import { UserView } from "./user-view"
11
+
12
+ /**
13
+ * Render the menu content for switching between multiple authenticated sessions.
14
+ *
15
+ * Shows the current session with a checkmark, lists other device sessions that can be activated,
16
+ * and provides an option to add a new account. This component should be rendered inside a
17
+ * Dropdown.SubmenuTrigger to defer the useListDeviceSessions query until the submenu is opened.
18
+ *
19
+ * @returns The switch account menu content as a JSX element
20
+ */
21
+ export function SwitchAccountMenu() {
22
+ const { basePaths, viewPaths, localization } = useAuth()
23
+ const { data: session } = useSession()
24
+ const { data: deviceSessions, isPending } = useListDeviceSessions({
25
+ throwOnError: (error) => {
26
+ if (error.error) toast.danger(error.error.message)
27
+ return false
28
+ }
29
+ })
30
+
31
+ return (
32
+ <Dropdown.Popover className="min-w-40 md:min-w-56 max-w-[48svw]">
33
+ <Dropdown.Menu>
34
+ <Dropdown.Item className="px-2">
35
+ <UserView isPending={isPending} />
36
+
37
+ {!isPending && <Check className="ml-auto" />}
38
+ </Dropdown.Item>
39
+
40
+ {deviceSessions
41
+ ?.filter(
42
+ (deviceSession) => deviceSession.session.id !== session?.session?.id
43
+ )
44
+ .map((deviceSession) => (
45
+ <SwitchAccountItem
46
+ key={deviceSession.session.id}
47
+ deviceSession={deviceSession}
48
+ />
49
+ ))}
50
+
51
+ <Separator />
52
+
53
+ <Dropdown.Item
54
+ textValue={localization.auth.addAccount}
55
+ href={`${basePaths.auth}/${viewPaths.auth.signIn}`}
56
+ >
57
+ <CirclePlus className="text-muted" />
58
+
59
+ <Label>{localization.auth.addAccount}</Label>
60
+ </Dropdown.Item>
61
+ </Dropdown.Menu>
62
+ </Dropdown.Popover>
63
+ )
64
+ }
@@ -0,0 +1,88 @@
1
+ import { useSession } from "@better-auth-ui/react"
2
+ import { Person } from "@gravity-ui/icons"
3
+ import { Avatar, type AvatarProps, cn, Skeleton } from "@heroui/react"
4
+ import type { User } from "better-auth"
5
+ import type { ReactNode } from "react"
6
+
7
+ export type UserAvatarProps = {
8
+ className?: string
9
+ fallback?: ReactNode
10
+ isPending?: boolean
11
+ /** @remarks `User` */
12
+ user?: User & { username?: string | null; displayUsername?: string | null }
13
+ size?: AvatarProps["size"]
14
+ }
15
+
16
+ /**
17
+ * Render a user avatar that shows the user's image, initials, or a custom fallback; displays a circular skeleton while the session (or explicit pending flag) is pending.
18
+ *
19
+ * @param className - Additional CSS classes applied to the outer avatar container
20
+ * @param fallback - Custom fallback content to render when no image or initials are available
21
+ * @param isPending - When true, force rendering of the loading skeleton (unless an explicit `user` prop is provided)
22
+ * @param user - Optional user object to display; when omitted the current session user is used if available
23
+ * @param size - Visual size of the avatar; one of `"sm"`, `"md"`, or `"lg"` (default: `"sm"`)
24
+ * @returns A React element that displays the user's avatar image, initials, or the provided fallback; renders a circular skeleton while loading
25
+ */
26
+ export function UserAvatar({
27
+ className,
28
+ fallback,
29
+ isPending,
30
+ user,
31
+ size = "sm",
32
+ style,
33
+ ...props
34
+ }: UserAvatarProps & AvatarProps) {
35
+ const { data: session, isPending: sessionPending } = useSession({
36
+ enabled: !user && !isPending
37
+ })
38
+
39
+ if ((isPending || sessionPending) && !user) {
40
+ return (
41
+ <Skeleton
42
+ className={cn(
43
+ "rounded-full",
44
+ size === "sm" ? "size-8" : size === "md" ? "size-10" : "size-12",
45
+ className
46
+ )}
47
+ style={style}
48
+ />
49
+ )
50
+ }
51
+
52
+ const resolvedUser = user ?? session?.user
53
+
54
+ const initials = (
55
+ resolvedUser?.username ||
56
+ resolvedUser?.name ||
57
+ resolvedUser?.email
58
+ )
59
+ ?.slice(0, 2)
60
+ .toUpperCase()
61
+
62
+ return (
63
+ <Avatar
64
+ size={size}
65
+ className={cn("rounded-full", className)}
66
+ style={style}
67
+ {...props}
68
+ >
69
+ <Avatar.Image
70
+ alt={
71
+ resolvedUser?.displayUsername ||
72
+ resolvedUser?.name ||
73
+ resolvedUser?.email
74
+ }
75
+ src={resolvedUser?.image ?? undefined}
76
+ />
77
+
78
+ <Avatar.Fallback
79
+ className={cn(
80
+ size === "lg" ? "text-xl" : size === "md" ? "text-base" : "text-sm"
81
+ )}
82
+ delayMs={resolvedUser?.image ? 600 : undefined}
83
+ >
84
+ {fallback || initials || <Person className="size-4" />}
85
+ </Avatar.Fallback>
86
+ </Avatar>
87
+ )
88
+ }
@@ -0,0 +1,221 @@
1
+ import { useAuth, useSession } from "@better-auth-ui/react"
2
+ import {
3
+ ArrowRightFromSquare,
4
+ ArrowRightToSquare,
5
+ ChevronsExpandVertical,
6
+ Display,
7
+ Gear,
8
+ Moon,
9
+ PersonPlus,
10
+ Persons,
11
+ Sun
12
+ } from "@gravity-ui/icons"
13
+ import {
14
+ Button,
15
+ type ButtonProps,
16
+ cn,
17
+ Dropdown,
18
+ type DropdownPopoverProps,
19
+ Label,
20
+ Separator,
21
+ Tabs
22
+ } from "@heroui/react"
23
+
24
+ import { SwitchAccountMenu } from "./switch-account-menu"
25
+ import { UserAvatar } from "./user-avatar"
26
+ import { UserView } from "./user-view"
27
+
28
+ export type UserButtonProps = {
29
+ className?: string
30
+ size?: "default" | "icon"
31
+ /**
32
+ * The placement of the element with respect to its anchor element.
33
+ * @default "bottom"
34
+ */
35
+ placement?: DropdownPopoverProps["placement"]
36
+ themeToggle?: boolean
37
+ variant?: ButtonProps["variant"]
38
+ }
39
+
40
+ /**
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.
44
+ *
45
+ * @param className - Additional CSS classes applied to the trigger element
46
+ * @param placement - Dropdown popover placement (e.g., "bottom", "top-start", "bottom-end")
47
+ * @param size - "icon" renders an avatar-only trigger; "default" renders a button with label and chevron
48
+ * @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
+ * @returns The user button and its dropdown menu as a JSX element
51
+ */
52
+ export function UserButton({
53
+ className,
54
+ placement = "bottom",
55
+ size = "default",
56
+ variant = "ghost",
57
+ themeToggle = true
58
+ }: UserButtonProps) {
59
+ const {
60
+ basePaths,
61
+ viewPaths,
62
+ localization,
63
+ multiSession,
64
+ appearance: { theme, setTheme, themes }
65
+ } = useAuth()
66
+
67
+ const { data: session, isPending: sessionPending } = useSession()
68
+
69
+ return (
70
+ <Dropdown>
71
+ {size === "icon" ? (
72
+ <Dropdown.Trigger className={cn("rounded-full", className)}>
73
+ <UserAvatar />
74
+ </Dropdown.Trigger>
75
+ ) : (
76
+ <Button
77
+ variant={variant}
78
+ className={cn(
79
+ "h-auto font-normal justify-start px-3 py-2 text-left",
80
+ className
81
+ )}
82
+ >
83
+ {session || sessionPending ? (
84
+ <UserView isPending={sessionPending} />
85
+ ) : (
86
+ <>
87
+ <UserAvatar />
88
+
89
+ <p className="text-sm font-medium">{localization.auth.account}</p>
90
+ </>
91
+ )}
92
+
93
+ <ChevronsExpandVertical className="ml-auto size-3.5" />
94
+ </Button>
95
+ )}
96
+
97
+ <Dropdown.Popover
98
+ placement={placement}
99
+ className="min-w-40 md:min-w-56 max-w-[48svw]"
100
+ >
101
+ {session && (
102
+ <div className="px-3 pt-3 pb-1">
103
+ <UserView />
104
+ </div>
105
+ )}
106
+
107
+ <Dropdown.Menu>
108
+ {session ? (
109
+ <>
110
+ <Dropdown.Item
111
+ textValue={localization.settings.settings}
112
+ href={`${basePaths.settings}/${viewPaths.settings.account}`}
113
+ >
114
+ <Gear className="text-muted" />
115
+
116
+ <Label>{localization.settings.settings}</Label>
117
+ </Dropdown.Item>
118
+
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
+ )}
184
+
185
+ <Separator />
186
+
187
+ <Dropdown.Item
188
+ textValue={localization.auth.signOut}
189
+ href={`${basePaths.auth}/${viewPaths.auth.signOut}`}
190
+ >
191
+ <ArrowRightFromSquare className="text-muted" />
192
+
193
+ <Label>{localization.auth.signOut}</Label>
194
+ </Dropdown.Item>
195
+ </>
196
+ ) : (
197
+ <>
198
+ <Dropdown.Item
199
+ textValue={localization.auth.signIn}
200
+ href={`${basePaths.auth}/${viewPaths.auth.signIn}`}
201
+ >
202
+ <ArrowRightToSquare className="text-muted" />
203
+
204
+ <Label>{localization.auth.signIn}</Label>
205
+ </Dropdown.Item>
206
+
207
+ <Dropdown.Item
208
+ textValue={localization.auth.signUp}
209
+ href={`${basePaths.auth}/${viewPaths.auth.signUp}`}
210
+ >
211
+ <PersonPlus className="text-muted" />
212
+
213
+ <Label>{localization.auth.signUp}</Label>
214
+ </Dropdown.Item>
215
+ </>
216
+ )}
217
+ </Dropdown.Menu>
218
+ </Dropdown.Popover>
219
+ </Dropdown>
220
+ )
221
+ }