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,209 @@
1
+ "use client"
2
+
3
+ import { zodResolver } from "@hookform/resolvers/zod"
4
+ import type { Organization } from "better-auth/plugins/organization"
5
+ import { Loader2 } from "lucide-react"
6
+ import { type ComponentProps, useContext, useMemo } from "react"
7
+ import { useForm } from "react-hook-form"
8
+ import * as z from "zod"
9
+
10
+ import { AuthUIContext } from "../../lib/auth-ui-provider"
11
+ import { cn, getLocalizedError } from "../../lib/utils"
12
+ import type { AuthLocalization } from "../../localization/auth-localization"
13
+ import type { SettingsCardClassNames } from "../settings/shared/settings-card"
14
+ import { Button } from "../ui/button"
15
+ import { Card } from "../ui/card"
16
+ import {
17
+ Dialog,
18
+ DialogContent,
19
+ DialogDescription,
20
+ DialogFooter,
21
+ DialogHeader,
22
+ DialogTitle
23
+ } from "../ui/dialog"
24
+ import {
25
+ Form,
26
+ FormControl,
27
+ FormField,
28
+ FormItem,
29
+ FormLabel,
30
+ FormMessage
31
+ } from "../ui/form"
32
+ import { Input } from "../ui/input"
33
+ import { OrganizationCellView } from "./organization-cell-view"
34
+
35
+ export interface DeleteOrganizationDialogProps
36
+ extends ComponentProps<typeof Dialog> {
37
+ classNames?: SettingsCardClassNames
38
+ localization?: AuthLocalization
39
+ organization: Organization
40
+ }
41
+
42
+ export function DeleteOrganizationDialog({
43
+ classNames,
44
+ localization: localizationProp,
45
+ onOpenChange,
46
+ organization,
47
+ ...props
48
+ }: DeleteOrganizationDialogProps) {
49
+ const {
50
+ authClient,
51
+ account: accountOptions,
52
+ hooks: { useListOrganizations },
53
+ localization: contextLocalization,
54
+ navigate,
55
+ toast
56
+ } = useContext(AuthUIContext)
57
+
58
+ const localization = useMemo(
59
+ () => ({ ...contextLocalization, ...localizationProp }),
60
+ [contextLocalization, localizationProp]
61
+ )
62
+
63
+ const { refetch: refetchOrganizations } = useListOrganizations()
64
+
65
+ const formSchema = z.object({
66
+ slug: z
67
+ .string()
68
+ .min(1, { message: localization.SLUG_REQUIRED! })
69
+ .refine((val) => val === organization.slug, {
70
+ message: localization.SLUG_DOES_NOT_MATCH!
71
+ })
72
+ })
73
+
74
+ const form = useForm({
75
+ resolver: zodResolver(formSchema),
76
+ defaultValues: {
77
+ slug: ""
78
+ }
79
+ })
80
+
81
+ const { isSubmitting } = form.formState
82
+
83
+ const deleteOrganization = async () => {
84
+ try {
85
+ await authClient.organization.delete({
86
+ organizationId: organization.id,
87
+ fetchOptions: { throw: true }
88
+ })
89
+
90
+ await refetchOrganizations?.()
91
+
92
+ toast({
93
+ variant: "success",
94
+ message: localization.DELETE_ORGANIZATION_SUCCESS!
95
+ })
96
+
97
+ navigate(
98
+ `${accountOptions?.basePath}/${accountOptions?.viewPaths.ORGANIZATIONS}`
99
+ )
100
+
101
+ onOpenChange?.(false)
102
+ } catch (error) {
103
+ toast({
104
+ variant: "error",
105
+ message: getLocalizedError({ error, localization })
106
+ })
107
+ }
108
+ }
109
+
110
+ return (
111
+ <Dialog onOpenChange={onOpenChange} {...props}>
112
+ <DialogContent
113
+ className={cn("sm:max-w-md", classNames?.dialog?.content)}
114
+ >
115
+ <DialogHeader className={classNames?.dialog?.header}>
116
+ <DialogTitle
117
+ className={cn("text-lg md:text-xl", classNames?.title)}
118
+ >
119
+ {localization?.DELETE_ORGANIZATION}
120
+ </DialogTitle>
121
+
122
+ <DialogDescription
123
+ className={cn(
124
+ "text-xs md:text-sm",
125
+ classNames?.description
126
+ )}
127
+ >
128
+ {localization?.DELETE_ORGANIZATION_DESCRIPTION}
129
+ </DialogDescription>
130
+ </DialogHeader>
131
+
132
+ <Card className={cn("my-2 flex-row p-4", classNames?.cell)}>
133
+ <OrganizationCellView
134
+ organization={organization}
135
+ localization={localization}
136
+ />
137
+ </Card>
138
+
139
+ <Form {...form}>
140
+ <form
141
+ onSubmit={form.handleSubmit(deleteOrganization)}
142
+ className="grid gap-6"
143
+ >
144
+ <FormField
145
+ control={form.control}
146
+ name="slug"
147
+ render={({ field }) => (
148
+ <FormItem>
149
+ <FormLabel className={classNames?.label}>
150
+ {
151
+ localization?.DELETE_ORGANIZATION_INSTRUCTIONS
152
+ }
153
+
154
+ <span className="font-bold">
155
+ {organization.slug}
156
+ </span>
157
+ </FormLabel>
158
+
159
+ <FormControl>
160
+ <Input
161
+ placeholder={organization.slug}
162
+ className={classNames?.input}
163
+ autoComplete="off"
164
+ {...field}
165
+ />
166
+ </FormControl>
167
+
168
+ <FormMessage
169
+ className={classNames?.error}
170
+ />
171
+ </FormItem>
172
+ )}
173
+ />
174
+
175
+ <DialogFooter className={classNames?.dialog?.footer}>
176
+ <Button
177
+ type="button"
178
+ variant="secondary"
179
+ className={cn(
180
+ classNames?.button,
181
+ classNames?.secondaryButton
182
+ )}
183
+ onClick={() => onOpenChange?.(false)}
184
+ >
185
+ {localization.CANCEL}
186
+ </Button>
187
+
188
+ <Button
189
+ className={cn(
190
+ classNames?.button,
191
+ classNames?.destructiveButton
192
+ )}
193
+ disabled={isSubmitting}
194
+ variant="destructive"
195
+ type="submit"
196
+ >
197
+ {isSubmitting && (
198
+ <Loader2 className="animate-spin" />
199
+ )}
200
+
201
+ {localization?.DELETE_ORGANIZATION}
202
+ </Button>
203
+ </DialogFooter>
204
+ </form>
205
+ </Form>
206
+ </DialogContent>
207
+ </Dialog>
208
+ )
209
+ }
@@ -0,0 +1,156 @@
1
+ "use client"
2
+
3
+ import type { Organization } from "better-auth/plugins/organization"
4
+ import { EllipsisIcon, Loader2, XIcon } from "lucide-react"
5
+ import { useContext, useMemo, useState } from "react"
6
+
7
+ import { AuthUIContext } from "../../lib/auth-ui-provider"
8
+ import { cn, getLocalizedError } from "../../lib/utils"
9
+ import type { AuthLocalization } from "../../localization/auth-localization"
10
+ import type { Invitation } from "../../types/invitation"
11
+ import type { SettingsCardClassNames } from "../settings/shared/settings-card"
12
+ import { Button } from "../ui/button"
13
+ import { Card } from "../ui/card"
14
+ import {
15
+ DropdownMenu,
16
+ DropdownMenuContent,
17
+ DropdownMenuItem,
18
+ DropdownMenuTrigger
19
+ } from "../ui/dropdown-menu"
20
+ import { UserAvatar } from "../user-avatar"
21
+
22
+ export interface InvitationCellProps {
23
+ className?: string
24
+ classNames?: SettingsCardClassNames
25
+ invitation: Invitation
26
+ localization?: AuthLocalization
27
+ organization: Organization
28
+ }
29
+
30
+ export function InvitationCell({
31
+ className,
32
+ classNames,
33
+ invitation,
34
+ localization: localizationProp,
35
+ organization
36
+ }: InvitationCellProps) {
37
+ const {
38
+ authClient,
39
+ hooks: { useListInvitations },
40
+ organization: organizationOptions,
41
+ localization: contextLocalization,
42
+ toast
43
+ } = useContext(AuthUIContext)
44
+
45
+ const localization = useMemo(
46
+ () => ({ ...contextLocalization, ...localizationProp }),
47
+ [contextLocalization, localizationProp]
48
+ )
49
+
50
+ const [isLoading, setIsLoading] = useState(false)
51
+
52
+ const builtInRoles = [
53
+ { role: "owner", label: localization.OWNER },
54
+ { role: "admin", label: localization.ADMIN },
55
+ { role: "member", label: localization.MEMBER }
56
+ ]
57
+
58
+ const roles = [...builtInRoles, ...(organizationOptions?.customRoles || [])]
59
+ const role = roles.find((r) => r.role === invitation.role)
60
+
61
+ const { refetch } = useListInvitations({
62
+ query: { organizationId: organization?.id }
63
+ })
64
+
65
+ const handleCancelInvitation = async () => {
66
+ setIsLoading(true)
67
+
68
+ try {
69
+ await authClient.organization.cancelInvitation({
70
+ invitationId: invitation.id,
71
+ fetchOptions: { throw: true }
72
+ })
73
+
74
+ await refetch?.()
75
+
76
+ toast({
77
+ variant: "success",
78
+ message: localization.INVITATION_CANCELLED
79
+ })
80
+ } catch (error) {
81
+ toast({
82
+ variant: "error",
83
+ message: getLocalizedError({ error, localization })
84
+ })
85
+ }
86
+
87
+ setIsLoading(false)
88
+ }
89
+
90
+ return (
91
+ <Card
92
+ className={cn(
93
+ "flex-row items-center p-4",
94
+ className,
95
+ classNames?.cell
96
+ )}
97
+ >
98
+ <div className="flex flex-1 items-center gap-2">
99
+ <UserAvatar
100
+ className="my-0.5"
101
+ user={invitation}
102
+ localization={localization}
103
+ />
104
+
105
+ <div className="grid flex-1 text-left leading-tight">
106
+ <span className="truncate font-semibold text-sm">
107
+ {invitation.email}
108
+ </span>
109
+
110
+ <span className="truncate text-muted-foreground text-xs">
111
+ {localization.EXPIRES}{" "}
112
+ {invitation.expiresAt.toLocaleDateString()}
113
+ </span>
114
+ </div>
115
+ </div>
116
+
117
+ <span className="truncate text-sm opacity-70">{role?.label}</span>
118
+
119
+ <DropdownMenu>
120
+ <DropdownMenuTrigger asChild>
121
+ <Button
122
+ className={cn(
123
+ "relative ms-auto",
124
+ classNames?.button,
125
+ classNames?.outlineButton
126
+ )}
127
+ disabled={isLoading}
128
+ size="icon"
129
+ type="button"
130
+ variant="outline"
131
+ >
132
+ {isLoading ? (
133
+ <Loader2 className="animate-spin" />
134
+ ) : (
135
+ <EllipsisIcon className={classNames?.icon} />
136
+ )}
137
+ </Button>
138
+ </DropdownMenuTrigger>
139
+
140
+ <DropdownMenuContent
141
+ onCloseAutoFocus={(e) => e.preventDefault()}
142
+ >
143
+ <DropdownMenuItem
144
+ onClick={handleCancelInvitation}
145
+ disabled={isLoading}
146
+ variant="destructive"
147
+ >
148
+ <XIcon className={classNames?.icon} />
149
+
150
+ {localization.CANCEL_INVITATION}
151
+ </DropdownMenuItem>
152
+ </DropdownMenuContent>
153
+ </DropdownMenu>
154
+ </Card>
155
+ )
156
+ }
@@ -0,0 +1,258 @@
1
+ "use client"
2
+
3
+ import { zodResolver } from "@hookform/resolvers/zod"
4
+ import type { Organization } from "better-auth/plugins/organization"
5
+ import { Loader2 } from "lucide-react"
6
+ import { type ComponentProps, useContext, useMemo } from "react"
7
+ import { useForm } from "react-hook-form"
8
+ import * as z from "zod"
9
+ import { AuthUIContext } from "../../lib/auth-ui-provider"
10
+ import { cn, getLocalizedError } from "../../lib/utils"
11
+ import type { AuthLocalization } from "../../localization/auth-localization"
12
+ import type { SettingsCardClassNames } from "../settings/shared/settings-card"
13
+ import { Button } from "../ui/button"
14
+ import {
15
+ Dialog,
16
+ DialogContent,
17
+ DialogDescription,
18
+ DialogFooter,
19
+ DialogHeader,
20
+ DialogTitle
21
+ } from "../ui/dialog"
22
+ import {
23
+ Form,
24
+ FormControl,
25
+ FormField,
26
+ FormItem,
27
+ FormLabel,
28
+ FormMessage
29
+ } from "../ui/form"
30
+ import { Input } from "../ui/input"
31
+ import {
32
+ Select,
33
+ SelectContent,
34
+ SelectItem,
35
+ SelectTrigger,
36
+ SelectValue
37
+ } from "../ui/select"
38
+
39
+ export interface InviteMemberDialogProps extends ComponentProps<typeof Dialog> {
40
+ classNames?: SettingsCardClassNames
41
+ localization?: AuthLocalization
42
+ organization: Organization
43
+ }
44
+
45
+ export function InviteMemberDialog({
46
+ classNames,
47
+ localization: localizationProp,
48
+ onOpenChange,
49
+ organization,
50
+ ...props
51
+ }: InviteMemberDialogProps) {
52
+ const {
53
+ authClient,
54
+ hooks: { useListInvitations, useListMembers, useSession },
55
+ localization: contextLocalization,
56
+ toast,
57
+ organization: organizationOptions
58
+ } = useContext(AuthUIContext)
59
+
60
+ const localization = useMemo(
61
+ () => ({ ...contextLocalization, ...localizationProp }),
62
+ [contextLocalization, localizationProp]
63
+ )
64
+
65
+ const { data } = useListMembers({
66
+ query: { organizationId: organization.id }
67
+ })
68
+
69
+ const { refetch } = useListInvitations({
70
+ query: { organizationId: organization.id }
71
+ })
72
+
73
+ const members = data?.members
74
+
75
+ const { data: sessionData } = useSession()
76
+ const membership = members?.find((m) => m.userId === sessionData?.user.id)
77
+
78
+ const builtInRoles = [
79
+ { role: "owner", label: localization.OWNER },
80
+ { role: "admin", label: localization.ADMIN },
81
+ { role: "member", label: localization.MEMBER }
82
+ ] as const
83
+
84
+ const roles = [...builtInRoles, ...(organizationOptions?.customRoles || [])]
85
+ const availableRoles = roles.filter(
86
+ (role) => membership?.role === "owner" || role.role !== "owner"
87
+ )
88
+
89
+ const formSchema = z.object({
90
+ email: z
91
+ .string()
92
+ .min(1, { message: localization.EMAIL_REQUIRED })
93
+ .email({
94
+ message: localization.INVALID_EMAIL
95
+ }),
96
+ role: z.string().min(1, {
97
+ message: `${localization.ROLE} ${localization.IS_REQUIRED}`
98
+ })
99
+ })
100
+
101
+ const form = useForm({
102
+ resolver: zodResolver(formSchema),
103
+ defaultValues: {
104
+ email: "",
105
+ role: "member"
106
+ }
107
+ })
108
+
109
+ const isSubmitting = form.formState.isSubmitting
110
+
111
+ async function onSubmit({ email, role }: z.infer<typeof formSchema>) {
112
+ try {
113
+ await authClient.organization.inviteMember({
114
+ email,
115
+ role: role as (typeof builtInRoles)[number]["role"],
116
+ organizationId: organization.id,
117
+ fetchOptions: { throw: true }
118
+ })
119
+
120
+ await refetch?.()
121
+
122
+ onOpenChange?.(false)
123
+ form.reset()
124
+
125
+ toast({
126
+ variant: "success",
127
+ message:
128
+ localization.SEND_INVITATION_SUCCESS ||
129
+ "Invitation sent successfully"
130
+ })
131
+ } catch (error) {
132
+ toast({
133
+ variant: "error",
134
+ message: getLocalizedError({ error, localization })
135
+ })
136
+ }
137
+ }
138
+
139
+ return (
140
+ <Dialog onOpenChange={onOpenChange} {...props}>
141
+ <DialogContent className={classNames?.dialog?.content}>
142
+ <DialogHeader className={classNames?.dialog?.header}>
143
+ <DialogTitle
144
+ className={cn("text-lg md:text-xl", classNames?.title)}
145
+ >
146
+ {localization.INVITE_MEMBER}
147
+ </DialogTitle>
148
+
149
+ <DialogDescription
150
+ className={cn(
151
+ "text-xs md:text-sm",
152
+ classNames?.description
153
+ )}
154
+ >
155
+ {localization.INVITE_MEMBER_DESCRIPTION}
156
+ </DialogDescription>
157
+ </DialogHeader>
158
+
159
+ <Form {...form}>
160
+ <form
161
+ onSubmit={form.handleSubmit(onSubmit)}
162
+ className="space-y-6"
163
+ >
164
+ <FormField
165
+ control={form.control}
166
+ name="email"
167
+ render={({ field }) => (
168
+ <FormItem>
169
+ <FormLabel className={classNames?.label}>
170
+ {localization.EMAIL}
171
+ </FormLabel>
172
+
173
+ <FormControl>
174
+ <Input
175
+ placeholder={
176
+ localization.EMAIL_PLACEHOLDER
177
+ }
178
+ type="email"
179
+ {...field}
180
+ className={classNames?.input}
181
+ />
182
+ </FormControl>
183
+
184
+ <FormMessage />
185
+ </FormItem>
186
+ )}
187
+ />
188
+
189
+ <FormField
190
+ control={form.control}
191
+ name="role"
192
+ render={({ field }) => (
193
+ <FormItem>
194
+ <FormLabel className={classNames?.label}>
195
+ {localization.ROLE}
196
+ </FormLabel>
197
+
198
+ <Select
199
+ onValueChange={field.onChange}
200
+ defaultValue={field.value}
201
+ >
202
+ <FormControl>
203
+ <SelectTrigger>
204
+ <SelectValue />
205
+ </SelectTrigger>
206
+ </FormControl>
207
+
208
+ <SelectContent>
209
+ {availableRoles.map((role) => (
210
+ <SelectItem
211
+ key={role.role}
212
+ value={role.role}
213
+ >
214
+ {role.label}
215
+ </SelectItem>
216
+ ))}
217
+ </SelectContent>
218
+ </Select>
219
+
220
+ <FormMessage />
221
+ </FormItem>
222
+ )}
223
+ />
224
+
225
+ <DialogFooter className={classNames?.dialog?.footer}>
226
+ <Button
227
+ type="button"
228
+ variant="outline"
229
+ onClick={() => onOpenChange?.(false)}
230
+ className={cn(
231
+ classNames?.button,
232
+ classNames?.outlineButton
233
+ )}
234
+ >
235
+ {localization.CANCEL}
236
+ </Button>
237
+
238
+ <Button
239
+ type="submit"
240
+ className={cn(
241
+ classNames?.button,
242
+ classNames?.primaryButton
243
+ )}
244
+ disabled={isSubmitting}
245
+ >
246
+ {isSubmitting && (
247
+ <Loader2 className="animate-spin" />
248
+ )}
249
+
250
+ {localization.SEND_INVITATION}
251
+ </Button>
252
+ </DialogFooter>
253
+ </form>
254
+ </Form>
255
+ </DialogContent>
256
+ </Dialog>
257
+ )
258
+ }