@better-auth-ui/heroui 1.6.8 → 1.6.10

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 (195) hide show
  1. package/dist/components/auth/api-key/api-key.d.ts +5 -1
  2. package/dist/components/auth/api-key/api-keys-empty.d.ts +3 -1
  3. package/dist/components/auth/api-key/api-keys.d.ts +9 -1
  4. package/dist/components/auth/api-key/create-api-key-dialog.d.ts +3 -1
  5. package/dist/components/auth/api-key/delete-api-key-dialog.d.ts +3 -1
  6. package/dist/components/auth/api-key/organization-api-keys.d.ts +14 -0
  7. package/dist/components/auth/delete-user/danger-zone.d.ts +1 -1
  8. package/dist/components/auth/delete-user/{delete-user.d.ts → delete-account.d.ts} +2 -2
  9. package/dist/components/auth/email/organization-invitation-email.d.ts +3 -0
  10. package/dist/components/auth/multi-session/switch-account-submenu-content.d.ts +3 -1
  11. package/dist/components/auth/multi-session/switch-account-submenu-item.d.ts +2 -1
  12. package/dist/components/auth/multi-session/switch-account-submenu.d.ts +2 -1
  13. package/dist/components/auth/organization/change-organization-logo.d.ts +4 -0
  14. package/dist/components/auth/organization/create-organization-dialog.d.ts +13 -0
  15. package/dist/components/auth/organization/delete-organization-dialog.d.ts +7 -0
  16. package/dist/components/auth/organization/delete-organization-skeleton.d.ts +4 -0
  17. package/dist/components/auth/organization/delete-organization.d.ts +5 -0
  18. package/dist/components/auth/organization/invite-member-dialog.d.ts +13 -0
  19. package/dist/components/auth/organization/leave-organization-dialog.d.ts +7 -0
  20. package/dist/components/auth/organization/leave-organization.d.ts +4 -0
  21. package/dist/components/auth/organization/organization-danger-zone.d.ts +16 -0
  22. package/dist/components/auth/organization/organization-invitation-row-skeleton.d.ts +5 -0
  23. package/dist/components/auth/organization/organization-invitation-row.d.ts +5 -0
  24. package/dist/components/auth/organization/organization-invitations-empty.d.ts +8 -0
  25. package/dist/components/auth/organization/organization-invitations.d.ts +9 -0
  26. package/dist/components/auth/organization/organization-logo.d.ts +15 -0
  27. package/dist/components/auth/organization/organization-member-row-skeleton.d.ts +4 -0
  28. package/dist/components/auth/organization/organization-member-row.d.ts +9 -0
  29. package/dist/components/auth/organization/organization-members.d.ts +9 -0
  30. package/dist/components/auth/organization/organization-people.d.ts +10 -0
  31. package/dist/components/auth/organization/organization-profile.d.ts +9 -0
  32. package/dist/components/auth/organization/organization-row.d.ts +8 -0
  33. package/dist/components/auth/organization/organization-settings.d.ts +12 -0
  34. package/dist/components/auth/organization/organization-switcher.d.ts +20 -0
  35. package/dist/components/auth/organization/organization-view-skeleton.d.ts +6 -0
  36. package/dist/components/auth/organization/organization-view.d.ts +15 -0
  37. package/dist/components/auth/organization/organization.d.ts +17 -0
  38. package/dist/components/auth/organization/organizations-empty.d.ts +4 -0
  39. package/dist/components/auth/organization/organizations-settings.d.ts +17 -0
  40. package/dist/components/auth/organization/organizations.d.ts +10 -0
  41. package/dist/components/auth/organization/remove-member-dialog.d.ts +9 -0
  42. package/dist/components/auth/organization/slug-field.d.ts +19 -0
  43. package/dist/components/auth/organization/user-invitation-row-skeleton.d.ts +4 -0
  44. package/dist/components/auth/organization/user-invitation-row.d.ts +10 -0
  45. package/dist/components/auth/organization/user-invitations-empty.d.ts +4 -0
  46. package/dist/components/auth/organization/user-invitations.d.ts +10 -0
  47. package/dist/components/auth/settings/settings.d.ts +1 -1
  48. package/dist/components/auth/user/user-avatar.d.ts +1 -1
  49. package/dist/components/auth/user/user-button.d.ts +4 -5
  50. package/dist/components/auth/user/user-view.d.ts +10 -4
  51. package/dist/email.d.ts +1 -0
  52. package/dist/email.js +211 -8
  53. package/dist/index.js +1682 -22
  54. package/dist/lib/auth/api-key-plugin.d.ts +5 -2
  55. package/dist/lib/auth/auth-plugin.d.ts +6 -2
  56. package/dist/lib/auth/delete-user-plugin.d.ts +4 -4
  57. package/dist/lib/auth/magic-link-plugin.d.ts +2 -2
  58. package/dist/lib/auth/multi-session-plugin.d.ts +2 -2
  59. package/dist/lib/auth/organization-plugin.d.ts +37 -0
  60. package/dist/lib/auth/passkey-plugin.d.ts +2 -2
  61. package/dist/lib/auth/theme-plugin.d.ts +3 -3
  62. package/dist/lib/auth/username-plugin.d.ts +8 -8
  63. package/dist/plugins.d.ts +25 -1
  64. package/dist/plugins.js +2824 -22
  65. package/dist/user-view--47tVtJ3.js +113 -0
  66. package/package.json +23 -18
  67. package/src/components/auth/api-key/api-key.tsx +25 -16
  68. package/src/components/auth/api-key/api-keys-empty.tsx +8 -4
  69. package/src/components/auth/api-key/api-keys.tsx +52 -14
  70. package/src/components/auth/api-key/create-api-key-dialog.tsx +15 -2
  71. package/src/components/auth/api-key/delete-api-key-dialog.tsx +10 -2
  72. package/src/components/auth/api-key/organization-api-keys.tsx +55 -0
  73. package/src/components/auth/delete-user/danger-zone.tsx +3 -3
  74. package/src/components/auth/delete-user/{delete-user.tsx → delete-account.tsx} +9 -9
  75. package/src/components/auth/email/organization-invitation-email.tsx +38 -0
  76. package/src/components/auth/error-toaster.tsx +28 -10
  77. package/src/components/auth/forgot-password.tsx +10 -2
  78. package/src/components/auth/multi-session/manage-accounts.tsx +1 -1
  79. package/src/components/auth/multi-session/switch-account-submenu-content.tsx +7 -2
  80. package/src/components/auth/multi-session/switch-account-submenu-item.tsx +4 -2
  81. package/src/components/auth/multi-session/switch-account-submenu.tsx +8 -4
  82. package/src/components/auth/organization/change-organization-logo.tsx +171 -0
  83. package/src/components/auth/organization/create-organization-dialog.tsx +133 -0
  84. package/src/components/auth/organization/delete-organization-dialog.tsx +97 -0
  85. package/src/components/auth/organization/delete-organization-skeleton.tsx +17 -0
  86. package/src/components/auth/organization/delete-organization.tsx +77 -0
  87. package/src/components/auth/organization/invite-member-dialog.tsx +177 -0
  88. package/src/components/auth/organization/leave-organization-dialog.tsx +95 -0
  89. package/src/components/auth/organization/leave-organization.tsx +59 -0
  90. package/src/components/auth/organization/organization-danger-zone.tsx +68 -0
  91. package/src/components/auth/organization/organization-invitation-row-skeleton.tsx +29 -0
  92. package/src/components/auth/organization/organization-invitation-row.tsx +97 -0
  93. package/src/components/auth/organization/organization-invitations-empty.tsx +39 -0
  94. package/src/components/auth/organization/organization-invitations.tsx +351 -0
  95. package/src/components/auth/organization/organization-logo.tsx +64 -0
  96. package/src/components/auth/organization/organization-member-row-skeleton.tsx +24 -0
  97. package/src/components/auth/organization/organization-member-row.tsx +157 -0
  98. package/src/components/auth/organization/organization-members.tsx +284 -0
  99. package/src/components/auth/organization/organization-people.tsx +26 -0
  100. package/src/components/auth/organization/organization-profile.tsx +141 -0
  101. package/src/components/auth/organization/organization-row.tsx +66 -0
  102. package/src/components/auth/organization/organization-settings.tsx +38 -0
  103. package/src/components/auth/organization/organization-switcher.tsx +233 -0
  104. package/src/components/auth/organization/organization-view-skeleton.tsx +36 -0
  105. package/src/components/auth/organization/organization-view.tsx +109 -0
  106. package/src/components/auth/organization/organization.tsx +162 -0
  107. package/src/components/auth/organization/organizations-empty.tsx +35 -0
  108. package/src/components/auth/organization/organizations-settings.tsx +36 -0
  109. package/src/components/auth/organization/organizations.tsx +80 -0
  110. package/src/components/auth/organization/remove-member-dialog.tsx +95 -0
  111. package/src/components/auth/organization/slug-field.tsx +111 -0
  112. package/src/components/auth/organization/user-invitation-row-skeleton.tsx +17 -0
  113. package/src/components/auth/organization/user-invitation-row.tsx +90 -0
  114. package/src/components/auth/organization/user-invitations-empty.tsx +30 -0
  115. package/src/components/auth/organization/user-invitations.tsx +59 -0
  116. package/src/components/auth/settings/account/change-email.tsx +1 -1
  117. package/src/components/auth/settings/account/user-profile.tsx +5 -5
  118. package/src/components/auth/settings/security/active-sessions.tsx +6 -4
  119. package/src/components/auth/settings/security/linked-accounts.tsx +2 -2
  120. package/src/components/auth/settings/settings.tsx +77 -14
  121. package/src/components/auth/user/user-avatar.tsx +4 -1
  122. package/src/components/auth/user/user-button.tsx +14 -11
  123. package/src/components/auth/user/user-view.tsx +32 -12
  124. package/src/email.ts +1 -0
  125. package/src/lib/auth/api-key-plugin.ts +10 -4
  126. package/src/lib/auth/auth-plugin.ts +6 -1
  127. package/src/lib/auth/organization-plugin.tsx +32 -0
  128. package/src/plugins.ts +25 -1
  129. package/dist/components/auth/additional-field.js +0 -270
  130. package/dist/components/auth/api-key/api-key-skeleton.js +0 -21
  131. package/dist/components/auth/api-key/api-key.js +0 -55
  132. package/dist/components/auth/api-key/api-keys-empty.js +0 -35
  133. package/dist/components/auth/api-key/api-keys.js +0 -41
  134. package/dist/components/auth/api-key/create-api-key-dialog.js +0 -74
  135. package/dist/components/auth/api-key/delete-api-key-dialog.js +0 -51
  136. package/dist/components/auth/api-key/new-api-key-dialog.js +0 -60
  137. package/dist/components/auth/auth-provider.js +0 -17
  138. package/dist/components/auth/auth.js +0 -63
  139. package/dist/components/auth/delete-user/danger-zone.js +0 -18
  140. package/dist/components/auth/delete-user/delete-user.js +0 -95
  141. package/dist/components/auth/email/email-changed-email.js +0 -30
  142. package/dist/components/auth/email/email-verification-email.js +0 -30
  143. package/dist/components/auth/email/magic-link-email.js +0 -30
  144. package/dist/components/auth/email/new-device-email.js +0 -30
  145. package/dist/components/auth/email/otp-email.js +0 -29
  146. package/dist/components/auth/email/password-changed-email.js +0 -30
  147. package/dist/components/auth/email/reset-password-email.js +0 -30
  148. package/dist/components/auth/error-toaster.js +0 -17
  149. package/dist/components/auth/field-separator.js +0 -18
  150. package/dist/components/auth/forgot-password.js +0 -89
  151. package/dist/components/auth/magic-link/magic-link-button.js +0 -20
  152. package/dist/components/auth/magic-link/magic-link.js +0 -87
  153. package/dist/components/auth/multi-session/manage-account.js +0 -49
  154. package/dist/components/auth/multi-session/manage-accounts.js +0 -34
  155. package/dist/components/auth/multi-session/switch-account-submenu-content.js +0 -28
  156. package/dist/components/auth/multi-session/switch-account-submenu-item.js +0 -20
  157. package/dist/components/auth/multi-session/switch-account-submenu.js +0 -24
  158. package/dist/components/auth/passkey/add-passkey-dialog.js +0 -65
  159. package/dist/components/auth/passkey/delete-passkey-dialog.js +0 -47
  160. package/dist/components/auth/passkey/passkey-button.js +0 -24
  161. package/dist/components/auth/passkey/passkey-skeleton.js +0 -17
  162. package/dist/components/auth/passkey/passkey.js +0 -48
  163. package/dist/components/auth/passkey/passkeys-empty.js +0 -35
  164. package/dist/components/auth/passkey/passkeys.js +0 -42
  165. package/dist/components/auth/provider-button.js +0 -25
  166. package/dist/components/auth/provider-buttons.js +0 -19
  167. package/dist/components/auth/reset-password.js +0 -147
  168. package/dist/components/auth/settings/account/account-settings.js +0 -20
  169. package/dist/components/auth/settings/account/change-avatar.js +0 -89
  170. package/dist/components/auth/settings/account/change-email.js +0 -54
  171. package/dist/components/auth/settings/account/user-profile.js +0 -86
  172. package/dist/components/auth/settings/security/active-session.js +0 -59
  173. package/dist/components/auth/settings/security/active-sessions.js +0 -34
  174. package/dist/components/auth/settings/security/change-password.js +0 -189
  175. package/dist/components/auth/settings/security/linked-account.js +0 -56
  176. package/dist/components/auth/settings/security/linked-accounts.js +0 -45
  177. package/dist/components/auth/settings/security/security-settings.js +0 -22
  178. package/dist/components/auth/settings/settings.js +0 -45
  179. package/dist/components/auth/sign-in.js +0 -133
  180. package/dist/components/auth/sign-out.js +0 -27
  181. package/dist/components/auth/sign-up.js +0 -212
  182. package/dist/components/auth/theme/appearance.js +0 -72
  183. package/dist/components/auth/theme/theme-toggle-item.js +0 -62
  184. package/dist/components/auth/user/user-avatar.js +0 -29
  185. package/dist/components/auth/user/user-button.js +0 -74
  186. package/dist/components/auth/user/user-view.js +0 -37
  187. package/dist/components/auth/username/sign-in-username.js +0 -148
  188. package/dist/components/auth/username/username-field.js +0 -53
  189. package/dist/lib/auth/api-key-plugin.js +0 -10
  190. package/dist/lib/auth/delete-user-plugin.js +0 -10
  191. package/dist/lib/auth/magic-link-plugin.js +0 -13
  192. package/dist/lib/auth/multi-session-plugin.js +0 -12
  193. package/dist/lib/auth/passkey-plugin.js +0 -12
  194. package/dist/lib/auth/theme-plugin.js +0 -23
  195. package/dist/lib/auth/username-plugin.js +0 -18
