better-auth-ui 3.2.5

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 (226) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/dist/auth-hooks-IOEvlYYv.d.cts +6966 -0
  4. package/dist/auth-hooks-IOEvlYYv.d.ts +6966 -0
  5. package/dist/auth-mutators-DdqOmQ32.d.cts +29 -0
  6. package/dist/auth-mutators-DdqOmQ32.d.ts +29 -0
  7. package/dist/auth-ui-provider-BsH3xJDw.d.ts +697 -0
  8. package/dist/auth-ui-provider-DhZfncd3.d.cts +697 -0
  9. package/dist/chunk-BDFQSFBU.js +750 -0
  10. package/dist/chunk-CRAHKL2C.cjs +801 -0
  11. package/dist/chunk-MJPOA6PK.js +801 -0
  12. package/dist/chunk-SV64DXGW.cjs +750 -0
  13. package/dist/index.cjs +12618 -0
  14. package/dist/index.d.cts +771 -0
  15. package/dist/index.d.ts +771 -0
  16. package/dist/index.js +12618 -0
  17. package/dist/instantdb.cjs +189 -0
  18. package/dist/instantdb.d.cts +36 -0
  19. package/dist/instantdb.d.ts +36 -0
  20. package/dist/instantdb.js +189 -0
  21. package/dist/metafile-cjs.json +1 -0
  22. package/dist/metafile-esm.json +1 -0
  23. package/dist/server.cjs +194 -0
  24. package/dist/server.d.cts +35 -0
  25. package/dist/server.d.ts +35 -0
  26. package/dist/server.js +194 -0
  27. package/dist/style.css +1 -0
  28. package/dist/tanstack.cjs +153 -0
  29. package/dist/tanstack.d.cts +18 -0
  30. package/dist/tanstack.d.ts +18 -0
  31. package/dist/tanstack.js +153 -0
  32. package/dist/triplit.cjs +201 -0
  33. package/dist/triplit.d.cts +31 -0
  34. package/dist/triplit.d.ts +31 -0
  35. package/dist/triplit.js +201 -0
  36. package/dist/utils-C5R37WDe.d.cts +3 -0
  37. package/dist/utils-C5R37WDe.d.ts +3 -0
  38. package/dist/view-paths-CHSJf5dv.d.cts +645 -0
  39. package/dist/view-paths-CHSJf5dv.d.ts +645 -0
  40. package/package.json +156 -0
  41. package/src/components/account/account-view.tsx +220 -0
  42. package/src/components/auth/auth-callback.tsx +36 -0
  43. package/src/components/auth/auth-form.tsx +277 -0
  44. package/src/components/auth/auth-view.tsx +389 -0
  45. package/src/components/auth/email-otp-button.tsx +53 -0
  46. package/src/components/auth/forms/email-otp-form.tsx +288 -0
  47. package/src/components/auth/forms/forgot-password-form.tsx +168 -0
  48. package/src/components/auth/forms/magic-link-form.tsx +191 -0
  49. package/src/components/auth/forms/recover-account-form.tsx +138 -0
  50. package/src/components/auth/forms/reset-password-form.tsx +215 -0
  51. package/src/components/auth/forms/sign-in-form.tsx +289 -0
  52. package/src/components/auth/forms/sign-up-form.tsx +788 -0
  53. package/src/components/auth/forms/two-factor-form.tsx +372 -0
  54. package/src/components/auth/magic-link-button.tsx +54 -0
  55. package/src/components/auth/one-tap.tsx +48 -0
  56. package/src/components/auth/otp-input-group.tsx +65 -0
  57. package/src/components/auth/passkey-button.tsx +85 -0
  58. package/src/components/auth/provider-button.tsx +141 -0
  59. package/src/components/auth/sign-out.tsx +25 -0
  60. package/src/components/auth-loading.tsx +21 -0
  61. package/src/components/captcha/captcha.tsx +79 -0
  62. package/src/components/captcha/recaptcha-badge.tsx +61 -0
  63. package/src/components/captcha/recaptcha-v2.tsx +58 -0
  64. package/src/components/captcha/recaptcha-v3.tsx +73 -0
  65. package/src/components/email/email-template.tsx +216 -0
  66. package/src/components/form-error.tsx +27 -0
  67. package/src/components/organization/accept-invitation-card.tsx +362 -0
  68. package/src/components/organization/create-organization-dialog.tsx +395 -0
  69. package/src/components/organization/delete-organization-card.tsx +101 -0
  70. package/src/components/organization/delete-organization-dialog.tsx +209 -0
  71. package/src/components/organization/invitation-cell.tsx +156 -0
  72. package/src/components/organization/invite-member-dialog.tsx +258 -0
  73. package/src/components/organization/leave-organization-dialog.tsx +150 -0
  74. package/src/components/organization/member-cell.tsx +187 -0
  75. package/src/components/organization/organization-cell-view.tsx +122 -0
  76. package/src/components/organization/organization-cell.tsx +154 -0
  77. package/src/components/organization/organization-invitations-card.tsx +94 -0
  78. package/src/components/organization/organization-logo-card.tsx +308 -0
  79. package/src/components/organization/organization-logo.tsx +120 -0
  80. package/src/components/organization/organization-members-card.tsx +155 -0
  81. package/src/components/organization/organization-name-card.tsx +204 -0
  82. package/src/components/organization/organization-settings-cards.tsx +67 -0
  83. package/src/components/organization/organization-slug-card.tsx +223 -0
  84. package/src/components/organization/organization-switcher.tsx +512 -0
  85. package/src/components/organization/organization-view.tsx +228 -0
  86. package/src/components/organization/organizations-card.tsx +72 -0
  87. package/src/components/organization/personal-account-view.tsx +115 -0
  88. package/src/components/organization/remove-member-dialog.tsx +144 -0
  89. package/src/components/organization/update-member-role-dialog.tsx +213 -0
  90. package/src/components/organization/user-invitations-card.tsx +238 -0
  91. package/src/components/password-input.tsx +56 -0
  92. package/src/components/provider-icons.tsx +385 -0
  93. package/src/components/redirect-to-sign-in.tsx +16 -0
  94. package/src/components/redirect-to-sign-up.tsx +16 -0
  95. package/src/components/settings/account/account-cell.tsx +158 -0
  96. package/src/components/settings/account/accounts-card.tsx +75 -0
  97. package/src/components/settings/account/delete-account-card.tsx +65 -0
  98. package/src/components/settings/account/delete-account-dialog.tsx +231 -0
  99. package/src/components/settings/account/update-avatar-card.tsx +198 -0
  100. package/src/components/settings/account/update-field-card.tsx +282 -0
  101. package/src/components/settings/account/update-name-card.tsx +39 -0
  102. package/src/components/settings/account/update-username-card.tsx +42 -0
  103. package/src/components/settings/account-settings-cards.tsx +123 -0
  104. package/src/components/settings/api-key/api-key-cell.tsx +108 -0
  105. package/src/components/settings/api-key/api-key-delete-dialog.tsx +162 -0
  106. package/src/components/settings/api-key/api-key-display-dialog.tsx +110 -0
  107. package/src/components/settings/api-key/api-keys-card.tsx +98 -0
  108. package/src/components/settings/api-key/create-api-key-dialog.tsx +376 -0
  109. package/src/components/settings/passkey/passkey-cell.tsx +113 -0
  110. package/src/components/settings/passkey/passkeys-card.tsx +111 -0
  111. package/src/components/settings/providers/provider-cell.tsx +152 -0
  112. package/src/components/settings/providers/providers-card.tsx +108 -0
  113. package/src/components/settings/security/change-email-card.tsx +200 -0
  114. package/src/components/settings/security/change-password-card.tsx +326 -0
  115. package/src/components/settings/security/session-cell.tsx +120 -0
  116. package/src/components/settings/security/sessions-card.tsx +58 -0
  117. package/src/components/settings/security-settings-cards.tsx +111 -0
  118. package/src/components/settings/shared/session-freshness-dialog.tsx +97 -0
  119. package/src/components/settings/shared/settings-action-button.tsx +51 -0
  120. package/src/components/settings/shared/settings-card-footer.tsx +94 -0
  121. package/src/components/settings/shared/settings-card-header.tsx +67 -0
  122. package/src/components/settings/shared/settings-card.tsx +106 -0
  123. package/src/components/settings/skeletons/input-field-skeleton.tsx +18 -0
  124. package/src/components/settings/skeletons/settings-cell-skeleton.tsx +37 -0
  125. package/src/components/settings/two-factor/backup-codes-dialog.tsx +113 -0
  126. package/src/components/settings/two-factor/two-factor-card.tsx +63 -0
  127. package/src/components/settings/two-factor/two-factor-password-dialog.tsx +226 -0
  128. package/src/components/signed-in.tsx +20 -0
  129. package/src/components/signed-out.tsx +20 -0
  130. package/src/components/ui/alert.tsx +66 -0
  131. package/src/components/ui/avatar.tsx +53 -0
  132. package/src/components/ui/button.tsx +59 -0
  133. package/src/components/ui/card.tsx +92 -0
  134. package/src/components/ui/checkbox.tsx +32 -0
  135. package/src/components/ui/dialog.tsx +143 -0
  136. package/src/components/ui/drawer.tsx +135 -0
  137. package/src/components/ui/dropdown-menu.tsx +257 -0
  138. package/src/components/ui/form.tsx +167 -0
  139. package/src/components/ui/input-otp.tsx +77 -0
  140. package/src/components/ui/input.tsx +21 -0
  141. package/src/components/ui/label.tsx +24 -0
  142. package/src/components/ui/select.tsx +185 -0
  143. package/src/components/ui/separator.tsx +28 -0
  144. package/src/components/ui/skeleton.tsx +13 -0
  145. package/src/components/ui/tabs.tsx +66 -0
  146. package/src/components/ui/textarea.tsx +18 -0
  147. package/src/components/user-avatar.tsx +147 -0
  148. package/src/components/user-button.tsx +409 -0
  149. package/src/components/user-view.tsx +138 -0
  150. package/src/hooks/use-auth-data.ts +184 -0
  151. package/src/hooks/use-authenticate.ts +62 -0
  152. package/src/hooks/use-captcha.tsx +138 -0
  153. package/src/hooks/use-current-organization.ts +59 -0
  154. package/src/hooks/use-hydrated.ts +13 -0
  155. package/src/hooks/use-lang.ts +32 -0
  156. package/src/hooks/use-success-transition.ts +51 -0
  157. package/src/hooks/use-theme.ts +39 -0
  158. package/src/index.ts +65 -0
  159. package/src/instantdb.ts +1 -0
  160. package/src/lib/auth-data-cache.ts +90 -0
  161. package/src/lib/auth-ui-provider.tsx +658 -0
  162. package/src/lib/gravatar-utils.ts +58 -0
  163. package/src/lib/image-utils.ts +55 -0
  164. package/src/lib/instantdb/model-names.ts +24 -0
  165. package/src/lib/instantdb/use-instant-options.ts +98 -0
  166. package/src/lib/instantdb/use-list-accounts.ts +38 -0
  167. package/src/lib/instantdb/use-list-sessions.ts +53 -0
  168. package/src/lib/instantdb/use-session.ts +55 -0
  169. package/src/lib/organization-refetcher.tsx +56 -0
  170. package/src/lib/social-providers.ts +144 -0
  171. package/src/lib/tanstack/auth-ui-provider-tanstack.tsx +49 -0
  172. package/src/lib/tanstack/use-tanstack-options.ts +112 -0
  173. package/src/lib/triplit/model-names.ts +24 -0
  174. package/src/lib/triplit/use-conditional-query.ts +82 -0
  175. package/src/lib/triplit/use-list-accounts.ts +31 -0
  176. package/src/lib/triplit/use-list-sessions.ts +33 -0
  177. package/src/lib/triplit/use-session.ts +42 -0
  178. package/src/lib/triplit/use-triplit-hooks.ts +68 -0
  179. package/src/lib/triplit/use-triplit-token.ts +44 -0
  180. package/src/lib/utils.ts +105 -0
  181. package/src/lib/view-paths.ts +55 -0
  182. package/src/localization/admin-error-codes.ts +20 -0
  183. package/src/localization/anonymous-error-codes.ts +6 -0
  184. package/src/localization/api-key-error-codes.ts +32 -0
  185. package/src/localization/auth-localization.ts +740 -0
  186. package/src/localization/base-error-codes.ts +27 -0
  187. package/src/localization/captcha-error-codes.ts +17 -0
  188. package/src/localization/email-otp-error-codes.ts +7 -0
  189. package/src/localization/generic-oauth-error-codes.ts +3 -0
  190. package/src/localization/haveibeenpwned-error-codes.ts +4 -0
  191. package/src/localization/multi-session-error-codes.ts +3 -0
  192. package/src/localization/organization-error-codes.ts +57 -0
  193. package/src/localization/passkey-error-codes.ts +10 -0
  194. package/src/localization/phone-number-error-codes.ts +10 -0
  195. package/src/localization/stripe-localization.ts +12 -0
  196. package/src/localization/two-factor-error-codes.ts +12 -0
  197. package/src/localization/username-error-codes.ts +9 -0
  198. package/src/server.ts +4 -0
  199. package/src/style.css +1 -0
  200. package/src/tanstack.ts +1 -0
  201. package/src/triplit.ts +1 -0
  202. package/src/types/account-options.ts +35 -0
  203. package/src/types/additional-fields.ts +21 -0
  204. package/src/types/any-auth-client.ts +6 -0
  205. package/src/types/api-key.ts +9 -0
  206. package/src/types/auth-client.ts +37 -0
  207. package/src/types/auth-hooks.ts +61 -0
  208. package/src/types/auth-mutators.ts +17 -0
  209. package/src/types/avatar-options.ts +29 -0
  210. package/src/types/captcha-options.ts +32 -0
  211. package/src/types/captcha-provider.ts +6 -0
  212. package/src/types/credentials-options.ts +32 -0
  213. package/src/types/delete-user-options.ts +7 -0
  214. package/src/types/fetch-error.ts +6 -0
  215. package/src/types/generic-oauth-options.ts +16 -0
  216. package/src/types/gravatar-options.ts +21 -0
  217. package/src/types/image.ts +7 -0
  218. package/src/types/invitation.ts +10 -0
  219. package/src/types/link.ts +7 -0
  220. package/src/types/organization-options.ts +106 -0
  221. package/src/types/password-validation.ts +16 -0
  222. package/src/types/profile.ts +15 -0
  223. package/src/types/refetch.ts +1 -0
  224. package/src/types/render-toast.ts +9 -0
  225. package/src/types/sign-up-options.ts +7 -0
  226. package/src/types/social-options.ts +16 -0
