"use client" import { createContext, type ReactNode, useMemo } from "react" import { toast } from "sonner" import { RecaptchaV3 } from "../components/captcha/recaptcha-v3" import { useAuthData } from "../hooks/use-auth-data" import { type AuthLocalization, authLocalization } from "../localization/auth-localization" import type { AccountOptions, AccountOptionsContext } from "../types/account-options" import type { AdditionalFields } from "../types/additional-fields" import type { AnyAuthClient } from "../types/any-auth-client" import type { AuthClient } from "../types/auth-client" import type { AuthHooks } from "../types/auth-hooks" import type { AuthMutators } from "../types/auth-mutators" import type { AvatarOptions } from "../types/avatar-options" import type { CaptchaOptions } from "../types/captcha-options" import type { CredentialsOptions } from "../types/credentials-options" import type { DeleteUserOptions } from "../types/delete-user-options" import type { EmailVerificationOptions } from "../types/email-verification-options" import type { GenericOAuthOptions } from "../types/generic-oauth-options" import type { GravatarOptions } from "../types/gravatar-options" import type { Link } from "../types/link" import type { OrganizationOptions, OrganizationOptionsContext } from "../types/organization-options" import type { RenderToast } from "../types/render-toast" import type { SignUpOptions } from "../types/sign-up-options" import type { SocialOptions } from "../types/social-options" import type { TeamOptions, TeamOptionsContext } from "../types/team-options" import { OrganizationRefetcher } from "./organization-refetcher" import type { AuthViewPaths } from "./view-paths" import { accountViewPaths, authViewPaths, organizationViewPaths } from "./view-paths" const DefaultLink: Link = ({ href, className, children }) => ( {children} ) const defaultNavigate = (href: string) => { window.location.href = href } const defaultReplace = (href: string) => { window.location.replace(href) } const defaultToast: RenderToast = ({ variant = "default", message }) => { if (variant === "default") { toast(message) } else { toast[variant](message) } } export type AuthUIContextType = { authClient: AuthClient /** * Additional fields for users */ additionalFields?: AdditionalFields /** * API Key plugin configuration */ apiKey?: | { /** * Prefix for API Keys */ prefix?: string /** * Metadata for API Keys */ metadata?: Record } | boolean /** * Avatar configuration * @default undefined */ avatar?: AvatarOptions /** * Base path for the auth views * @default "/auth" */ basePath: string /** * Front end base URL for auth API callbacks */ baseURL?: string /** * Captcha configuration */ captcha?: CaptchaOptions credentials?: CredentialsOptions /** * Default redirect URL after authenticating * @default "/" */ redirectTo: string /** * Enable or disable user change email support * @default true */ changeEmail?: boolean /** * User Account deletion configuration * @default undefined */ deleteUser?: DeleteUserOptions /** * Email verification configuration */ emailVerification?: EmailVerificationOptions /** * Freshness age for Session data * @default 60 * 60 * 24 */ freshAge: number /** * Generic OAuth provider configuration */ genericOAuth?: GenericOAuthOptions /** * Gravatar configuration */ gravatar?: boolean | GravatarOptions hooks: AuthHooks localization: typeof authLocalization /** * Enable or disable error localization. * When false, errors messages from backend will be used directly. * @default true */ localizeErrors: boolean /** * Enable or disable Magic Link support * @default false */ magicLink?: boolean /** * Enable or disable Email OTP support * @default false */ emailOTP?: boolean /** * Enable or disable Multi Session support * @default false */ multiSession?: boolean mutators: AuthMutators /** * Whether the name field should be required * @default true */ nameRequired?: boolean /** * Enable or disable One Tap support * @default false */ oneTap?: boolean /** * Perform some User updates optimistically * @default false */ optimistic?: boolean /** * Organization configuration */ organization?: OrganizationOptionsContext /** * Teams configuration (requires organizations to be enabled) */ teams?: TeamOptionsContext /** * Enable or disable Passkey support * @default false */ passkey?: boolean /** * Forces better-auth-tanstack to refresh the Session on the auth callback page * @default false */ persistClient?: boolean /** * Account configuration */ account?: AccountOptionsContext /** * Sign Up configuration */ signUp?: SignUpOptions /** * Social provider configuration */ social?: SocialOptions toast: RenderToast /** * Enable or disable two-factor authentication support * @default undefined */ twoFactor?: ("otp" | "totp")[] viewPaths: AuthViewPaths /** * Navigate to a new URL * @default window.location.href */ navigate: (href: string) => void /** * Called whenever the Session changes */ onSessionChange?: () => void | Promise /** * Replace the current URL * @default navigate */ replace: (href: string) => void /** * Custom Link component for navigation * @default */ Link: Link } export type AuthUIProviderProps = { children: ReactNode /** * Better Auth client returned from createAuthClient * @default Required * @remarks `AuthClient` */ authClient: AnyAuthClient /** * Enable account view & account configuration * @default { fields: ["image", "name"] } */ account?: boolean | Partial /** * Avatar configuration * @default undefined */ avatar?: boolean | Partial /** * User Account deletion configuration * @default undefined */ deleteUser?: DeleteUserOptions | boolean /** * ADVANCED: Custom hooks for fetching auth data */ hooks?: Partial /** * Customize the paths for the auth views * @default authViewPaths * @remarks `AuthViewPaths` */ viewPaths?: Partial /** * Email verification configuration * @default undefined */ emailVerification?: boolean | Partial /** * Render custom Toasts * @default Sonner */ toast?: RenderToast /** * Customize the Localization strings * @default authLocalization * @remarks `AuthLocalization` */ localization?: AuthLocalization /** * Enable or disable error localization. * When false, errors messages from backend will be used directly. * @default true */ localizeErrors?: boolean /** * ADVANCED: Custom mutators for updating auth data */ mutators?: Partial /** * Organization plugin configuration */ organization?: OrganizationOptions | boolean /** * Teams plugin configuration (requires organizations to be enabled) */ teams?: TeamOptions | boolean /** * Enable or disable Credentials support * @default { forgotPassword: true } */ credentials?: boolean | CredentialsOptions /** * Enable or disable Sign Up form * @default { fields: ["name"] } */ signUp?: SignUpOptions | boolean } & Partial< Omit< AuthUIContextType, | "authClient" | "viewPaths" | "localization" | "mutators" | "toast" | "hooks" | "avatar" | "account" | "deleteUser" | "credentials" | "signUp" | "organization" | "localizeErrors" | "teams" | "emailVerification" > > export const AuthUIContext = createContext( {} as unknown as AuthUIContextType ) export const AuthUIProvider = ({ children, authClient: authClientProp, account: accountProp, avatar: avatarProp, deleteUser: deleteUserProp, social: socialProp, genericOAuth: genericOAuthProp, basePath = "/auth", baseURL = "", captcha, redirectTo = "/", credentials: credentialsProp, changeEmail = true, freshAge = 60 * 60 * 24, hooks: hooksProp, mutators: mutatorsProp, localization: localizationProp, localizeErrors = true, nameRequired = true, organization: organizationProp, teams: teamsProp, signUp: signUpProp = true, toast = defaultToast, viewPaths: viewPathsProp, navigate, replace, Link = DefaultLink, emailVerification: emailVerificationProp, ...props }: AuthUIProviderProps) => { const authClient = authClientProp as AuthClient const avatar = useMemo(() => { if (!avatarProp) return if (avatarProp === true) { return { extension: "png", size: 128 } } return { upload: avatarProp.upload, delete: avatarProp.delete, extension: avatarProp.extension || "png", size: avatarProp.size || (avatarProp.upload ? 256 : 128), Image: avatarProp.Image } }, [avatarProp]) const emailVerification = useMemo< EmailVerificationOptions | undefined >(() => { if (!emailVerificationProp) return if (emailVerificationProp === true) { return { otp: false } } return { otp: emailVerificationProp.otp ?? false } }, [emailVerificationProp]) const account = useMemo(() => { if (accountProp === false) return if (accountProp === true || accountProp === undefined) { return { basePath: "/account", fields: ["image", "name"], viewPaths: accountViewPaths } } // Remove trailing slash from basePath const basePath = accountProp.basePath?.endsWith("/") ? accountProp.basePath.slice(0, -1) : accountProp.basePath return { basePath: basePath ?? "/account", fields: accountProp.fields || ["image", "name"], viewPaths: { ...accountViewPaths, ...accountProp.viewPaths } } }, [accountProp]) const deleteUser = useMemo(() => { if (!deleteUserProp) return if (deleteUserProp === true) { return {} } return deleteUserProp }, [deleteUserProp]) const social = useMemo(() => { if (!socialProp) return return socialProp }, [socialProp]) const genericOAuth = useMemo(() => { if (!genericOAuthProp) return return genericOAuthProp }, [genericOAuthProp]) const credentials = useMemo(() => { if (credentialsProp === false) return if (credentialsProp === true) { return { forgotPassword: true, usernameRequired: true } } return { ...credentialsProp, forgotPassword: credentialsProp?.forgotPassword ?? true, usernameRequired: credentialsProp?.usernameRequired ?? true } }, [credentialsProp]) const signUp = useMemo(() => { if (signUpProp === false) return if (signUpProp === true || signUpProp === undefined) { return { fields: ["name"] } } return { fields: signUpProp.fields || ["name"] } }, [signUpProp]) const organization = useMemo(() => { if (!organizationProp) return if (organizationProp === true) { return { basePath: "/organization", viewPaths: organizationViewPaths, customRoles: [] } } let logo: OrganizationOptionsContext["logo"] | undefined if (organizationProp.logo === true) { logo = { extension: "png", size: 128 } } else if (organizationProp.logo) { logo = { upload: organizationProp.logo.upload, delete: organizationProp.logo.delete, extension: organizationProp.logo.extension || "png", size: organizationProp.logo.size || (organizationProp.logo.upload ? 256 : 128) } } // Remove trailing slash from basePath const basePath = organizationProp.basePath?.endsWith("/") ? organizationProp.basePath.slice(0, -1) : organizationProp.basePath return { ...organizationProp, logo, basePath: basePath ?? "/organization", customRoles: organizationProp.customRoles || [], viewPaths: { ...organizationViewPaths, ...organizationProp.viewPaths } } }, [organizationProp]) const teams = useMemo(() => { if (!teamsProp || !organization) return if (teamsProp === true) { return { enabled: true, customRoles: [], colors: { count: 5, prefix: "team" } } } return { enabled: teamsProp.enabled ?? true, customRoles: teamsProp.customRoles || [], colors: { count: teamsProp.colors?.count ?? 5, prefix: teamsProp.colors?.prefix ?? "team" } } }, [teamsProp, organization]) const defaultMutators = useMemo(() => { return { deleteApiKey: (params) => authClient.apiKey.delete({ ...params, fetchOptions: { throw: true } }), deletePasskey: (params) => authClient.passkey.deletePasskey({ ...params, fetchOptions: { throw: true } }), revokeDeviceSession: (params) => authClient.multiSession.revoke({ ...params, fetchOptions: { throw: true } }), revokeSession: (params) => authClient.revokeSession({ ...params, fetchOptions: { throw: true } }), setActiveSession: (params) => authClient.multiSession.setActive({ ...params, fetchOptions: { throw: true } }), updateOrganization: (params) => authClient.organization.update({ ...params, fetchOptions: { throw: true } }), updateTeam: (params) => authClient.$fetch("/organization/update-team", { method: "POST", body: params, throw: true }), updateUser: (params) => authClient.updateUser({ ...params, fetchOptions: { throw: true } }), unlinkAccount: (params) => authClient.unlinkAccount({ ...params, fetchOptions: { throw: true } }) } as AuthMutators }, [authClient]) const defaultHooks = useMemo(() => { return { useSession: authClient.useSession, useListAccounts: () => useAuthData({ queryFn: authClient.listAccounts, cacheKey: "listAccounts" }), useAccountInfo: (params) => useAuthData({ queryFn: () => authClient.accountInfo(params), cacheKey: `accountInfo:${JSON.stringify(params)}` }), useListDeviceSessions: () => useAuthData({ queryFn: authClient.multiSession.listDeviceSessions, cacheKey: "listDeviceSessions" }), useListSessions: () => useAuthData({ queryFn: authClient.listSessions, cacheKey: "listSessions" }), useListPasskeys: authClient.useListPasskeys, useListApiKeys: () => useAuthData({ queryFn: authClient.apiKey.list, cacheKey: "listApiKeys" }), useActiveOrganization: authClient.useActiveOrganization, useListOrganizations: authClient.useListOrganizations, useHasPermission: (params) => useAuthData({ queryFn: () => authClient.$fetch("/organization/has-permission", { method: "POST", body: params }), cacheKey: `hasPermission:${JSON.stringify(params)}` }), useInvitation: (params) => useAuthData({ queryFn: () => authClient.organization.getInvitation(params), cacheKey: `invitation:${JSON.stringify(params)}` }), useListInvitations: (params) => useAuthData({ queryFn: () => authClient.$fetch( `/organization/list-invitations?organizationId=${ params?.query?.organizationId || "" }` ), cacheKey: `listInvitations:${JSON.stringify(params)}` }), useListUserInvitations: () => useAuthData({ queryFn: () => authClient.$fetch( "/organization/list-user-invitations" ), cacheKey: `listUserInvitations` }), useListMembers: (params) => useAuthData({ queryFn: () => authClient.$fetch( `/organization/list-members?organizationId=${ params?.query?.organizationId || "" }` ), cacheKey: `listMembers:${JSON.stringify(params)}` }), useListTeams: (params) => useAuthData({ queryFn: () => authClient.$fetch( `/organization/list-teams?organizationId=${ params?.organizationId || "" }` ), cacheKey: `listTeams:${JSON.stringify(params)}` }), useListTeamMembers: (params) => useAuthData({ queryFn: () => authClient.$fetch("/organization/list-team-members", { method: "POST", body: params?.teamId ? { query: { teamId: params.teamId } } : undefined }), cacheKey: `listTeamMembers:${JSON.stringify(params)}` }), useListUserTeams: () => useAuthData({ queryFn: () => authClient.$fetch("/organization/list-user-teams"), cacheKey: "listUserTeams" }) } as AuthHooks }, [authClient]) const viewPaths = useMemo(() => { return { ...authViewPaths, ...viewPathsProp } }, [viewPathsProp]) const localization = useMemo(() => { return { ...authLocalization, ...localizationProp } }, [localizationProp]) const hooks = useMemo(() => { return { ...defaultHooks, ...hooksProp } }, [defaultHooks, hooksProp]) const mutators = useMemo(() => { return { ...defaultMutators, ...mutatorsProp } }, [defaultMutators, mutatorsProp]) // Remove trailing slash from baseURL baseURL = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL // Remove trailing slash from basePath basePath = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath const { data: sessionData } = hooks.useSession() return ( {sessionData && organization && } {captcha?.provider === "google-recaptcha-v3" ? ( {children} ) : ( children )} ) }