"use client" import { ChevronsUpDown, LogInIcon, LogOutIcon, PlusCircleIcon, SettingsIcon, UserRoundPlus } from "lucide-react" import { type ComponentProps, Fragment, type ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react" import { useIsHydrated } from "../hooks/use-hydrated" import { AuthUIContext } from "../lib/auth-ui-provider" import { cn, getLocalizedError } from "../lib/utils" import type { AuthLocalization } from "../localization/auth-localization" import type { AnyAuthClient } from "../types/any-auth-client" import type { User } from "../types/auth-client" import { Button } from "./ui/button" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "./ui/dropdown-menu" import { UserAvatar, type UserAvatarClassNames } from "./user-avatar" import { UserView, type UserViewClassNames } from "./user-view" export interface UserButtonClassNames { base?: string skeleton?: string trigger?: { base?: string avatar?: UserAvatarClassNames user?: UserViewClassNames skeleton?: string } content?: { base?: string user?: UserViewClassNames avatar?: UserAvatarClassNames menuItem?: string separator?: string } } export interface UserButtonProps { className?: string classNames?: UserButtonClassNames align?: "center" | "start" | "end" alignOffset?: number side?: "top" | "right" | "bottom" | "left" sideOffset?: number additionalLinks?: ( | { href: string icon?: ReactNode label: ReactNode signedIn?: boolean separator?: boolean } | ReactNode )[] trigger?: ReactNode disableDefaultLinks?: boolean /** * @default authLocalization * @remarks `AuthLocalization` */ localization?: AuthLocalization } /** * Displays an interactive user button with dropdown menu functionality * * Renders a user interface element that can be displayed as either an icon or full button: * - Shows a user avatar or placeholder when in icon mode * - Displays user name and email with dropdown indicator in full mode * - Provides dropdown menu with authentication options (sign in/out, settings, etc.) * - Supports multi-session functionality for switching between accounts * - Can be customized with additional links and styling options */ export function UserButton({ className, classNames, align, alignOffset, side, sideOffset, trigger, additionalLinks, disableDefaultLinks, localization: propLocalization, size, ...props }: UserButtonProps & ComponentProps) { const { basePath, hooks: { useSession, useListDeviceSessions }, mutators: { setActiveSession }, localization: contextLocalization, multiSession, account: accountOptions, signUp, toast, viewPaths, onSessionChange, Link, localizeErrors } = useContext(AuthUIContext) const localization = useMemo( () => ({ ...contextLocalization, ...propLocalization }), [contextLocalization, propLocalization] ) let deviceSessions: | AnyAuthClient["$Infer"]["Session"][] | undefined | null = null let deviceSessionsPending = false if (multiSession) { const { data, isPending } = useListDeviceSessions() deviceSessions = data deviceSessionsPending = isPending } const { data: sessionData, isPending: sessionPending } = useSession() const user = sessionData?.user const [activeSessionPending, setActiveSessionPending] = useState(false) const isHydrated = useIsHydrated() const isPending = sessionPending || activeSessionPending || !isHydrated const switchAccount = useCallback( async (sessionToken: string) => { setActiveSessionPending(true) try { await setActiveSession({ sessionToken }) onSessionChange?.() } catch (error) { toast({ variant: "error", message: getLocalizedError({ error, localization, localizeErrors }) }) setActiveSessionPending(false) } }, [setActiveSession, onSessionChange, toast, localization, localizeErrors] ) // biome-ignore lint/correctness/useExhaustiveDependencies: ignore useEffect(() => { if (!multiSession) return setActiveSessionPending(false) }, [sessionData, multiSession]) const warningLogged = useRef(false) useEffect(() => { if (size || warningLogged.current) return console.warn( "[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." ) warningLogged.current = true }, [size]) return ( {trigger || (size === "icon" ? ( ) : ( ))} e.preventDefault()} >
{(user && !(user as User).isAnonymous) || isPending ? ( ) : (
{localization.ACCOUNT}
)}
{additionalLinks?.map((link, index) => { // Handle ReactNode directly if ( !link || typeof link !== "object" || !("href" in link) ) { return ( {link} ) } const { href, icon, label, signedIn, separator } = link if ( signedIn !== undefined && ((signedIn && !sessionData) || (!signedIn && sessionData)) ) { return null } return ( {icon} {label} {separator && ( )} ) })} {!user || (user as User).isAnonymous ? ( <> {localization.SIGN_IN} {signUp && ( {localization.SIGN_UP} )} ) : ( <> {!disableDefaultLinks && accountOptions && ( {localization.SETTINGS} )} {localization.SIGN_OUT} )} {user && multiSession && ( <> {!deviceSessions && deviceSessionsPending && ( <> )} {deviceSessions ?.filter( (sessionData) => sessionData.user.id !== user?.id ) .map(({ session, user }) => ( switchAccount(session.token) } > ))} {localization.ADD_ACCOUNT} )}
) }