@@ -0,0 +1,94 @@
1
+ "use client"
2
+
3
+ import type { Organization } from "better-auth/plugins/organization"
4
+ import { useContext, useMemo } from "react"
5
+ import { useCurrentOrganization } from "../../hooks/use-current-organization"
6
+ import { AuthUIContext } from "../../lib/auth-ui-provider"
7
+ import { cn } from "../../lib/utils"
8
+ import type { SettingsCardProps } from "../settings/shared/settings-card"
9
+ import { SettingsCard } from "../settings/shared/settings-card"
10
+ import { CardContent } from "../ui/card"
11
+ import { InvitationCell } from "./invitation-cell"
12
+
13
+ export function OrganizationInvitationsCard({
14
+ className,
15
+ classNames,
16
+ localization: localizationProp,
17
+ slug: slugProp,
18
+ ...props
19
+ }: SettingsCardProps & { slug?: string }) {
20
+ const {
21
+ localization: contextLocalization,
22
+ organization: organizationOptions
23
+ } = useContext(AuthUIContext)
24
+
25
+ const localization = useMemo(
26
+ () => ({ ...contextLocalization, ...localizationProp }),
27
+ [contextLocalization, localizationProp]
28
+ )
29
+
30
+ const slug = slugProp || organizationOptions?.slug
31
+
32
+ const { data: organization } = useCurrentOrganization({ slug })
33
+
34
+ if (!organization) return null
35
+
36
+ return (
37
+ <OrganizationInvitationsContent
38
+ className={className}
39
+ classNames={classNames}
40
+ localization={localization}
41
+ organization={organization}
42
+ {...props}
43
+ />
44
+ )
45
+ }
46
+
47
+ function OrganizationInvitationsContent({
48
+ className,
49
+ classNames,
50
+ localization: localizationProp,
51
+ organization,
52
+ ...props
53
+ }: SettingsCardProps & { organization: Organization }) {
54
+ const {
55
+ hooks: { useListInvitations },
56
+ localization: contextLocalization
57
+ } = useContext(AuthUIContext)
58
+
59
+ const localization = useMemo(
60
+ () => ({ ...contextLocalization, ...localizationProp }),
61
+ [contextLocalization, localizationProp]
62
+ )
63
+
64
+ const { data: invitations } = useListInvitations({
65
+ query: { organizationId: organization.id }
66
+ })
67
+
68
+ const pendingInvitations = invitations?.filter(
69
+ (invitation) => invitation.status === "pending"
70
+ )
71
+ if (!pendingInvitations?.length) return null
72
+
73
+ return (
74
+ <SettingsCard
75
+ className={className}
76
+ classNames={classNames}
77
+ title={localization.PENDING_INVITATIONS}
78
+ description={localization.PENDING_INVITATIONS_DESCRIPTION}
79
+ {...props}
80
+ >
81
+ <CardContent className={cn("grid gap-4", classNames?.content)}>
82
+ {pendingInvitations.map((invitation) => (
83
+ <InvitationCell
84
+ key={invitation.id}
85
+ classNames={classNames}
86
+ invitation={invitation}
87
+ localization={localization}
88
+ organization={organization}
89
+ />
90
+ ))}
91
+ </CardContent>
92
+ </SettingsCard>
93
+ )
94
+ }
@@ -0,0 +1,308 @@
1
+ "use client"
2
+
3
+ import type { Organization } from "better-auth/plugins/organization"
4
+ import { Trash2Icon, UploadCloudIcon } from "lucide-react"
5
+ import {
6
+ type ComponentProps,
7
+ useContext,
8
+ useMemo,
9
+ useRef,
10
+ useState
11
+ } from "react"
12
+
13
+ import { useCurrentOrganization } from "../../hooks/use-current-organization"
14
+ import { AuthUIContext } from "../../lib/auth-ui-provider"
15
+ import { fileToBase64, resizeAndCropImage } from "../../lib/image-utils"
16
+ import { cn, getLocalizedError } from "../../lib/utils"
17
+ import type { AuthLocalization } from "../../localization/auth-localization"
18
+ import type { SettingsCardClassNames } from "../settings/shared/settings-card"
19
+ import { SettingsCardFooter } from "../settings/shared/settings-card-footer"
20
+ import { SettingsCardHeader } from "../settings/shared/settings-card-header"
21
+ import { Button } from "../ui/button"
22
+ import { Card } from "../ui/card"
23
+ import {
24
+ DropdownMenu,
25
+ DropdownMenuContent,
26
+ DropdownMenuItem,
27
+ DropdownMenuTrigger
28
+ } from "../ui/dropdown-menu"
29
+ import { OrganizationLogo } from "./organization-logo"
30
+
31
+ export interface OrganizationLogoCardProps extends ComponentProps<typeof Card> {
32
+ className?: string
33
+ classNames?: SettingsCardClassNames
34
+ localization?: AuthLocalization
35
+ slug?: string
36
+ }
37
+
38
+ export function OrganizationLogoCard({
39
+ className,
40
+ classNames,
41
+ localization: localizationProp,
42
+ slug,
43
+ ...props
44
+ }: OrganizationLogoCardProps) {
45
+ const { localization: contextLocalization } = useContext(AuthUIContext)
46
+
47
+ const localization = useMemo(
48
+ () => ({ ...contextLocalization, ...localizationProp }),
49
+ [contextLocalization, localizationProp]
50
+ )
51
+
52
+ const { data: organization } = useCurrentOrganization({ slug })
53
+
54
+ if (!organization) {
55
+ return (
56
+ <Card
57
+ className={cn(
58
+ "w-full pb-0 text-start",
59
+ className,
60
+ classNames?.base
61
+ )}
62
+ {...props}
63
+ >
64
+ <div className="flex justify-between">
65
+ <SettingsCardHeader
66
+ className="grow self-start"
67
+ title={localization.LOGO}
68
+ description={localization.LOGO_DESCRIPTION}
69
+ isPending
70
+ classNames={classNames}
71
+ />
72
+
73
+ <Button
74
+ type="button"
75
+ className="me-6 size-fit rounded-full"
76
+ size="icon"
77
+ variant="ghost"
78
+ disabled
79
+ >
80
+ <OrganizationLogo
81
+ isPending
82
+ className="size-20 text-2xl"
83
+ classNames={classNames?.avatar}
84
+ localization={localization}
85
+ />
86
+ </Button>
87
+ </div>
88
+
89
+ <SettingsCardFooter
90
+ className="!py-5"
91
+ instructions={localization.LOGO_INSTRUCTIONS}
92
+ classNames={classNames}
93
+ isPending
94
+ />
95
+ </Card>
96
+ )
97
+ }
98
+
99
+ return (
100
+ <OrganizationLogoForm
101
+ className={className}
102
+ classNames={classNames}
103
+ localization={localization}
104
+ organization={organization}
105
+ {...props}
106
+ />
107
+ )
108
+ }
109
+
110
+ function OrganizationLogoForm({
111
+ className,
112
+ classNames,
113
+ localization: localizationProp,
114
+ organization,
115
+ ...props
116
+ }: OrganizationLogoCardProps & { organization: Organization }) {
117
+ const {
118
+ hooks: { useHasPermission },
119
+ localization: authLocalization,
120
+ organization: organizationOptions,
121
+ mutators: { updateOrganization },
122
+ toast
123
+ } = useContext(AuthUIContext)
124
+
125
+ const localization = useMemo(
126
+ () => ({ ...authLocalization, ...localizationProp }),
127
+ [authLocalization, localizationProp]
128
+ )
129
+
130
+ const { refetch: refetchOrganization } = useCurrentOrganization({
131
+ slug: organization.slug
132
+ })
133
+
134
+ const { data: hasPermission, isPending: permissionPending } =
135
+ useHasPermission({
136
+ organizationId: organization.id,
137
+ permissions: {
138
+ organization: ["update"]
139
+ }
140
+ })
141
+
142
+ const isPending = permissionPending
143
+
144
+ const fileInputRef = useRef<HTMLInputElement | null>(null)
145
+ const [loading, setLoading] = useState(false)
146
+
147
+ const handleLogoChange = async (file: File) => {
148
+ if (!organizationOptions?.logo || !hasPermission?.success) return
149
+
150
+ setLoading(true)
151
+
152
+ const resizedFile = await resizeAndCropImage(
153
+ file,
154
+ crypto.randomUUID(),
155
+ organizationOptions.logo.size,
156
+ organizationOptions.logo.extension
157
+ )
158
+
159
+ let image: string | undefined | null
160
+
161
+ if (organizationOptions.logo.upload) {
162
+ image = await organizationOptions.logo.upload(resizedFile)
163
+ } else {
164
+ image = await fileToBase64(resizedFile)
165
+ }
166
+
167
+ if (!image) {
168
+ setLoading(false)
169
+ return
170
+ }
171
+
172
+ try {
173
+ await updateOrganization({
174
+ organizationId: organization.id,
175
+ data: { logo: image }
176
+ })
177
+
178
+ await refetchOrganization?.()
179
+ } catch (error) {
180
+ toast({
181
+ variant: "error",
182
+ message: getLocalizedError({ error, localization })
183
+ })
184
+ }
185
+
186
+ setLoading(false)
187
+ }
188
+
189
+ const handleDeleteLogo = async () => {
190
+ if (!hasPermission?.success) return
191
+
192
+ setLoading(true)
193
+
194
+ try {
195
+ if (organization.logo) {
196
+ await organizationOptions?.logo?.delete?.(organization.logo)
197
+ }
198
+
199
+ await updateOrganization({
200
+ organizationId: organization.id,
201
+ data: { logo: "" }
202
+ })
203
+
204
+ await refetchOrganization?.()
205
+ } catch (error) {
206
+ toast({
207
+ variant: "error",
208
+ message: getLocalizedError({ error, localization })
209
+ })
210
+ }
211
+
212
+ setLoading(false)
213
+ }
214
+
215
+ const openFileDialog = () => {
216
+ fileInputRef.current?.click()
217
+ }
218
+
219
+ return (
220
+ <Card
221
+ className={cn(
222
+ "w-full pb-0 text-start",
223
+ className,
224
+ classNames?.base
225
+ )}
226
+ {...props}
227
+ >
228
+ <input
229
+ ref={fileInputRef}
230
+ accept="image/*"
231
+ disabled={loading || !hasPermission?.success}
232
+ hidden
233
+ type="file"
234
+ onChange={(e) => {
235
+ const file = e.target.files?.item(0)
236
+ if (file) handleLogoChange(file)
237
+
238
+ e.target.value = ""
239
+ }}
240
+ />
241
+
242
+ <div className="flex justify-between">
243
+ <SettingsCardHeader
244
+ className="grow self-start"
245
+ title={localization.LOGO}
246
+ description={localization.LOGO_DESCRIPTION}
247
+ isPending={isPending}
248
+ classNames={classNames}
249
+ />
250
+
251
+ <DropdownMenu>
252
+ <DropdownMenuTrigger asChild>
253
+ <Button
254
+ type="button"
255
+ className="me-6 size-fit rounded-full"
256
+ size="icon"
257
+ variant="ghost"
258
+ disabled={!hasPermission?.success}
259
+ >
260
+ <OrganizationLogo
261
+ isPending={isPending || loading}
262
+ key={organization.logo}
263
+ className="size-20 text-2xl"
264
+ classNames={classNames?.avatar}
265
+ organization={organization}
266
+ localization={localization}
267
+ />
268
+ </Button>
269
+ </DropdownMenuTrigger>
270
+
271
+ <DropdownMenuContent
272
+ align="end"
273
+ onCloseAutoFocus={(e) => e.preventDefault()}
274
+ >
275
+ <DropdownMenuItem
276
+ onClick={openFileDialog}
277
+ disabled={loading || !hasPermission?.success}
278
+ >
279
+ <UploadCloudIcon />
280
+
281
+ {localization.UPLOAD_LOGO}
282
+ </DropdownMenuItem>
283
+
284
+ {organization.logo && (
285
+ <DropdownMenuItem
286
+ onClick={handleDeleteLogo}
287
+ disabled={loading || !hasPermission?.success}
288
+ variant="destructive"
289
+ >
290
+ <Trash2Icon />
291
+
292
+ {localization.DELETE_LOGO}
293
+ </DropdownMenuItem>
294
+ )}
295
+ </DropdownMenuContent>
296
+ </DropdownMenu>
297
+ </div>
298
+
299
+ <SettingsCardFooter
300
+ className="!py-5"
301
+ instructions={localization.LOGO_INSTRUCTIONS}
302
+ classNames={classNames}
303
+ isPending={isPending}
304
+ isSubmitting={loading}
305
+ />
306
+ </Card>
307
+ )
308
+ }
@@ -0,0 +1,120 @@
1
+ "use client"
2
+
3
+ import type { Organization } from "better-auth/plugins/organization"
4
+ import { BuildingIcon } from "lucide-react"
5
+ import { type ComponentProps, useContext, useMemo } from "react"
6
+
7
+ import { AuthUIContext } from "../../lib/auth-ui-provider"
8
+ import { cn } from "../../lib/utils"
9
+ import type { AuthLocalization } from "../../localization/auth-localization"
10
+ import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar"
11
+ import { Skeleton } from "../ui/skeleton"
12
+
13
+ export interface OrganizationLogoClassNames {
14
+ base?: string
15
+ image?: string
16
+ fallback?: string
17
+ fallbackIcon?: string
18
+ skeleton?: string
19
+ }
20
+
21
+ export interface OrganizationLogoProps {
22
+ classNames?: OrganizationLogoClassNames
23
+ isPending?: boolean
24
+ size?: "sm" | "default" | "lg" | "xl" | null
25
+ organization?: Partial<Organization> | null
26
+ /**
27
+ * @default authLocalization
28
+ * @remarks `AuthLocalization`
29
+ */
30
+ localization?: AuthLocalization
31
+ }
32
+
33
+ /**
34
+ * Displays an organization logo with image and fallback support
35
+ *
36
+ * Renders an organization's logo image when available, with appropriate fallbacks:
37
+ * - Shows a skeleton when isPending is true
38
+ * - Falls back to a building icon when no logo is available
39
+ */
40
+ export function OrganizationLogo({
41
+ className,
42
+ classNames,
43
+ isPending,
44
+ size,
45
+ organization,
46
+ localization: propLocalization,
47
+ ...props
48
+ }: OrganizationLogoProps & ComponentProps<typeof Avatar>) {
49
+ const { localization: contextLocalization, avatar } =
50
+ useContext(AuthUIContext)
51
+
52
+ const localization = useMemo(
53
+ () => ({ ...contextLocalization, ...propLocalization }),
54
+ [contextLocalization, propLocalization]
55
+ )
56
+
57
+ const name = organization?.name
58
+ const src = organization?.logo
59
+
60
+ if (isPending) {
61
+ return (
62
+ <Skeleton
63
+ className={cn(
64
+ "shrink-0 rounded-full",
65
+ size === "sm"
66
+ ? "size-6"
67
+ : size === "lg"
68
+ ? "size-10"
69
+ : size === "xl"
70
+ ? "size-12"
71
+ : "size-8",
72
+ className,
73
+ classNames?.base,
74
+ classNames?.skeleton
75
+ )}
76
+ />
77
+ )
78
+ }
79
+
80
+ return (
81
+ <Avatar
82
+ className={cn(
83
+ "bg-muted",
84
+ size === "sm"
85
+ ? "size-6"
86
+ : size === "lg"
87
+ ? "size-10"
88
+ : size === "xl"
89
+ ? "size-12"
90
+ : "size-8",
91
+ className,
92
+ classNames?.base
93
+ )}
94
+ {...props}
95
+ >
96
+ {avatar?.Image ? (
97
+ <avatar.Image
98
+ alt={name || localization?.ORGANIZATION!}
99
+ className={classNames?.image}
100
+ src={src || ""}
101
+ />
102
+ ) : (
103
+ <AvatarImage
104
+ alt={name || localization?.ORGANIZATION!}
105
+ className={classNames?.image}
106
+ src={src || undefined}
107
+ />
108
+ )}
109
+
110
+ <AvatarFallback
111
+ className={cn("text-foreground", classNames?.fallback)}
112
+ delayMs={src ? 600 : undefined}
113
+ >
114
+ <BuildingIcon
115
+ className={cn("size-[50%]", classNames?.fallbackIcon)}
116
+ />
117
+ </AvatarFallback>
118
+ </Avatar>
119
+ )
120
+ }
@@ -0,0 +1,155 @@
1
+ "use client"
2
+
3
+ import type { Organization } from "better-auth/plugins/organization"
4
+ import { useContext, useMemo, useState } from "react"
5
+
6
+ import { useCurrentOrganization } from "../../hooks/use-current-organization"
7
+ import { AuthUIContext } from "../../lib/auth-ui-provider"
8
+ import { cn } from "../../lib/utils"
9
+ import type { SettingsCardProps } from "../settings/shared/settings-card"
10
+ import { SettingsCard } from "../settings/shared/settings-card"
11
+ import { CardContent } from "../ui/card"
12
+ import { InviteMemberDialog } from "./invite-member-dialog"
13
+ import { MemberCell } from "./member-cell"
14
+
15
+ export function OrganizationMembersCard({
16
+ className,
17
+ classNames,
18
+ localization: localizationProp,
19
+ slug: slugProp,
20
+ ...props
21
+ }: SettingsCardProps & { slug?: string }) {
22
+ const {
23
+ localization: contextLocalization,
24
+ organization: organizationOptions
25
+ } = useContext(AuthUIContext)
26
+
27
+ const localization = useMemo(
28
+ () => ({ ...contextLocalization, ...localizationProp }),
29
+ [contextLocalization, localizationProp]
30
+ )
31
+
32
+ const slug = slugProp || organizationOptions?.slug
33
+
34
+ const { data: organization } = useCurrentOrganization({ slug })
35
+
36
+ if (!organization) {
37
+ return (
38
+ <SettingsCard
39
+ className={className}
40
+ classNames={classNames}
41
+ title={localization.MEMBERS}
42
+ description={localization.MEMBERS_DESCRIPTION}
43
+ instructions={localization.MEMBERS_INSTRUCTIONS}
44
+ actionLabel={localization.INVITE_MEMBER}
45
+ isPending
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ return (
52
+ <OrganizationMembersContent
53
+ className={className}
54
+ classNames={classNames}
55
+ localization={localization}
56
+ organization={organization}
57
+ {...props}
58
+ />
59
+ )
60
+ }
61
+
62
+ function OrganizationMembersContent({
63
+ className,
64
+ classNames,
65
+ localization: localizationProp,
66
+ organization,
67
+ ...props
68
+ }: SettingsCardProps & { organization: Organization }) {
69
+ const {
70
+ hooks: { useHasPermission, useListMembers },
71
+ localization: contextLocalization
72
+ } = useContext(AuthUIContext)
73
+
74
+ const localization = useMemo(
75
+ () => ({ ...contextLocalization, ...localizationProp }),
76
+ [contextLocalization, localizationProp]
77
+ )
78
+
79
+ const { data: hasPermissionInvite, isPending: isPendingInvite } =
80
+ useHasPermission({
81
+ organizationId: organization.id,
82
+ permissions: {
83
+ invitation: ["create"]
84
+ }
85
+ })
86
+
87
+ const {
88
+ data: hasPermissionUpdateMember,
89
+ isPending: isPendingUpdateMember
90
+ } = useHasPermission({
91
+ organizationId: organization.id,
92
+ permission: {
93
+ member: ["update"]
94
+ }
95
+ })
96
+
97
+ const isPending = isPendingInvite || isPendingUpdateMember
98
+
99
+ const { data } = useListMembers({
100
+ query: { organizationId: organization.id }
101
+ })
102
+
103
+ const members = data?.members
104
+
105
+ const [inviteDialogOpen, setInviteDialogOpen] = useState(false)
106
+
107
+ return (
108
+ <>
109
+ <SettingsCard
110
+ className={className}
111
+ classNames={classNames}
112
+ title={localization.MEMBERS}
113
+ description={localization.MEMBERS_DESCRIPTION}
114
+ instructions={localization.MEMBERS_INSTRUCTIONS}
115
+ actionLabel={localization.INVITE_MEMBER}
116
+ action={() => setInviteDialogOpen(true)}
117
+ isPending={isPending}
118
+ disabled={!hasPermissionInvite?.success}
119
+ {...props}
120
+ >
121
+ {!isPending && members && members.length > 0 && (
122
+ <CardContent
123
+ className={cn("grid gap-4", classNames?.content)}
124
+ >
125
+ {members
126
+ .sort(
127
+ (a, b) =>
128
+ new Date(a.createdAt).getTime() -
129
+ new Date(b.createdAt).getTime()
130
+ )
131
+ .map((member) => (
132
+ <MemberCell
133
+ key={member.id}
134
+ classNames={classNames}
135
+ member={member}
136
+ localization={localization}
137
+ hideActions={
138
+ !hasPermissionUpdateMember?.success
139
+ }
140
+ />
141
+ ))}
142
+ </CardContent>
143
+ )}
144
+ </SettingsCard>
145
+
146
+ <InviteMemberDialog
147
+ open={inviteDialogOpen}
148
+ onOpenChange={setInviteDialogOpen}
149
+ classNames={classNames}
150
+ localization={localization}
151
+ organization={organization}
152
+ />
153
+ </>
154
+ )
155
+ }