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,409 @@
1
+ "use client"
2
+ import {
3
+ ChevronsUpDown,
4
+ LogInIcon,
5
+ LogOutIcon,
6
+ PlusCircleIcon,
7
+ SettingsIcon,
8
+ UserRoundPlus
9
+ } from "lucide-react"
10
+ import {
11
+ type ComponentProps,
12
+ Fragment,
13
+ type ReactNode,
14
+ useCallback,
15
+ useContext,
16
+ useEffect,
17
+ useMemo,
18
+ useRef,
19
+ useState
20
+ } from "react"
21
+
22
+ import { useIsHydrated } from "../hooks/use-hydrated"
23
+ import { AuthUIContext } from "../lib/auth-ui-provider"
24
+ import { cn, getLocalizedError } from "../lib/utils"
25
+ import type { AuthLocalization } from "../localization/auth-localization"
26
+ import type { AnyAuthClient } from "../types/any-auth-client"
27
+ import type { User } from "../types/auth-client"
28
+ import { Button } from "./ui/button"
29
+ import {
30
+ DropdownMenu,
31
+ DropdownMenuContent,
32
+ DropdownMenuItem,
33
+ DropdownMenuSeparator,
34
+ DropdownMenuTrigger
35
+ } from "./ui/dropdown-menu"
36
+ import { UserAvatar, type UserAvatarClassNames } from "./user-avatar"
37
+ import { UserView, type UserViewClassNames } from "./user-view"
38
+
39
+ export interface UserButtonClassNames {
40
+ base?: string
41
+ skeleton?: string
42
+ trigger?: {
43
+ base?: string
44
+ avatar?: UserAvatarClassNames
45
+ user?: UserViewClassNames
46
+ skeleton?: string
47
+ }
48
+ content?: {
49
+ base?: string
50
+ user?: UserViewClassNames
51
+ avatar?: UserAvatarClassNames
52
+ menuItem?: string
53
+ separator?: string
54
+ }
55
+ }
56
+
57
+ export interface UserButtonProps {
58
+ className?: string
59
+ classNames?: UserButtonClassNames
60
+ align?: "center" | "start" | "end"
61
+ alignOffset?: number
62
+ side?: "top" | "right" | "bottom" | "left"
63
+ sideOffset?: number
64
+ additionalLinks?: {
65
+ href: string
66
+ icon?: ReactNode
67
+ label: ReactNode
68
+ signedIn?: boolean
69
+ separator?: boolean
70
+ }[]
71
+ trigger?: ReactNode
72
+ disableDefaultLinks?: boolean
73
+ /**
74
+ * @default authLocalization
75
+ * @remarks `AuthLocalization`
76
+ */
77
+ localization?: AuthLocalization
78
+ }
79
+
80
+ /**
81
+ * Displays an interactive user button with dropdown menu functionality
82
+ *
83
+ * Renders a user interface element that can be displayed as either an icon or full button:
84
+ * - Shows a user avatar or placeholder when in icon mode
85
+ * - Displays user name and email with dropdown indicator in full mode
86
+ * - Provides dropdown menu with authentication options (sign in/out, settings, etc.)
87
+ * - Supports multi-session functionality for switching between accounts
88
+ * - Can be customized with additional links and styling options
89
+ */
90
+ export function UserButton({
91
+ className,
92
+ classNames,
93
+ align,
94
+ alignOffset,
95
+ side,
96
+ sideOffset,
97
+ trigger,
98
+ additionalLinks,
99
+ disableDefaultLinks,
100
+ localization: propLocalization,
101
+ size,
102
+ ...props
103
+ }: UserButtonProps & ComponentProps<typeof Button>) {
104
+ const {
105
+ basePath,
106
+ hooks: { useSession, useListDeviceSessions },
107
+ mutators: { setActiveSession },
108
+ localization: contextLocalization,
109
+ multiSession,
110
+ account: accountOptions,
111
+ signUp,
112
+ toast,
113
+ viewPaths,
114
+ onSessionChange,
115
+ Link
116
+ } = useContext(AuthUIContext)
117
+
118
+ const localization = useMemo(
119
+ () => ({ ...contextLocalization, ...propLocalization }),
120
+ [contextLocalization, propLocalization]
121
+ )
122
+
123
+ let deviceSessions:
124
+ | AnyAuthClient["$Infer"]["Session"][]
125
+ | undefined
126
+ | null = null
127
+ let deviceSessionsPending = false
128
+
129
+ if (multiSession) {
130
+ const { data, isPending } = useListDeviceSessions()
131
+ deviceSessions = data
132
+ deviceSessionsPending = isPending
133
+ }
134
+
135
+ const { data: sessionData, isPending: sessionPending } = useSession()
136
+ const user = sessionData?.user
137
+ const [activeSessionPending, setActiveSessionPending] = useState(false)
138
+
139
+ const isHydrated = useIsHydrated()
140
+ const isPending = sessionPending || activeSessionPending || !isHydrated
141
+
142
+ const switchAccount = useCallback(
143
+ async (sessionToken: string) => {
144
+ setActiveSessionPending(true)
145
+
146
+ try {
147
+ await setActiveSession({ sessionToken })
148
+
149
+ onSessionChange?.()
150
+ } catch (error) {
151
+ toast({
152
+ variant: "error",
153
+ message: getLocalizedError({ error, localization })
154
+ })
155
+ setActiveSessionPending(false)
156
+ }
157
+ },
158
+ [setActiveSession, onSessionChange, toast, localization]
159
+ )
160
+
161
+ // biome-ignore lint/correctness/useExhaustiveDependencies: ignore
162
+ useEffect(() => {
163
+ if (!multiSession) return
164
+
165
+ setActiveSessionPending(false)
166
+ }, [sessionData, multiSession])
167
+
168
+ const warningLogged = useRef(false)
169
+
170
+ useEffect(() => {
171
+ if (size || warningLogged.current) return
172
+
173
+ console.warn(
174
+ "[Better Auth UI] The `size` prop of `UserButton` no longer defaults to `icon`. Please pass `size='icon'` to the `UserButton` component to get the same behaviour as before. This warning will be removed in a future release. It can be suppressed in the meantime by defining the `size` prop."
175
+ )
176
+
177
+ warningLogged.current = true
178
+ }, [size])
179
+
180
+ return (
181
+ <DropdownMenu>
182
+ <DropdownMenuTrigger
183
+ asChild
184
+ className={cn(
185
+ size === "icon" && "rounded-full",
186
+ classNames?.trigger?.base
187
+ )}
188
+ >
189
+ {trigger ||
190
+ (size === "icon" ? (
191
+ <Button
192
+ size="icon"
193
+ className="size-fit rounded-full"
194
+ variant="ghost"
195
+ >
196
+ <UserAvatar
197
+ key={user?.image}
198
+ isPending={isPending}
199
+ className={cn(className, classNames?.base)}
200
+ classNames={classNames?.trigger?.avatar}
201
+ user={user}
202
+ aria-label={localization.ACCOUNT}
203
+ localization={localization}
204
+ />
205
+ </Button>
206
+ ) : (
207
+ <Button
208
+ className={cn(
209
+ "!p-2 h-fit",
210
+ className,
211
+ classNames?.trigger?.base
212
+ )}
213
+ size={size}
214
+ {...props}
215
+ >
216
+ <UserView
217
+ size={size}
218
+ user={
219
+ !(user as User)?.isAnonymous ? user : null
220
+ }
221
+ isPending={isPending}
222
+ classNames={classNames?.trigger?.user}
223
+ localization={localization}
224
+ />
225
+
226
+ <ChevronsUpDown className="ml-auto" />
227
+ </Button>
228
+ ))}
229
+ </DropdownMenuTrigger>
230
+
231
+ <DropdownMenuContent
232
+ className={cn(
233
+ "w-[--radix-dropdown-menu-trigger-width] min-w-56 max-w-64",
234
+ classNames?.content?.base
235
+ )}
236
+ align={align}
237
+ alignOffset={alignOffset}
238
+ side={side}
239
+ sideOffset={sideOffset}
240
+ onCloseAutoFocus={(e) => e.preventDefault()}
241
+ >
242
+ <div className={cn("p-2", classNames?.content?.menuItem)}>
243
+ {(user && !(user as User).isAnonymous) || isPending ? (
244
+ <UserView
245
+ user={user}
246
+ isPending={isPending}
247
+ classNames={classNames?.content?.user}
248
+ localization={localization}
249
+ />
250
+ ) : (
251
+ <div className="-my-1 text-muted-foreground text-xs">
252
+ {localization.ACCOUNT}
253
+ </div>
254
+ )}
255
+ </div>
256
+
257
+ <DropdownMenuSeparator
258
+ className={classNames?.content?.separator}
259
+ />
260
+
261
+ {additionalLinks?.map(
262
+ ({ href, icon, label, signedIn, separator }, index) =>
263
+ (signedIn === undefined ||
264
+ (signedIn && !!sessionData) ||
265
+ (!signedIn && !sessionData)) && (
266
+ <Fragment key={index}>
267
+ <Link href={href}>
268
+ <DropdownMenuItem
269
+ className={
270
+ classNames?.content?.menuItem
271
+ }
272
+ >
273
+ {icon}
274
+ {label}
275
+ </DropdownMenuItem>
276
+ </Link>
277
+ {separator && (
278
+ <DropdownMenuSeparator
279
+ className={
280
+ classNames?.content?.separator
281
+ }
282
+ />
283
+ )}
284
+ </Fragment>
285
+ )
286
+ )}
287
+
288
+ {!user || (user as User).isAnonymous ? (
289
+ <>
290
+ <Link href={`${basePath}/${viewPaths.SIGN_IN}`}>
291
+ <DropdownMenuItem
292
+ className={classNames?.content?.menuItem}
293
+ >
294
+ <LogInIcon />
295
+
296
+ {localization.SIGN_IN}
297
+ </DropdownMenuItem>
298
+ </Link>
299
+
300
+ {signUp && (
301
+ <Link href={`${basePath}/${viewPaths.SIGN_UP}`}>
302
+ <DropdownMenuItem
303
+ className={classNames?.content?.menuItem}
304
+ >
305
+ <UserRoundPlus />
306
+
307
+ {localization.SIGN_UP}
308
+ </DropdownMenuItem>
309
+ </Link>
310
+ )}
311
+ </>
312
+ ) : (
313
+ <>
314
+ {!disableDefaultLinks && accountOptions && (
315
+ <Link
316
+ href={`${accountOptions.basePath}/${accountOptions.viewPaths?.SETTINGS}`}
317
+ >
318
+ <DropdownMenuItem
319
+ className={classNames?.content?.menuItem}
320
+ >
321
+ <SettingsIcon />
322
+
323
+ {localization.SETTINGS}
324
+ </DropdownMenuItem>
325
+ </Link>
326
+ )}
327
+
328
+ <Link href={`${basePath}/${viewPaths.SIGN_OUT}`}>
329
+ <DropdownMenuItem
330
+ className={classNames?.content?.menuItem}
331
+ >
332
+ <LogOutIcon />
333
+
334
+ {localization.SIGN_OUT}
335
+ </DropdownMenuItem>
336
+ </Link>
337
+ </>
338
+ )}
339
+
340
+ {user && multiSession && (
341
+ <>
342
+ <DropdownMenuSeparator
343
+ className={classNames?.content?.separator}
344
+ />
345
+
346
+ {!deviceSessions && deviceSessionsPending && (
347
+ <>
348
+ <DropdownMenuItem
349
+ disabled
350
+ className={classNames?.content?.menuItem}
351
+ >
352
+ <UserView
353
+ isPending={true}
354
+ classNames={classNames?.content?.user}
355
+ />
356
+ </DropdownMenuItem>
357
+
358
+ <DropdownMenuSeparator
359
+ className={classNames?.content?.separator}
360
+ />
361
+ </>
362
+ )}
363
+
364
+ {deviceSessions
365
+ ?.filter(
366
+ (sessionData) =>
367
+ sessionData.user.id !== user?.id
368
+ )
369
+ .map(({ session, user }) => (
370
+ <Fragment key={session.id}>
371
+ <DropdownMenuItem
372
+ className={
373
+ classNames?.content?.menuItem
374
+ }
375
+ onClick={() =>
376
+ switchAccount(session.token)
377
+ }
378
+ >
379
+ <UserView
380
+ user={user}
381
+ classNames={
382
+ classNames?.content?.user
383
+ }
384
+ />
385
+ </DropdownMenuItem>
386
+
387
+ <DropdownMenuSeparator
388
+ className={
389
+ classNames?.content?.separator
390
+ }
391
+ />
392
+ </Fragment>
393
+ ))}
394
+
395
+ <Link href={`${basePath}/${viewPaths.SIGN_IN}`}>
396
+ <DropdownMenuItem
397
+ className={classNames?.content?.menuItem}
398
+ >
399
+ <PlusCircleIcon />
400
+
401
+ {localization.ADD_ACCOUNT}
402
+ </DropdownMenuItem>
403
+ </Link>
404
+ </>
405
+ )}
406
+ </DropdownMenuContent>
407
+ </DropdownMenu>
408
+ )
409
+ }
@@ -0,0 +1,138 @@
1
+ "use client"
2
+
3
+ import { useContext, useMemo } from "react"
4
+ import { AuthUIContext } from "../lib/auth-ui-provider"
5
+ import { cn } from "../lib/utils"
6
+ import type { AuthLocalization } from "../localization/auth-localization"
7
+ import type { Profile } from "../types/profile"
8
+ import { Skeleton } from "./ui/skeleton"
9
+ import { UserAvatar, type UserAvatarClassNames } from "./user-avatar"
10
+
11
+ export interface UserViewClassNames {
12
+ base?: string
13
+ avatar?: UserAvatarClassNames
14
+ content?: string
15
+ title?: string
16
+ subtitle?: string
17
+ skeleton?: string
18
+ }
19
+
20
+ export interface UserViewProps {
21
+ className?: string
22
+ classNames?: UserViewClassNames
23
+ isPending?: boolean
24
+ size?: "sm" | "default" | "lg" | null
25
+ user?: Profile | null
26
+ /**
27
+ * @default authLocalization
28
+ * @remarks `AuthLocalization`
29
+ */
30
+ localization?: AuthLocalization
31
+ }
32
+
33
+ /**
34
+ * Displays user information with avatar and details in a compact view
35
+ *
36
+ * Renders a user's profile information with appropriate fallbacks:
37
+ * - Shows avatar alongside user name and email when available
38
+ * - Shows loading skeletons when isPending is true
39
+ * - Falls back to generic "User" text when neither name nor email is available
40
+ * - Supports customization through classNames prop
41
+ */
42
+ export function UserView({
43
+ className,
44
+ classNames,
45
+ isPending,
46
+ size,
47
+ user,
48
+ localization: propLocalization
49
+ }: UserViewProps) {
50
+ const { localization: contextLocalization } = useContext(AuthUIContext)
51
+
52
+ const localization = useMemo(
53
+ () => ({ ...contextLocalization, ...propLocalization }),
54
+ [contextLocalization, propLocalization]
55
+ )
56
+
57
+ return (
58
+ <div
59
+ className={cn(
60
+ "flex items-center gap-2",
61
+ className,
62
+ classNames?.base
63
+ )}
64
+ >
65
+ <UserAvatar
66
+ className={cn(size !== "sm" && "my-0.5")}
67
+ classNames={classNames?.avatar}
68
+ isPending={isPending}
69
+ size={size}
70
+ user={user}
71
+ localization={localization}
72
+ />
73
+
74
+ <div
75
+ className={cn(
76
+ "grid flex-1 text-left leading-tight",
77
+ classNames?.content
78
+ )}
79
+ >
80
+ {isPending ? (
81
+ <>
82
+ <Skeleton
83
+ className={cn(
84
+ "max-w-full",
85
+ size === "lg" ? "h-4.5 w-32" : "h-3.5 w-24",
86
+ classNames?.title,
87
+ classNames?.skeleton
88
+ )}
89
+ />
90
+ {size !== "sm" && (
91
+ <Skeleton
92
+ className={cn(
93
+ "mt-1.5 max-w-full",
94
+ size === "lg" ? "h-3.5 w-40" : "h-3 w-32",
95
+ classNames?.subtitle,
96
+ classNames?.skeleton
97
+ )}
98
+ />
99
+ )}
100
+ </>
101
+ ) : (
102
+ <>
103
+ <span
104
+ className={cn(
105
+ "truncate font-semibold",
106
+ size === "lg" ? "text-base" : "text-sm",
107
+ classNames?.title
108
+ )}
109
+ >
110
+ {user?.displayName ||
111
+ user?.name ||
112
+ user?.fullName ||
113
+ user?.firstName ||
114
+ user?.displayUsername ||
115
+ user?.username ||
116
+ user?.email ||
117
+ localization?.USER}
118
+ </span>
119
+
120
+ {!user?.isAnonymous &&
121
+ size !== "sm" &&
122
+ (user?.name || user?.username) && (
123
+ <span
124
+ className={cn(
125
+ "truncate opacity-70",
126
+ size === "lg" ? "text-sm" : "text-xs",
127
+ classNames?.subtitle
128
+ )}
129
+ >
130
+ {user?.email}
131
+ </span>
132
+ )}
133
+ </>
134
+ )}
135
+ </div>
136
+ </div>
137
+ )
138
+ }