@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
@@ -1,5 +1,10 @@
1
+ import { authMutationKeys, authQueryKeys } from "@better-auth-ui/core"
1
2
  import { toast } from "@heroui/react"
2
- import { useQueryClient } from "@tanstack/react-query"
3
+ import {
4
+ matchMutation,
5
+ matchQuery,
6
+ useQueryClient
7
+ } from "@tanstack/react-query"
3
8
  import type { BetterFetchError } from "better-auth/react"
4
9
  import { useEffect } from "react"
5
10
 
@@ -7,18 +12,31 @@ export function ErrorToaster() {
7
12
  const queryClient = useQueryClient()
8
13
 
9
14
  useEffect(() => {
10
- queryClient.getQueryCache().config.onError = (error) => {
11
- const err = error as BetterFetchError
15
+ const unsubscribeQuery = queryClient.getQueryCache().subscribe((event) => {
16
+ if (event.type !== "updated" || event.action.type !== "error") return
17
+ if (!matchQuery({ queryKey: authQueryKeys.all }, event.query)) return
18
+
19
+ const err = event.action.error as BetterFetchError
12
20
  if (err?.error) toast.danger(err.error.message)
13
- }
21
+ })
14
22
 
15
- queryClient.setMutationDefaults([], {
16
- onError: (error) => {
17
- toast.danger(
18
- (error as BetterFetchError)?.error?.message || error.message
23
+ const unsubscribeMutation = queryClient
24
+ .getMutationCache()
25
+ .subscribe((event) => {
26
+ if (event.type !== "updated" || event.action.type !== "error") return
27
+ if (
28
+ !matchMutation({ mutationKey: authMutationKeys.all }, event.mutation)
19
29
  )
20
- }
21
- })
30
+ return
31
+
32
+ const err = event.action.error as BetterFetchError
33
+ toast.danger(err.error?.message || err.message)
34
+ })
35
+
36
+ return () => {
37
+ unsubscribeQuery()
38
+ unsubscribeMutation()
39
+ }
22
40
  }, [queryClient])
23
41
 
24
42
  return null
@@ -39,8 +39,15 @@ export function ForgotPassword({
39
39
  variant,
40
40
  ...props
41
41
  }: ForgotPasswordProps & Omit<CardProps, "children">) {
42
- const { authClient, basePaths, localization, viewPaths, navigate, plugins } =
43
- useAuth()
42
+ const {
43
+ authClient,
44
+ baseURL,
45
+ basePaths,
46
+ localization,
47
+ viewPaths,
48
+ navigate,
49
+ plugins
50
+ } = useAuth()
44
51
 
45
52
  const { fetchOptions, resetFetchOptions } = useFetchOptions()
46
53
 
@@ -64,6 +71,7 @@ export function ForgotPassword({
64
71
  const formData = new FormData(e.currentTarget)
65
72
  requestPasswordReset({
66
73
  email: formData.get("email") as string,
74
+ redirectTo: `${baseURL}${basePaths.auth}/${viewPaths.auth.resetPassword}`,
67
75
  fetchOptions
68
76
  })
69
77
  }
@@ -66,7 +66,7 @@ export function ManageAccounts({
66
66
  {allRows.map((row, index) => (
67
67
  <div key={row.key}>
68
68
  {index > 0 && (
69
- <div className="border-b border-dashed -mx-4 md:-mx-6 my-4" />
69
+ <div className="border-b border-dashed -mx-4 my-4" />
70
70
  )}
71
71
 
72
72
  <ManageAccount
@@ -21,7 +21,11 @@ import { SwitchAccountSubmenuItem } from "./switch-account-submenu-item"
21
21
  *
22
22
  * @returns The switch account submenu content as a JSX element
23
23
  */
24
- export function SwitchAccountSubmenuContent() {
24
+ export function SwitchAccountSubmenuContent({
25
+ hideSubtitle
26
+ }: {
27
+ hideSubtitle?: boolean
28
+ }) {
25
29
  const { authClient, basePaths, viewPaths } = useAuth()
26
30
  const { localization: multiSessionLocalization } =
27
31
  useAuthPlugin(multiSessionPlugin)
@@ -34,7 +38,7 @@ export function SwitchAccountSubmenuContent() {
34
38
  <Dropdown.Popover className="min-w-40 md:min-w-56 max-w-[48svw]">
35
39
  <Dropdown.Menu>
36
40
  <Dropdown.Item className="px-2">
37
- <UserView isPending={isPending} />
41
+ <UserView isPending={isPending} hideSubtitle={hideSubtitle} />
38
42
 
39
43
  {!isPending && <Check className="ml-auto" />}
40
44
  </Dropdown.Item>
@@ -47,6 +51,7 @@ export function SwitchAccountSubmenuContent() {
47
51
  <SwitchAccountSubmenuItem
48
52
  key={deviceSession.session.id}
49
53
  deviceSession={deviceSession}
54
+ hideSubtitle={hideSubtitle}
50
55
  />
51
56
  ))}
52
57
 
@@ -14,6 +14,7 @@ type DeviceSession = {
14
14
 
15
15
  export type SwitchAccountSubmenuItemProps = {
16
16
  deviceSession: DeviceSession
17
+ hideSubtitle?: boolean
17
18
  }
18
19
 
19
20
  /**
@@ -23,7 +24,8 @@ export type SwitchAccountSubmenuItemProps = {
23
24
  * @returns The switch account dropdown item as a JSX element
24
25
  */
25
26
  export function SwitchAccountSubmenuItem({
26
- deviceSession
27
+ deviceSession,
28
+ hideSubtitle
27
29
  }: SwitchAccountSubmenuItemProps) {
28
30
  const { authClient } = useAuth()
29
31
  const { mutate: setActiveSession, isPending } = useSetActiveSession(
@@ -41,7 +43,7 @@ export function SwitchAccountSubmenuItem({
41
43
  setActiveSession({ sessionToken: deviceSession.session.token })
42
44
  }
43
45
  >
44
- <UserView user={deviceSession.user} />
46
+ <UserView user={deviceSession.user} hideSubtitle={hideSubtitle} />
45
47
 
46
48
  {isPending && <Spinner color="current" size="sm" className="ml-auto" />}
47
49
  </Dropdown.Item>
@@ -1,5 +1,5 @@
1
1
  import { useAuth, useAuthPlugin, useSession } from "@better-auth-ui/react"
2
- import { Persons } from "@gravity-ui/icons"
2
+ import { ArrowRightArrowLeft } from "@gravity-ui/icons"
3
3
  import { Dropdown, Label } from "@heroui/react"
4
4
 
5
5
  import { multiSessionPlugin } from "../../../lib/auth/multi-session-plugin"
@@ -8,6 +8,7 @@ import { SwitchAccountSubmenuContent } from "./switch-account-submenu-content"
8
8
 
9
9
  export type SwitchAccountSubmenuProps = {
10
10
  className?: string
11
+ hideSubtitle?: boolean
11
12
  }
12
13
 
13
14
  /**
@@ -19,7 +20,10 @@ export type SwitchAccountSubmenuProps = {
19
20
  * @param className - Optional additional CSS class names
20
21
  * @returns The switch account submenu as a JSX element
21
22
  */
22
- export function SwitchAccountSubmenu({ className }: SwitchAccountSubmenuProps) {
23
+ export function SwitchAccountSubmenu({
24
+ className,
25
+ hideSubtitle
26
+ }: SwitchAccountSubmenuProps) {
23
27
  const { authClient } = useAuth()
24
28
  const { data: session } = useSession(authClient)
25
29
  const { localization: multiSessionLocalization } =
@@ -35,7 +39,7 @@ export function SwitchAccountSubmenu({ className }: SwitchAccountSubmenuProps) {
35
39
  className={className}
36
40
  textValue={multiSessionLocalization.switchAccount}
37
41
  >
38
- <Persons className="text-muted" />
42
+ <ArrowRightArrowLeft className="text-muted" />
39
43
 
40
44
  <Label>{multiSessionLocalization.switchAccount}</Label>
41
45
 
@@ -43,7 +47,7 @@ export function SwitchAccountSubmenu({ className }: SwitchAccountSubmenuProps) {
43
47
  </Dropdown.Item>
44
48
 
45
49
  <Dropdown.Popover className="min-w-40 md:min-w-56 max-w-[48svw]">
46
- <SwitchAccountSubmenuContent />
50
+ <SwitchAccountSubmenuContent hideSubtitle={hideSubtitle} />
47
51
  </Dropdown.Popover>
48
52
  </Dropdown.SubmenuTrigger>
49
53
  )
@@ -0,0 +1,171 @@
1
+ import { fileToBase64 } from "@better-auth-ui/core"
2
+ import {
3
+ type OrganizationAuthClient,
4
+ useActiveOrganization,
5
+ useAuth,
6
+ useAuthPlugin,
7
+ useUpdateOrganization
8
+ } from "@better-auth-ui/react"
9
+ import { CloudArrowUpIn, TrashBin } from "@gravity-ui/icons"
10
+ import { Button, cn, Dropdown, Label, Spinner, toast } from "@heroui/react"
11
+ import { type ChangeEvent, useRef, useState } from "react"
12
+ import { organizationPlugin } from "../../../lib/auth/organization-plugin"
13
+ import { OrganizationLogo } from "./organization-logo"
14
+
15
+ export type ChangeOrganizationLogoProps = {
16
+ className?: string
17
+ }
18
+
19
+ export function ChangeOrganizationLogo({
20
+ className
21
+ }: ChangeOrganizationLogoProps) {
22
+ const { authClient } = useAuth()
23
+ const { logo, localization: organizationLocalization } =
24
+ useAuthPlugin(organizationPlugin)
25
+
26
+ const { data: activeOrganization, isPending: activeOrganizationPending } =
27
+ useActiveOrganization(authClient as OrganizationAuthClient)
28
+
29
+ const { mutate: updateOrganization, isPending: updatePending } =
30
+ useUpdateOrganization(authClient as OrganizationAuthClient)
31
+
32
+ const fileInputRef = useRef<HTMLInputElement>(null)
33
+ const [isUploading, setIsUploading] = useState(false)
34
+ const [isDeleting, setIsDeleting] = useState(false)
35
+
36
+ const isPending = updatePending || isUploading || isDeleting
37
+
38
+ async function handleFileChange(e: ChangeEvent<HTMLInputElement>) {
39
+ const file = e.target.files?.[0]
40
+ if (!file || !activeOrganization) return
41
+
42
+ e.target.value = ""
43
+
44
+ setIsUploading(true)
45
+
46
+ try {
47
+ const resized =
48
+ (await logo.resize?.(file, logo.size, logo.extension)) || file
49
+
50
+ const image =
51
+ (await logo.upload?.(resized)) || (await fileToBase64(resized))
52
+
53
+ updateOrganization(
54
+ { data: { logo: image } },
55
+ {
56
+ onSuccess: () =>
57
+ toast.success(organizationLocalization.logoChangedSuccess),
58
+ onSettled: () => setIsUploading(false)
59
+ }
60
+ )
61
+ } catch (error) {
62
+ setIsUploading(false)
63
+ if (error instanceof Error) {
64
+ toast.danger(error.message)
65
+ }
66
+ }
67
+ }
68
+
69
+ async function handleDelete() {
70
+ const currentLogo = activeOrganization?.logo
71
+
72
+ updateOrganization(
73
+ {
74
+ data: { logo: "" }
75
+ },
76
+ {
77
+ onSuccess: async () => {
78
+ if (!currentLogo) {
79
+ toast.success(organizationLocalization.logoDeletedSuccess)
80
+ return
81
+ }
82
+
83
+ setIsDeleting(true)
84
+ try {
85
+ await logo.delete?.(currentLogo)
86
+ toast.success(organizationLocalization.logoDeletedSuccess)
87
+ } catch (error) {
88
+ if (error instanceof Error) {
89
+ toast.danger(error.message)
90
+ }
91
+ } finally {
92
+ setIsDeleting(false)
93
+ }
94
+ }
95
+ }
96
+ )
97
+ }
98
+
99
+ if (!logo.enabled) {
100
+ return null
101
+ }
102
+
103
+ return (
104
+ <div className={cn("flex flex-col gap-1", className)}>
105
+ <Label isDisabled={!activeOrganization}>
106
+ {organizationLocalization.logo}
107
+ </Label>
108
+
109
+ <input
110
+ ref={fileInputRef}
111
+ type="file"
112
+ accept="image/*"
113
+ className="hidden"
114
+ onChange={handleFileChange}
115
+ />
116
+
117
+ <div className="flex items-center gap-4">
118
+ <Button
119
+ type="button"
120
+ isIconOnly
121
+ variant="ghost"
122
+ className="p-0 h-auto w-auto rounded-full"
123
+ isDisabled={!activeOrganization || isPending}
124
+ onPress={() => fileInputRef.current?.click()}
125
+ >
126
+ <OrganizationLogo
127
+ size="lg"
128
+ isPending={activeOrganizationPending}
129
+ organization={activeOrganization}
130
+ />
131
+ </Button>
132
+
133
+ <Dropdown>
134
+ <Button
135
+ isDisabled={!activeOrganization || isPending}
136
+ size="sm"
137
+ variant="secondary"
138
+ >
139
+ {isPending && <Spinner size="sm" />}
140
+
141
+ {organizationLocalization.changeLogo}
142
+ </Button>
143
+
144
+ <Dropdown.Popover className="min-w-fit">
145
+ <Dropdown.Menu>
146
+ <Dropdown.Item
147
+ textValue={organizationLocalization.uploadLogo}
148
+ onAction={() => fileInputRef.current?.click()}
149
+ >
150
+ <CloudArrowUpIn className="text-muted" />
151
+
152
+ <Label>{organizationLocalization.uploadLogo}</Label>
153
+ </Dropdown.Item>
154
+
155
+ <Dropdown.Item
156
+ textValue={organizationLocalization.deleteLogo}
157
+ isDisabled={!activeOrganization?.logo}
158
+ onAction={handleDelete}
159
+ variant="danger"
160
+ >
161
+ <TrashBin className="text-danger" />
162
+
163
+ <Label>{organizationLocalization.deleteLogo}</Label>
164
+ </Dropdown.Item>
165
+ </Dropdown.Menu>
166
+ </Dropdown.Popover>
167
+ </Dropdown>
168
+ </div>
169
+ </div>
170
+ )
171
+ }
@@ -0,0 +1,133 @@
1
+ import {
2
+ type OrganizationAuthClient,
3
+ useAuth,
4
+ useAuthPlugin,
5
+ useCreateOrganization
6
+ } from "@better-auth-ui/react"
7
+ import { Briefcase } from "@gravity-ui/icons"
8
+ import {
9
+ AlertDialog,
10
+ Button,
11
+ FieldError,
12
+ Form,
13
+ Input,
14
+ Label,
15
+ Spinner,
16
+ TextField
17
+ } from "@heroui/react"
18
+ import { type SyntheticEvent, useEffect, useState } from "react"
19
+
20
+ import { organizationPlugin } from "../../../lib/auth/organization-plugin"
21
+ import { SlugField, sanitizeSlug } from "./slug-field"
22
+
23
+ /** Props for the {@link CreateOrganizationDialog} component. */
24
+ export type CreateOrganizationDialogProps = {
25
+ isOpen: boolean
26
+ onOpenChange: (open: boolean) => void
27
+ }
28
+
29
+ /**
30
+ * Render a dialog for creating a new organization.
31
+ *
32
+ * @param isOpen - Whether the dialog is open
33
+ * @param onOpenChange - Callback for when the dialog open state changes
34
+ * @returns The create organization dialog as a JSX element
35
+ */
36
+ export function CreateOrganizationDialog({
37
+ isOpen,
38
+ onOpenChange
39
+ }: CreateOrganizationDialogProps) {
40
+ const { authClient, localization } = useAuth()
41
+ const { localization: organizationLocalization } =
42
+ useAuthPlugin(organizationPlugin)
43
+
44
+ const [name, setName] = useState("")
45
+ const [slug, setSlug] = useState("")
46
+
47
+ const { mutate: createOrganization, isPending: isCreating } =
48
+ useCreateOrganization(authClient as OrganizationAuthClient, {
49
+ onSuccess: () => onOpenChange(false)
50
+ })
51
+
52
+ const handleSubmit = (e: SyntheticEvent<HTMLFormElement>) => {
53
+ e.preventDefault()
54
+ createOrganization({ name, slug })
55
+ }
56
+
57
+ useEffect(() => {
58
+ if (!isOpen) {
59
+ setSlug("")
60
+ setName("")
61
+ }
62
+ }, [isOpen])
63
+
64
+ useEffect(() => {
65
+ setSlug(sanitizeSlug(name))
66
+ }, [name])
67
+
68
+ return (
69
+ <AlertDialog.Backdrop isOpen={isOpen} onOpenChange={onOpenChange}>
70
+ <AlertDialog.Container>
71
+ <AlertDialog.Dialog>
72
+ <Form onSubmit={handleSubmit}>
73
+ <AlertDialog.CloseTrigger />
74
+
75
+ <AlertDialog.Header>
76
+ <AlertDialog.Icon status="default">
77
+ <Briefcase />
78
+ </AlertDialog.Icon>
79
+
80
+ <AlertDialog.Heading>
81
+ {organizationLocalization.createOrganization}
82
+ </AlertDialog.Heading>
83
+ </AlertDialog.Header>
84
+
85
+ <AlertDialog.Body className="flex flex-col gap-4 overflow-visible">
86
+ <p className="text-muted text-sm">
87
+ {organizationLocalization.organizationsDescription}
88
+ </p>
89
+
90
+ <TextField
91
+ id="name"
92
+ name="name"
93
+ isDisabled={isCreating}
94
+ value={name}
95
+ onChange={setName}
96
+ >
97
+ <Label>{organizationLocalization.name}</Label>
98
+
99
+ <Input
100
+ required
101
+ autoFocus
102
+ placeholder={organizationLocalization.namePlaceholder}
103
+ variant="secondary"
104
+ />
105
+
106
+ <FieldError />
107
+ </TextField>
108
+
109
+ <SlugField
110
+ value={slug}
111
+ onChange={setSlug}
112
+ isDisabled={isCreating}
113
+ variant="secondary"
114
+ />
115
+ </AlertDialog.Body>
116
+
117
+ <AlertDialog.Footer>
118
+ <Button slot="close" variant="tertiary" isDisabled={isCreating}>
119
+ {localization.settings.cancel}
120
+ </Button>
121
+
122
+ <Button type="submit" isPending={isCreating}>
123
+ {isCreating && <Spinner color="current" size="sm" />}
124
+
125
+ {organizationLocalization.createOrganization}
126
+ </Button>
127
+ </AlertDialog.Footer>
128
+ </Form>
129
+ </AlertDialog.Dialog>
130
+ </AlertDialog.Container>
131
+ </AlertDialog.Backdrop>
132
+ )
133
+ }
@@ -0,0 +1,97 @@
1
+ import {
2
+ type OrganizationAuthClient,
3
+ useAuth,
4
+ useAuthPlugin,
5
+ useDeleteOrganization
6
+ } from "@better-auth-ui/react"
7
+ import { TriangleExclamation } from "@gravity-ui/icons"
8
+ import { AlertDialog, Button, Card, Form, Spinner, toast } from "@heroui/react"
9
+ import type { Organization } from "better-auth/client"
10
+ import type { SyntheticEvent } from "react"
11
+
12
+ import { organizationPlugin } from "../../../lib/auth/organization-plugin"
13
+ import { OrganizationView } from "./organization-view"
14
+
15
+ export type DeleteOrganizationDialogProps = {
16
+ isOpen: boolean
17
+ onOpenChange: (open: boolean) => void
18
+ organization: Organization
19
+ }
20
+
21
+ export function DeleteOrganizationDialog({
22
+ isOpen,
23
+ onOpenChange,
24
+ organization
25
+ }: DeleteOrganizationDialogProps) {
26
+ const { authClient, basePaths, localization, navigate } = useAuth()
27
+ const {
28
+ localization: organizationLocalization,
29
+ viewPaths: organizationPluginViewPaths
30
+ } = useAuthPlugin(organizationPlugin)
31
+
32
+ const { mutate: deleteOrganization, isPending } = useDeleteOrganization(
33
+ authClient as OrganizationAuthClient,
34
+ {
35
+ onSuccess: () => {
36
+ onOpenChange(false)
37
+ toast.success(organizationLocalization.organizationDeleted)
38
+
39
+ navigate({
40
+ to: `${basePaths.settings}/${organizationPluginViewPaths.settings.organizations}`,
41
+ replace: true
42
+ })
43
+ }
44
+ }
45
+ )
46
+
47
+ function handleSubmit(e: SyntheticEvent<HTMLFormElement>) {
48
+ e.preventDefault()
49
+ deleteOrganization({ organizationId: organization.id })
50
+ }
51
+
52
+ return (
53
+ <AlertDialog.Backdrop isOpen={isOpen} onOpenChange={onOpenChange}>
54
+ <AlertDialog.Container>
55
+ <AlertDialog.Dialog>
56
+ <Form onSubmit={handleSubmit}>
57
+ <AlertDialog.CloseTrigger />
58
+
59
+ <AlertDialog.Header>
60
+ <AlertDialog.Icon status="danger">
61
+ <TriangleExclamation />
62
+ </AlertDialog.Icon>
63
+
64
+ <AlertDialog.Heading>
65
+ {organizationLocalization.deleteOrganization}
66
+ </AlertDialog.Heading>
67
+ </AlertDialog.Header>
68
+
69
+ <AlertDialog.Body className="flex flex-col gap-4 overflow-visible">
70
+ <p className="text-muted text-sm">
71
+ {organizationLocalization.deleteOrganizationDescription}
72
+ </p>
73
+
74
+ <Card variant="secondary">
75
+ <Card.Content>
76
+ <OrganizationView organization={organization} hideRole />
77
+ </Card.Content>
78
+ </Card>
79
+ </AlertDialog.Body>
80
+
81
+ <AlertDialog.Footer>
82
+ <Button slot="close" variant="tertiary" isDisabled={isPending}>
83
+ {localization.settings.cancel}
84
+ </Button>
85
+
86
+ <Button type="submit" variant="danger" isPending={isPending}>
87
+ {isPending && <Spinner color="current" size="sm" />}
88
+
89
+ {organizationLocalization.deleteOrganization}
90
+ </Button>
91
+ </AlertDialog.Footer>
92
+ </Form>
93
+ </AlertDialog.Dialog>
94
+ </AlertDialog.Container>
95
+ </AlertDialog.Backdrop>
96
+ )
97
+ }
@@ -0,0 +1,17 @@
1
+ import { Skeleton } from "@heroui/react"
2
+
3
+ /**
4
+ * Placeholder matching {@link DeleteOrganization} while the delete permission resolves.
5
+ */
6
+ export function DeleteOrganizationSkeleton() {
7
+ return (
8
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
9
+ <div className="flex flex-col gap-1">
10
+ <Skeleton className="h-3.5 w-40 rounded-lg" />
11
+ <Skeleton className="h-3 w-64 rounded-lg" />
12
+ </div>
13
+
14
+ <Skeleton className="h-8 w-36 shrink-0 rounded-full" />
15
+ </div>
16
+ )
17
+ }
@@ -0,0 +1,77 @@
1
+ import {
2
+ type OrganizationAuthClient,
3
+ useActiveOrganization,
4
+ useAuth,
5
+ useAuthPlugin,
6
+ useHasPermission
7
+ } from "@better-auth-ui/react"
8
+ import { AlertDialog, Button } from "@heroui/react"
9
+ import { useState } from "react"
10
+
11
+ import { organizationPlugin } from "../../../lib/auth/organization-plugin"
12
+ import { DeleteOrganizationDialog } from "./delete-organization-dialog"
13
+ import { DeleteOrganizationSkeleton } from "./delete-organization-skeleton"
14
+
15
+ /**
16
+ * Danger-zone row to delete the active organization. Hidden for members without
17
+ * the `organization:delete` permission.
18
+ */
19
+ export function DeleteOrganization() {
20
+ const { authClient } = useAuth()
21
+ const { localization: organizationLocalization } =
22
+ useAuthPlugin(organizationPlugin)
23
+
24
+ const { data: activeOrganization } = useActiveOrganization(
25
+ authClient as OrganizationAuthClient
26
+ )
27
+
28
+ const { data: permission, isPending: permissionPending } = useHasPermission(
29
+ authClient as OrganizationAuthClient,
30
+ {
31
+ permissions: { organization: ["delete"] }
32
+ }
33
+ )
34
+
35
+ const [confirmOpen, setConfirmOpen] = useState(false)
36
+
37
+ if (permissionPending) {
38
+ return <DeleteOrganizationSkeleton />
39
+ }
40
+
41
+ if (!permission?.success) {
42
+ return null
43
+ }
44
+
45
+ return (
46
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
47
+ <div>
48
+ <p className="text-sm font-medium leading-tight">
49
+ {organizationLocalization.deleteOrganization}
50
+ </p>
51
+
52
+ <p className="text-muted mt-0.5 text-xs">
53
+ {organizationLocalization.deleteOrganizationDescription}
54
+ </p>
55
+ </div>
56
+
57
+ <AlertDialog>
58
+ <Button
59
+ isDisabled={!activeOrganization}
60
+ size="sm"
61
+ variant="danger-soft"
62
+ onPress={() => setConfirmOpen(true)}
63
+ >
64
+ {organizationLocalization.deleteOrganization}
65
+ </Button>
66
+
67
+ {activeOrganization && (
68
+ <DeleteOrganizationDialog
69
+ isOpen={confirmOpen}
70
+ onOpenChange={setConfirmOpen}
71
+ organization={activeOrganization}
72
+ />
73
+ )}
74
+ </AlertDialog>
75
+ </div>
76
+ )
77
+ }