@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,123 @@
1
+ import {
2
+ useAuth,
3
+ useRevokeMultiSession,
4
+ useSetActiveSession,
5
+ useUser
6
+ } from "@better-auth-ui/react"
7
+ import {
8
+ ArrowRightArrowLeft,
9
+ ArrowRightFromSquare,
10
+ Ellipsis
11
+ } from "@gravity-ui/icons"
12
+ import { Button, Dropdown, Spinner, toast } from "@heroui/react"
13
+ import type { Session, User } from "better-auth"
14
+
15
+ import { UserView } from "../../user/user-view"
16
+
17
+ export type DeviceSession = {
18
+ session: Session
19
+ user: User
20
+ }
21
+
22
+ export type ManageAccountProps = {
23
+ deviceSession?: DeviceSession | null
24
+ isPending?: boolean
25
+ }
26
+
27
+ /**
28
+ * Render a single account row with user info and a dropdown for switch/sign-out actions.
29
+ *
30
+ * Shows the user's avatar and info. A three-dot menu provides options to
31
+ * switch account (for non-active sessions) and sign out.
32
+ *
33
+ * @param deviceSession - The device session object containing session and user data
34
+ * @returns A JSX element containing the account row
35
+ */
36
+ export function ManageAccount({
37
+ deviceSession,
38
+ isPending
39
+ }: ManageAccountProps) {
40
+ const { localization } = useAuth()
41
+ const { data: user } = useUser()
42
+
43
+ const { mutate: setActiveSession, isPending: isSwitching } =
44
+ useSetActiveSession({
45
+ onError: (error) => toast.danger(error.error?.message || error.message)
46
+ })
47
+
48
+ const { mutate: revokeSession, isPending: isRevoking } =
49
+ useRevokeMultiSession({
50
+ onError: (error) => toast.danger(error.error?.message || error.message),
51
+ onSuccess: () => toast.success(localization.settings.revokeSessionSuccess)
52
+ })
53
+
54
+ const isActive = deviceSession?.session.userId === user?.id
55
+ const isBusy = isSwitching || isRevoking
56
+
57
+ return (
58
+ <div className="flex items-center justify-between gap-3">
59
+ <UserView user={deviceSession?.user} isPending={isPending} size="md" />
60
+
61
+ {deviceSession && isActive && (
62
+ <Button
63
+ className="shrink-0"
64
+ variant="outline"
65
+ size="sm"
66
+ onPress={() =>
67
+ revokeSession({ sessionToken: deviceSession.session.token })
68
+ }
69
+ isDisabled={isBusy}
70
+ >
71
+ {isRevoking ? (
72
+ <Spinner color="current" size="sm" />
73
+ ) : (
74
+ <ArrowRightFromSquare />
75
+ )}
76
+ {localization.auth.signOut}
77
+ </Button>
78
+ )}
79
+
80
+ {deviceSession && !isActive && (
81
+ <Dropdown>
82
+ <Button
83
+ isIconOnly
84
+ variant="ghost"
85
+ className="shrink-0"
86
+ size="sm"
87
+ isDisabled={isBusy}
88
+ >
89
+ <Ellipsis />
90
+ </Button>
91
+
92
+ <Dropdown.Popover>
93
+ <Dropdown.Menu>
94
+ <Dropdown.Item
95
+ textValue={localization.auth.switchAccount}
96
+ onAction={() =>
97
+ setActiveSession({
98
+ sessionToken: deviceSession.session.token
99
+ })
100
+ }
101
+ >
102
+ <ArrowRightArrowLeft className="text-muted" />
103
+ {localization.auth.switchAccount}
104
+ </Dropdown.Item>
105
+
106
+ <Dropdown.Item
107
+ textValue={localization.auth.signOut}
108
+ onAction={() =>
109
+ revokeSession({
110
+ sessionToken: deviceSession.session.token
111
+ })
112
+ }
113
+ >
114
+ <ArrowRightFromSquare className="text-muted" />
115
+ {localization.auth.signOut}
116
+ </Dropdown.Item>
117
+ </Dropdown.Menu>
118
+ </Dropdown.Popover>
119
+ </Dropdown>
120
+ )}
121
+ </div>
122
+ )
123
+ }
@@ -0,0 +1,78 @@
1
+ import {
2
+ useAuth,
3
+ useListDeviceSessions,
4
+ useSession
5
+ } from "@better-auth-ui/react"
6
+ import { Card, type CardProps, cn, toast } from "@heroui/react"
7
+ import { ManageAccount } from "./manage-account"
8
+
9
+ export type ManageAccountsProps = {
10
+ className?: string
11
+ variant?: CardProps["variant"]
12
+ }
13
+
14
+ /**
15
+ * Render a card that lists and manages all device sessions for the current user.
16
+ *
17
+ * Shows each session with user information and actions to switch to or revoke a session.
18
+ * When device session data is loading, a pending placeholder row is displayed.
19
+ *
20
+ * @returns A JSX element containing the accounts management card
21
+ */
22
+ export function ManageAccounts({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: ManageAccountsProps & CardProps) {
27
+ const { localization } = useAuth()
28
+ const { data: session } = useSession()
29
+
30
+ const { data: deviceSessions, isPending } = useListDeviceSessions({
31
+ throwOnError: (error) => {
32
+ if (error.error) toast.danger(error.error.message)
33
+ return false
34
+ }
35
+ })
36
+
37
+ const otherSessions = deviceSessions?.filter(
38
+ (deviceSession) => deviceSession.session.id !== session?.session.id
39
+ )
40
+
41
+ const allRows = [
42
+ {
43
+ key: "current",
44
+ deviceSession: !isPending ? session : null,
45
+ isPending
46
+ },
47
+ ...(otherSessions?.map((deviceSession) => ({
48
+ key: deviceSession.session.id,
49
+ deviceSession,
50
+ isPending: false
51
+ })) ?? [])
52
+ ]
53
+
54
+ return (
55
+ <div>
56
+ <h2 className={cn("text-sm font-semibold mb-3")}>
57
+ {localization.settings.manageAccounts}
58
+ </h2>
59
+
60
+ <Card className={cn(className)} variant={variant} {...props}>
61
+ <Card.Content className="gap-0">
62
+ {allRows.map((row, index) => (
63
+ <div key={row.key}>
64
+ {index > 0 && (
65
+ <div className="border-b border-dashed -mx-4 md:-mx-6 my-4" />
66
+ )}
67
+
68
+ <ManageAccount
69
+ deviceSession={row.deviceSession}
70
+ isPending={row.isPending}
71
+ />
72
+ </div>
73
+ ))}
74
+ </Card.Content>
75
+ </Card>
76
+ </div>
77
+ )
78
+ }
@@ -0,0 +1,103 @@
1
+ import { useAuth, useSession, useUpdateUser } from "@better-auth-ui/react"
2
+ import {
3
+ Button,
4
+ Card,
5
+ type CardProps,
6
+ cn,
7
+ FieldError,
8
+ Fieldset,
9
+ Form,
10
+ Input,
11
+ Label,
12
+ Skeleton,
13
+ Spinner,
14
+ TextField,
15
+ toast
16
+ } from "@heroui/react"
17
+ import type { SyntheticEvent } from "react"
18
+ import { ChangeAvatar } from "./change-avatar"
19
+
20
+ export type UserProfileProps = {
21
+ className?: string
22
+ variant?: CardProps["variant"]
23
+ }
24
+
25
+ /**
26
+ * Render a profile card that lets the authenticated user view and update their display name and avatar.
27
+ *
28
+ * @returns A JSX element containing the user profile card with avatar upload and an editable name form
29
+ */
30
+ export function UserProfile({
31
+ className,
32
+ variant,
33
+ ...props
34
+ }: UserProfileProps & CardProps) {
35
+ const { localization } = useAuth()
36
+ const { data: session } = useSession()
37
+
38
+ const { mutate: updateUser, isPending } = useUpdateUser({
39
+ onError: (error) => toast.danger(error.error?.message || error.message),
40
+ onSuccess: () => toast.success(localization.settings.profileUpdatedSuccess)
41
+ })
42
+
43
+ function handleSubmit(e: SyntheticEvent<HTMLFormElement>) {
44
+ e.preventDefault()
45
+ const formData = new FormData(e.currentTarget)
46
+ updateUser({ name: formData.get("name") as string })
47
+ }
48
+
49
+ return (
50
+ <div>
51
+ <h2 className={cn("text-sm font-semibold mb-3")}>
52
+ {localization.settings.profile}
53
+ </h2>
54
+
55
+ <Card className={cn("p-4 gap-4", className)} variant={variant} {...props}>
56
+ <Card.Content>
57
+ <Form onSubmit={handleSubmit}>
58
+ <Fieldset className="w-full gap-4">
59
+ <ChangeAvatar />
60
+
61
+ <Fieldset.Group>
62
+ <TextField
63
+ key={session?.user?.name}
64
+ name="name"
65
+ defaultValue={session?.user.name}
66
+ isDisabled={isPending || !session}
67
+ >
68
+ <Label>{localization.auth.name}</Label>
69
+
70
+ {session ? (
71
+ <Input
72
+ autoComplete="name"
73
+ placeholder={localization.auth.name}
74
+ variant={
75
+ variant === "transparent" ? "primary" : "secondary"
76
+ }
77
+ />
78
+ ) : (
79
+ <Skeleton className="h-10 md:h-9 w-full rounded-xl" />
80
+ )}
81
+
82
+ <FieldError />
83
+ </TextField>
84
+ </Fieldset.Group>
85
+
86
+ <Fieldset.Actions>
87
+ <Button
88
+ type="submit"
89
+ isPending={isPending}
90
+ isDisabled={!session}
91
+ size="sm"
92
+ >
93
+ {isPending && <Spinner color="current" size="sm" />}
94
+ {localization.settings.saveChanges}
95
+ </Button>
96
+ </Fieldset.Actions>
97
+ </Fieldset>
98
+ </Form>
99
+ </Card.Content>
100
+ </Card>
101
+ </div>
102
+ )
103
+ }
@@ -0,0 +1,123 @@
1
+ import { useAuth, useRevokeSession, useSession } from "@better-auth-ui/react"
2
+ import {
3
+ ArrowRightFromSquare,
4
+ Display,
5
+ Smartphone,
6
+ Xmark
7
+ } from "@gravity-ui/icons"
8
+ import { Button, Chip, Spinner, toast } from "@heroui/react"
9
+ import type { Session } from "better-auth"
10
+ import Bowser from "bowser"
11
+
12
+ function timeAgo(date: Date) {
13
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1000)
14
+ const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" })
15
+
16
+ const UNITS: [Intl.RelativeTimeFormatUnit, number][] = [
17
+ ["year", 31536000],
18
+ ["month", 2592000],
19
+ ["week", 604800],
20
+ ["day", 86400],
21
+ ["hour", 3600],
22
+ ["minute", 60],
23
+ ["second", 1]
24
+ ]
25
+
26
+ for (const [unit, threshold] of UNITS) {
27
+ if (seconds >= threshold) {
28
+ return rtf.format(-Math.floor(seconds / threshold), unit)
29
+ }
30
+ }
31
+
32
+ return rtf.format(0, "second")
33
+ }
34
+
35
+ export type ActiveSessionProps = {
36
+ activeSession: Session
37
+ }
38
+
39
+ /**
40
+ * Render a single active session row with device info and revoke control.
41
+ *
42
+ * Shows the session's browser, OS, IP address, and creation time. The current session is marked
43
+ * and navigates to sign-out on click, while other sessions can be revoked individually.
44
+ *
45
+ * @param session - The session object containing id, token, userAgent, ipAddress, and createdAt
46
+ * @returns A JSX element containing the active session row
47
+ */
48
+ export function ActiveSession({ activeSession }: ActiveSessionProps) {
49
+ const { basePaths, localization, viewPaths, navigate } = useAuth()
50
+ const { data: session } = useSession({ refetchOnMount: false })
51
+
52
+ const { mutate: revokeSession, isPending: isRevoking } = useRevokeSession({
53
+ onError: (error) => toast.danger(error.error?.message || error.message),
54
+ onSuccess: () => toast.success(localization.settings.revokeSessionSuccess)
55
+ })
56
+
57
+ const isCurrentSession = activeSession.token === session?.session.token
58
+ const ua = Bowser.parse(activeSession.userAgent || "")
59
+ const isMobile =
60
+ ua.platform.type === "mobile" || ua.platform.type === "tablet"
61
+
62
+ return (
63
+ <div className="flex items-center gap-3">
64
+ <div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-surface-secondary">
65
+ {isMobile ? (
66
+ <Smartphone className="size-4.5" />
67
+ ) : (
68
+ <Display className="size-4.5" />
69
+ )}
70
+ </div>
71
+
72
+ <div className="flex flex-col min-w-0">
73
+ <span className="text-sm font-medium truncate">
74
+ {ua.browser.name || "Unknown Browser"}
75
+ {ua.os.name ? `, ${ua.os.name}` : ""}
76
+ </span>
77
+
78
+ {isCurrentSession ? (
79
+ <Chip color="accent" size="sm">
80
+ {localization.settings.currentSession}
81
+ </Chip>
82
+ ) : (
83
+ activeSession.createdAt && (
84
+ <span className="text-xs text-muted capitalize">
85
+ {timeAgo(activeSession.createdAt)}
86
+ </span>
87
+ )
88
+ )}
89
+ </div>
90
+
91
+ <Button
92
+ className="ml-auto shrink-0"
93
+ variant="outline"
94
+ size="sm"
95
+ onPress={() =>
96
+ isCurrentSession
97
+ ? navigate({
98
+ to: `${basePaths.auth}/${viewPaths.auth.signOut}`
99
+ })
100
+ : revokeSession(activeSession)
101
+ }
102
+ isPending={isRevoking}
103
+ aria-label={
104
+ isCurrentSession
105
+ ? localization.auth.signOut
106
+ : localization.settings.revokeSession
107
+ }
108
+ >
109
+ {isRevoking ? (
110
+ <Spinner color="current" size="sm" />
111
+ ) : isCurrentSession ? (
112
+ <ArrowRightFromSquare />
113
+ ) : (
114
+ <Xmark />
115
+ )}
116
+
117
+ {isCurrentSession
118
+ ? localization.auth.signOut
119
+ : localization.settings.revoke}
120
+ </Button>
121
+ </div>
122
+ )
123
+ }
@@ -0,0 +1,77 @@
1
+ import { useAuth, useListSessions, useSession } from "@better-auth-ui/react"
2
+ import { Card, type CardProps, cn, Skeleton, toast } from "@heroui/react"
3
+ import { ActiveSession } from "./active-session"
4
+
5
+ export type ActiveSessionsProps = {
6
+ className?: string
7
+ variant?: CardProps["variant"]
8
+ }
9
+
10
+ /**
11
+ * Render a card listing all active sessions for the current user with revoke controls.
12
+ *
13
+ * Shows each session's browser, OS, IP address, and creation time. The current session is marked
14
+ * and navigates to sign-out on click, while other sessions can be revoked individually.
15
+ *
16
+ * @returns A JSX element containing the sessions card
17
+ */
18
+ export function ActiveSessions({
19
+ className,
20
+ variant,
21
+ ...props
22
+ }: ActiveSessionsProps & CardProps) {
23
+ const { localization } = useAuth()
24
+ const { data: session } = useSession()
25
+
26
+ const { data: sessions, isPending } = useListSessions({
27
+ throwOnError: (error) => {
28
+ if (error.error) toast.danger(error.error.message)
29
+ return false
30
+ }
31
+ })
32
+
33
+ const activeSessions = sessions?.toSorted((activeSession) =>
34
+ activeSession.id === session?.session.id ? -1 : 1
35
+ )
36
+
37
+ return (
38
+ <div>
39
+ <h2 className={cn("text-sm font-semibold mb-3")}>
40
+ {localization.settings.activeSessions}
41
+ </h2>
42
+
43
+ <Card className={cn(className)} variant={variant} {...props}>
44
+ <Card.Content className="gap-0">
45
+ {isPending ? (
46
+ <SessionRowSkeleton />
47
+ ) : (
48
+ activeSessions?.map((activeSession, index) => (
49
+ <div key={activeSession.id}>
50
+ {index > 0 && (
51
+ <div className="border-b border-dashed -mx-4 md:-mx-6 my-4" />
52
+ )}
53
+
54
+ <ActiveSession activeSession={activeSession} />
55
+ </div>
56
+ ))
57
+ )}
58
+ </Card.Content>
59
+ </Card>
60
+ </div>
61
+ )
62
+ }
63
+
64
+ function SessionRowSkeleton() {
65
+ return (
66
+ <div className="flex items-center justify-between">
67
+ <div className="flex items-center gap-3">
68
+ <Skeleton className="size-10 rounded-xl" />
69
+
70
+ <div className="flex flex-col gap-1">
71
+ <Skeleton className="h-4 w-32 rounded-lg" />
72
+ <Skeleton className="h-3 w-24 rounded-lg" />
73
+ </div>
74
+ </div>
75
+ </div>
76
+ )
77
+ }