@@ -0,0 +1,95 @@
1
+ import {
2
+ type OrganizationAuthClient,
3
+ useAuth,
4
+ useAuthPlugin,
5
+ useRemoveMember
6
+ } from "@better-auth-ui/react"
7
+ import { TrashBin } from "@gravity-ui/icons"
8
+ import { AlertDialog, Button, Card, Chip, Spinner, toast } from "@heroui/react"
9
+ import type { Member, User } from "better-auth/client"
10
+
11
+ import { organizationPlugin } from "../../../lib/auth/organization-plugin"
12
+ import { UserView } from "../user/user-view"
13
+
14
+ export type RemoveMemberDialogProps = {
15
+ isOpen: boolean
16
+ onOpenChange: (open: boolean) => void
17
+ member: Member & { user: Partial<User> }
18
+ }
19
+
20
+ export function RemoveMemberDialog({
21
+ isOpen,
22
+ onOpenChange,
23
+ member
24
+ }: RemoveMemberDialogProps) {
25
+ const { authClient, localization } = useAuth()
26
+ const { localization: organizationLocalization, roles } =
27
+ useAuthPlugin(organizationPlugin)
28
+
29
+ const { mutate: removeMember, isPending } = useRemoveMember(
30
+ authClient as OrganizationAuthClient,
31
+ {
32
+ onSuccess: () => {
33
+ onOpenChange(false)
34
+ toast.success(organizationLocalization.memberRemoved)
35
+ }
36
+ }
37
+ )
38
+
39
+ return (
40
+ <AlertDialog.Backdrop isOpen={isOpen} onOpenChange={onOpenChange}>
41
+ <AlertDialog.Container>
42
+ <AlertDialog.Dialog>
43
+ <AlertDialog.CloseTrigger />
44
+
45
+ <AlertDialog.Header>
46
+ <AlertDialog.Icon status="danger">
47
+ <TrashBin />
48
+ </AlertDialog.Icon>
49
+
50
+ <AlertDialog.Heading>
51
+ {organizationLocalization.removeMember}
52
+ </AlertDialog.Heading>
53
+ </AlertDialog.Header>
54
+
55
+ <AlertDialog.Body className="flex flex-col gap-4 overflow-visible">
56
+ <p className="text-muted text-sm">
57
+ {organizationLocalization.removeMemberWarning}
58
+ </p>
59
+
60
+ <Card variant="secondary">
61
+ <Card.Content className="justify-between flex-row items-center gap-2">
62
+ <UserView user={member.user} />
63
+
64
+ <Chip size="sm" variant="tertiary">
65
+ <Chip.Label>{roles?.[member.role] ?? member.role}</Chip.Label>
66
+ </Chip>
67
+ </Card.Content>
68
+ </Card>
69
+ </AlertDialog.Body>
70
+
71
+ <AlertDialog.Footer>
72
+ <Button slot="close" variant="tertiary" isDisabled={isPending}>
73
+ {localization.settings.cancel}
74
+ </Button>
75
+
76
+ <Button
77
+ variant="danger"
78
+ isPending={isPending}
79
+ onPress={() =>
80
+ removeMember({
81
+ memberIdOrEmail: member.id,
82
+ organizationId: member.organizationId
83
+ })
84
+ }
85
+ >
86
+ {isPending && <Spinner color="current" size="sm" />}
87
+
88
+ {organizationLocalization.removeMember}
89
+ </Button>
90
+ </AlertDialog.Footer>
91
+ </AlertDialog.Dialog>
92
+ </AlertDialog.Container>
93
+ </AlertDialog.Backdrop>
94
+ )
95
+ }
@@ -0,0 +1,111 @@
1
+ import {
2
+ type OrganizationAuthClient,
3
+ useAuth,
4
+ useAuthPlugin,
5
+ useCheckSlug
6
+ } from "@better-auth-ui/react"
7
+ import { Check, Xmark } from "@gravity-ui/icons"
8
+ import {
9
+ FieldError,
10
+ InputGroup,
11
+ type InputProps,
12
+ Label,
13
+ Spinner,
14
+ TextField,
15
+ type TextFieldProps
16
+ } from "@heroui/react"
17
+ import { useDebouncer } from "@tanstack/react-pacer"
18
+ import { useEffect } from "react"
19
+
20
+ import { organizationPlugin } from "../../../lib/auth/organization-plugin"
21
+
22
+ /** Props for the {@link SlugField} component. */
23
+ export type SlugFieldProps = {
24
+ value: string
25
+ onChange: (value: string) => void
26
+ currentSlug?: string
27
+ isDisabled?: boolean
28
+ variant?: InputProps["variant"]
29
+ }
30
+
31
+ /**
32
+ * Sanitize a slug value so it only contains lowercase alphanumeric characters
33
+ * and dashes. Runs of disallowed characters are collapsed to a single dash, but
34
+ * leading/trailing dashes are preserved while the user is still typing.
35
+ */
36
+ export function sanitizeSlug(value: string) {
37
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-")
38
+ }
39
+
40
+ /**
41
+ * Organization slug field with debounced availability checking.
42
+ */
43
+ export function SlugField({
44
+ value,
45
+ onChange,
46
+ currentSlug,
47
+ variant,
48
+ ...props
49
+ }: SlugFieldProps & TextFieldProps) {
50
+ const { authClient } = useAuth()
51
+ const { localization, checkSlug: checkSlugEnabled } =
52
+ useAuthPlugin(organizationPlugin)
53
+
54
+ const {
55
+ mutate: checkSlug,
56
+ data: checkSlugData,
57
+ error: checkSlugError,
58
+ reset: resetCheckSlug
59
+ } = useCheckSlug(authClient as OrganizationAuthClient)
60
+
61
+ const debouncer = useDebouncer(
62
+ (value) => {
63
+ if (!checkSlugEnabled || !value.trim() || value.trim() === currentSlug)
64
+ return
65
+
66
+ checkSlug({ slug: value.trim() })
67
+ },
68
+ { wait: 500 }
69
+ )
70
+
71
+ useEffect(() => {
72
+ if (!checkSlugEnabled) return
73
+
74
+ resetCheckSlug()
75
+ debouncer.maybeExecute(value)
76
+ }, [checkSlugEnabled, value, debouncer.maybeExecute, resetCheckSlug])
77
+
78
+ const handleChange = (next: string) => {
79
+ onChange(sanitizeSlug(next))
80
+ }
81
+
82
+ return (
83
+ <TextField
84
+ id="slug"
85
+ name="slug"
86
+ {...props}
87
+ value={value}
88
+ onChange={handleChange}
89
+ >
90
+ <Label>{localization.slug}</Label>
91
+
92
+ <InputGroup variant={variant}>
93
+ <InputGroup.Input placeholder={localization.slugPlaceholder} required />
94
+
95
+ {checkSlugEnabled && !!value.trim() && value.trim() !== currentSlug && (
96
+ <InputGroup.Suffix className="px-2">
97
+ {checkSlugData?.status ? (
98
+ <Check className="text-success" />
99
+ ) : checkSlugError ? (
100
+ <Xmark className="text-danger" />
101
+ ) : (
102
+ <Spinner size="sm" color="current" />
103
+ )}
104
+ </InputGroup.Suffix>
105
+ )}
106
+ </InputGroup>
107
+
108
+ <FieldError />
109
+ </TextField>
110
+ )
111
+ }
@@ -0,0 +1,17 @@
1
+ import { Skeleton } from "@heroui/react"
2
+
3
+ /**
4
+ * Placeholder row matching {@link UserInvitationRow} while invitations load.
5
+ */
6
+ export function UserInvitationRowSkeleton() {
7
+ return (
8
+ <div className="flex items-center gap-3">
9
+ <Skeleton className="size-10 shrink-0 rounded-xl" />
10
+
11
+ <div className="flex flex-col gap-1">
12
+ <Skeleton className="h-4 w-40 rounded-lg" />
13
+ <Skeleton className="h-3 w-28 rounded-lg" />
14
+ </div>
15
+ </div>
16
+ )
17
+ }
@@ -0,0 +1,90 @@
1
+ import {
2
+ type OrganizationAuthClient,
3
+ useAcceptInvitation,
4
+ useAuth,
5
+ useAuthPlugin,
6
+ useRejectInvitation
7
+ } from "@better-auth-ui/react"
8
+ import { Check, Clock, Xmark } from "@gravity-ui/icons"
9
+ import { Button, Chip, Spinner } from "@heroui/react"
10
+ import type { Invitation } from "better-auth/client"
11
+
12
+ import { organizationPlugin } from "../../../lib/auth/organization-plugin"
13
+
14
+ export type UserInvitationRowProps = {
15
+ invitation: Invitation & { organizationName?: string }
16
+ }
17
+
18
+ /**
19
+ * Single invitation row with accept/reject actions for the current user.
20
+ */
21
+ export function UserInvitationRow({ invitation }: UserInvitationRowProps) {
22
+ const { authClient } = useAuth()
23
+ const { localization: organizationLocalization, roles } =
24
+ useAuthPlugin(organizationPlugin)
25
+
26
+ const { mutate: acceptInvitation, isPending: isAccepting } =
27
+ useAcceptInvitation(authClient as OrganizationAuthClient)
28
+
29
+ const { mutate: rejectInvitation, isPending: isRejecting } =
30
+ useRejectInvitation(authClient as OrganizationAuthClient)
31
+
32
+ return (
33
+ <div className="flex items-center gap-3">
34
+ <div className="flex size-10 shrink-0 items-center justify-center rounded-xl bg-surface-secondary">
35
+ <Clock className="size-4.5" />
36
+ </div>
37
+
38
+ <div className="flex flex-col">
39
+ <div className="flex items-center gap-1.5">
40
+ <span className="truncate text-sm font-medium leading-tight">
41
+ {invitation.organizationName}
42
+ </span>
43
+
44
+ <Chip size="sm">{roles?.[invitation.role] ?? invitation.role}</Chip>
45
+ </div>
46
+
47
+ <span className="truncate text-muted text-xs">
48
+ {new Date(invitation.createdAt).toLocaleString(undefined, {
49
+ dateStyle: "medium",
50
+ timeStyle: "short"
51
+ })}
52
+ </span>
53
+ </div>
54
+
55
+ <div className="ml-auto flex shrink-0 items-center gap-2">
56
+ <Button
57
+ variant="outline"
58
+ size="sm"
59
+ isPending={isAccepting}
60
+ isDisabled={isRejecting}
61
+ onPress={() =>
62
+ acceptInvitation({
63
+ invitationId: invitation.id
64
+ })
65
+ }
66
+ >
67
+ {isAccepting ? <Spinner color="current" size="sm" /> : <Check />}
68
+
69
+ {organizationLocalization.accept}
70
+ </Button>
71
+
72
+ <Button
73
+ variant="danger-soft"
74
+ size="sm"
75
+ isIconOnly
76
+ isPending={isRejecting}
77
+ isDisabled={isAccepting}
78
+ onPress={() =>
79
+ rejectInvitation({
80
+ invitationId: invitation.id
81
+ })
82
+ }
83
+ aria-label={organizationLocalization.rejectInvitation}
84
+ >
85
+ {isRejecting ? <Spinner color="current" size="sm" /> : <Xmark />}
86
+ </Button>
87
+ </div>
88
+ </div>
89
+ )
90
+ }
@@ -0,0 +1,30 @@
1
+ import { useAuthPlugin } from "@better-auth-ui/react"
2
+ import { PaperPlane } from "@gravity-ui/icons"
3
+ import { EmptyState } from "@heroui/react"
4
+ import { organizationPlugin } from "../../../lib/auth/organization-plugin"
5
+
6
+ /**
7
+ * Empty state for `UserInvitations`
8
+ */
9
+ export function UserInvitationsEmpty() {
10
+ const { localization: organizationLocalization } =
11
+ useAuthPlugin(organizationPlugin)
12
+
13
+ return (
14
+ <EmptyState className="flex flex-col items-center gap-4 text-center">
15
+ <div className="flex size-12 items-center justify-center rounded-full bg-surface-secondary">
16
+ <PaperPlane className="size-5" />
17
+ </div>
18
+
19
+ <div className="flex flex-col gap-2">
20
+ <p className="text-sm font-semibold text-foreground">
21
+ {organizationLocalization.noInvitations}
22
+ </p>
23
+
24
+ <span className="text-sm text-muted">
25
+ {organizationLocalization.userInvitationsEmptyDescription}
26
+ </span>
27
+ </div>
28
+ </EmptyState>
29
+ )
30
+ }
@@ -0,0 +1,59 @@
1
+ import {
2
+ type OrganizationAuthClient,
3
+ useAuth,
4
+ useAuthPlugin,
5
+ useListUserInvitations
6
+ } from "@better-auth-ui/react"
7
+ import { Card, type CardProps } from "@heroui/react"
8
+
9
+ import { organizationPlugin } from "../../../lib/auth/organization-plugin"
10
+ import { UserInvitationRow } from "./user-invitation-row"
11
+ import { UserInvitationRowSkeleton } from "./user-invitation-row-skeleton"
12
+ import { UserInvitationsEmpty } from "./user-invitations-empty"
13
+
14
+ export type UserInvitationsProps = {
15
+ variant?: CardProps["variant"]
16
+ }
17
+
18
+ /**
19
+ * Organization invitations for the signed-in user (from
20
+ * {@link useListUserInvitations}). Always renders the section card; uses
21
+ * {@link UserInvitationsEmpty} when there are no pending invitations.
22
+ */
23
+ export function UserInvitations({ variant }: UserInvitationsProps) {
24
+ const { authClient } = useAuth()
25
+ const { localization: organizationLocalization } =
26
+ useAuthPlugin(organizationPlugin)
27
+
28
+ const { data: invitations, isPending } = useListUserInvitations(
29
+ authClient as OrganizationAuthClient
30
+ )
31
+
32
+ return (
33
+ <div className="flex flex-col gap-3">
34
+ <h2 className="truncate text-sm font-semibold">
35
+ {organizationLocalization.invitations}
36
+ </h2>
37
+
38
+ <Card variant={variant}>
39
+ <Card.Content>
40
+ {isPending ? (
41
+ <UserInvitationRowSkeleton />
42
+ ) : !invitations?.length ? (
43
+ <UserInvitationsEmpty />
44
+ ) : (
45
+ invitations?.map((invitation, index) => (
46
+ <div key={invitation.id}>
47
+ {index > 0 && (
48
+ <div className="border-b border-dashed -mx-4 my-4" />
49
+ )}
50
+
51
+ <UserInvitationRow invitation={invitation} />
52
+ </div>
53
+ ))
54
+ )}
55
+ </Card.Content>
56
+ </Card>
57
+ </div>
58
+ )
59
+ }
@@ -64,7 +64,7 @@ export function ChangeEmail({
64
64
  <Fieldset className="w-full gap-4">
65
65
  <Fieldset.Group>
66
66
  <TextField
67
- key={session?.user.email}
67
+ key={`${session?.user.id}-${session?.user.email}-email`}
68
68
  name="email"
69
69
  type="email"
70
70
  defaultValue={session?.user.email}
@@ -81,7 +81,7 @@ export function UserProfile({
81
81
  return (
82
82
  <div>
83
83
  <h2 className={cn("text-sm font-semibold mb-3")}>
84
- {localization.settings.profile}
84
+ {localization.settings.userProfile}
85
85
  </h2>
86
86
 
87
87
  <Card className={cn("p-4 gap-4", className)} variant={variant} {...props}>
@@ -90,7 +90,7 @@ export function UserProfile({
90
90
  <ChangeAvatar />
91
91
 
92
92
  <TextField
93
- key={session?.user?.name}
93
+ key={`${session?.user?.id}-${session?.user?.name}-name`}
94
94
  name="name"
95
95
  defaultValue={session?.user.name}
96
96
  isDisabled={isPending || !session}
@@ -131,9 +131,9 @@ export function UserProfile({
131
131
  field.name
132
132
  ]
133
133
 
134
- // Re-mount when the session value loads so the field's
135
- // uncontrolled `defaultValue` reflects the latest data.
136
- const key = `${field.name}:${
134
+ // Re-mount when the user or the session value changes so the
135
+ // field's uncontrolled `defaultValue` reflects the latest data.
136
+ const key = `${session.user.id}-${field.name}-${
137
137
  value instanceof Date
138
138
  ? value.toISOString()
139
139
  : String(value ?? "")
@@ -25,9 +25,11 @@ export function ActiveSessions({
25
25
 
26
26
  const { data: sessions, isPending } = useListSessions(authClient)
27
27
 
28
- const activeSessions = sessions?.toSorted((activeSession) =>
29
- activeSession.id === session?.session.id ? -1 : 1
30
- )
28
+ const activeSessions =
29
+ sessions &&
30
+ [...sessions].sort((a, b) =>
31
+ a.id === session?.session.id ? -1 : b.id === session?.session.id ? 1 : 0
32
+ )
31
33
 
32
34
  return (
33
35
  <div>
@@ -43,7 +45,7 @@ export function ActiveSessions({
43
45
  activeSessions?.map((activeSession, index) => (
44
46
  <div key={activeSession.id}>
45
47
  {index > 0 && (
46
- <div className="border-b border-dashed -mx-4 md:-mx-6 my-4" />
48
+ <div className="border-b border-dashed -mx-4 my-4" />
47
49
  )}
48
50
 
49
51
  <ActiveSession activeSession={activeSession} />
@@ -54,7 +54,7 @@ export function LinkedAccounts({
54
54
  ? socialProviders?.map((provider, index) => (
55
55
  <div key={provider}>
56
56
  {index > 0 && (
57
- <div className="border-b border-dashed -mx-4 md:-mx-6 my-4" />
57
+ <div className="border-b border-dashed -mx-4 my-4" />
58
58
  )}
59
59
  <AccountRowSkeleton />
60
60
  </div>
@@ -62,7 +62,7 @@ export function LinkedAccounts({
62
62
  : allRows.map((row, index) => (
63
63
  <div key={row.key}>
64
64
  {index > 0 && (
65
- <div className="border-b border-dashed -mx-4 md:-mx-6 my-4" />
65
+ <div className="border-b border-dashed -mx-4 my-4" />
66
66
  )}
67
67
 
68
68
  <LinkedAccount
@@ -1,8 +1,8 @@
1
1
  import type { SettingsView } from "@better-auth-ui/core"
2
2
  import { useAuth, useAuthenticate } from "@better-auth-ui/react"
3
+ import { Person, Shield } from "@gravity-ui/icons"
3
4
  import { type CardProps, cn, Tabs } from "@heroui/react"
4
5
  import { type ComponentProps, useMemo } from "react"
5
-
6
6
  import { AccountSettings } from "./account/account-settings"
7
7
  import { SecuritySettings } from "./security/security-settings"
8
8
 
@@ -25,7 +25,7 @@ export type SettingsProps = {
25
25
  * @param variant - Card variant forwarded to each settings section card
26
26
  * @returns A DOM tree containing the responsive settings tabs and the currently selected settings panel
27
27
  *
28
- * @throws Error if neither `view` nor `path` is provided
28
+ * @throws Error if neither `view` nor `path` is provided, or if `path` does not match any settings view
29
29
  */
30
30
  export function Settings({
31
31
  className,
@@ -35,22 +35,39 @@ export function Settings({
35
35
  view,
36
36
  ...props
37
37
  }: SettingsProps & ComponentProps<"div">) {
38
- const { authClient, basePaths, localization, viewPaths } = useAuth()
39
- useAuthenticate(authClient)
40
-
41
38
  if (!view && !path) {
42
39
  throw new Error("[Better Auth UI] Either `view` or `path` must be provided")
43
40
  }
44
41
 
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
- )
42
+ const { authClient, basePaths, localization, viewPaths, plugins } = useAuth()
43
+ useAuthenticate(authClient)
44
+
45
+ // Built-ins first, then plugin segments
46
+ const currentView = useMemo(() => {
47
+ if (view) return view
48
+ if (!path) return undefined
52
49
 
53
- const currentView = view || (path ? settingsPathViews[path] : undefined)
50
+ const match = [
51
+ viewPaths.settings,
52
+ ...plugins.map((plugin) => plugin.viewPaths?.settings)
53
+ ]
54
+ .flatMap((source) => Object.entries(source ?? {}))
55
+ .find(([, segment]) => segment === path)
56
+
57
+ return match?.[0] as SettingsView | undefined
58
+ }, [view, path, viewPaths.settings, plugins])
59
+
60
+ if (!currentView) {
61
+ const validPaths = [
62
+ viewPaths.settings,
63
+ ...plugins.map((plugin) => plugin.viewPaths?.settings)
64
+ ]
65
+ .flatMap((source) => Object.values(source ?? {}))
66
+ .join(", ")
67
+ throw new Error(
68
+ `[Better Auth UI] Unknown settings path "${path}". Valid paths are: ${validPaths}`
69
+ )
70
+ }
54
71
 
55
72
  return (
56
73
  <Tabs
@@ -62,12 +79,18 @@ export function Settings({
62
79
  <Tabs.ListContainer>
63
80
  <Tabs.List
64
81
  aria-label={localization.settings.settings}
65
- className="overflow-auto w-fit"
82
+ className="max-w-fit overflow-x-auto [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
66
83
  >
67
84
  <Tabs.Tab
68
85
  id="account"
69
86
  href={`${basePaths.settings}/${viewPaths.settings.account}`}
87
+ className="gap-2"
88
+ onPress={(e) =>
89
+ e.target.scrollIntoView({ behavior: "smooth", inline: "center" })
90
+ }
70
91
  >
92
+ <Person className="text-muted" />
93
+
71
94
  {localization.settings.account}
72
95
 
73
96
  <Tabs.Indicator />
@@ -76,11 +99,39 @@ export function Settings({
76
99
  <Tabs.Tab
77
100
  id="security"
78
101
  href={`${basePaths.settings}/${viewPaths.settings.security}`}
102
+ className="gap-2"
103
+ onPress={(e) =>
104
+ e.target.scrollIntoView({ behavior: "smooth", inline: "center" })
105
+ }
79
106
  >
107
+ <Shield className="text-muted" />
108
+
80
109
  {localization.settings.security}
81
110
 
82
111
  <Tabs.Indicator />
83
112
  </Tabs.Tab>
113
+
114
+ {plugins.flatMap(
115
+ (plugin) =>
116
+ plugin.settingsTabs?.map((settingsTab, index) => (
117
+ <Tabs.Tab
118
+ key={`${plugin.id}-${index.toString()}`}
119
+ id={settingsTab.view}
120
+ href={`${basePaths.settings}/${plugin.viewPaths?.settings?.[settingsTab.view]}`}
121
+ className="gap-2"
122
+ onPress={(e) =>
123
+ e.target.scrollIntoView({
124
+ behavior: "smooth",
125
+ inline: "center"
126
+ })
127
+ }
128
+ >
129
+ {settingsTab.label}
130
+
131
+ <Tabs.Indicator />
132
+ </Tabs.Tab>
133
+ )) ?? []
134
+ )}
84
135
  </Tabs.List>
85
136
  </Tabs.ListContainer>
86
137
 
@@ -91,6 +142,18 @@ export function Settings({
91
142
  <Tabs.Panel id="security" className="px-0">
92
143
  <SecuritySettings variant={variant} />
93
144
  </Tabs.Panel>
145
+
146
+ {plugins.flatMap((plugin) =>
147
+ plugin.settingsTabs?.map((settingsTab, index) => (
148
+ <Tabs.Panel
149
+ key={`${plugin.id}-${index.toString()}`}
150
+ id={settingsTab.view}
151
+ className="px-0"
152
+ >
153
+ <settingsTab.component variant={variant} />
154
+ </Tabs.Panel>
155
+ ))
156
+ )}
94
157
  </Tabs>
95
158
  )
96
159
  }
@@ -13,7 +13,10 @@ export type UserAvatarProps = {
13
13
  fallback?: ReactNode
14
14
  isPending?: boolean
15
15
  /** @remarks `User` */
16
- user?: User & { username?: string | null; displayUsername?: string | null }
16
+ user?: Partial<User> & {
17
+ username?: string | null
18
+ displayUsername?: string | null
19
+ }
17
20
  size?: AvatarProps["size"]
18
21
  }
19
22