UNPKG

@daveyplate/better-auth-ui

Version:

Plug & play shadcn/ui components for better-auth

826 lines (809 loc) 24.8 kB
"use client" import { accountViewPaths, authLocalization, authViewPaths, getLocalizedError, organizationViewPaths } from "./chunk-QGW7NU44.js"; // src/hooks/use-auth-data.ts import { useCallback, useContext as useContext4, useEffect as useEffect5, useRef, useState as useState3, useSyncExternalStore as useSyncExternalStore2 } from "react"; // src/lib/auth-data-cache.ts var AuthDataCache = class { cache = /* @__PURE__ */ new Map(); listeners = /* @__PURE__ */ new Map(); inFlightRequests = /* @__PURE__ */ new Map(); get(key) { return this.cache.get(key); } set(key, data) { const entry = { data, timestamp: Date.now(), isRefetching: false }; this.cache.set(key, entry); this.notify(key); } setRefetching(key, isRefetching) { const entry = this.cache.get(key); if (entry) { entry.isRefetching = isRefetching; this.notify(key); } } clear(key) { if (key) { this.cache.delete(key); this.inFlightRequests.delete(key); this.notify(key); } else { this.cache.clear(); this.inFlightRequests.clear(); const keys = Array.from(this.listeners.keys()); for (const key2 of keys) { this.notify(key2); } } } getInFlightRequest(key) { return this.inFlightRequests.get(key); } setInFlightRequest(key, promise) { this.inFlightRequests.set(key, promise); } removeInFlightRequest(key) { this.inFlightRequests.delete(key); } subscribe(key, callback) { if (!this.listeners.has(key)) { this.listeners.set(key, /* @__PURE__ */ new Set()); } this.listeners.get(key).add(callback); return () => { const callbacks = this.listeners.get(key); if (callbacks) { callbacks.delete(callback); if (callbacks.size === 0) { this.listeners.delete(key); } } }; } notify(key) { const callbacks = this.listeners.get(key); if (callbacks) { const callbackArray = Array.from(callbacks); for (const callback of callbackArray) { callback(); } } } }; var authDataCache = new AuthDataCache(); // src/lib/auth-ui-provider.tsx import { createContext, useMemo as useMemo2 } from "react"; import { toast } from "sonner"; // src/components/captcha/recaptcha-v3.tsx import { GoogleReCaptchaProvider, useGoogleReCaptcha } from "@wojtekmaj/react-recaptcha-v3"; import { useContext, useEffect as useEffect3 } from "react"; // src/hooks/use-hydrated.ts import { useSyncExternalStore } from "react"; function subscribe() { return () => { }; } function useIsHydrated() { return useSyncExternalStore( subscribe, () => true, () => false ); } // src/hooks/use-lang.ts import { useEffect, useState } from "react"; function useLang() { const [lang, setLang] = useState(); useEffect(() => { const checkLang = () => { const currentLang = document.documentElement.getAttribute("lang"); setLang(currentLang ?? void 0); }; checkLang(); const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.attributeName === "lang") { checkLang(); } } }); observer.observe(document.documentElement, { attributes: true }); return () => { observer.disconnect(); }; }, []); return { lang }; } // src/hooks/use-theme.ts import { useEffect as useEffect2, useState as useState2 } from "react"; function useTheme() { const [theme, setTheme] = useState2("light"); useEffect2(() => { const checkTheme = () => { var _a; const isDark = document.documentElement.classList.contains("dark") || ((_a = document.documentElement.getAttribute("style")) == null ? void 0 : _a.includes("color-scheme: dark")); setTheme(isDark ? "dark" : "light"); }; checkTheme(); const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.attributeName === "style" || mutation.attributeName === "class") { checkTheme(); } } }); observer.observe(document.documentElement, { attributes: true }); return () => { observer.disconnect(); }; }, []); return { theme }; } // src/components/captcha/recaptcha-v3.tsx import { jsx, jsxs } from "react/jsx-runtime"; function RecaptchaV3({ children }) { const isHydrated = useIsHydrated(); const { captcha } = useContext(AuthUIContext); if ((captcha == null ? void 0 : captcha.provider) !== "google-recaptcha-v3") return children; return /* @__PURE__ */ jsxs( GoogleReCaptchaProvider, { reCaptchaKey: captcha.siteKey, useEnterprise: captcha.enterprise, useRecaptchaNet: captcha.recaptchaNet, children: [ isHydrated && /* @__PURE__ */ jsx("style", { children: ` .grecaptcha-badge { visibility: hidden; border-radius: var(--radius) !important; --tw-shadow: 0 1px 2px 0 var(--tw-shadow-color, #0000000d); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow) !important; border-style: var(--tw-border-style) !important; border-width: 1px; } .dark .grecaptcha-badge { border-color: var(--input) !important; } ` }), /* @__PURE__ */ jsx(RecaptchaV3Style, {}), children ] } ); } function RecaptchaV3Style() { const { executeRecaptcha } = useGoogleReCaptcha(); const { theme } = useTheme(); const { lang } = useLang(); useEffect3(() => { if (!executeRecaptcha) return; const updateRecaptcha = async () => { const iframe = document.querySelector( "iframe[title='reCAPTCHA']" ); if (iframe) { const iframeSrcUrl = new URL(iframe.src); iframeSrcUrl.searchParams.set("theme", theme); if (lang) iframeSrcUrl.searchParams.set("hl", lang); iframe.src = iframeSrcUrl.toString(); } }; updateRecaptcha(); }, [executeRecaptcha, theme, lang]); return null; } // src/lib/organization-refetcher.tsx import { useContext as useContext3, useEffect as useEffect4 } from "react"; // src/hooks/use-current-organization.ts import { useContext as useContext2, useMemo } from "react"; function useCurrentOrganization({ slug: slugProp } = {}) { const { organization: organizationOptions, hooks: { useActiveOrganization, useListOrganizations } } = useContext2(AuthUIContext); const { pathMode, slug: contextSlug } = organizationOptions || {}; let data; let isPending; let isRefetching; let refetch; const { data: organizations, isPending: organizationsPending, isRefetching: organizationsRefetching } = useListOrganizations(); if (pathMode === "slug") { const slug = slugProp || contextSlug; data = organizations == null ? void 0 : organizations.find((organization) => organization.slug === slug); isPending = organizationsPending; isRefetching = organizationsRefetching; } else { const { data: activeOrganization, isPending: organizationPending, isRefetching: organizationRefetching, refetch: refetchOrganization } = useActiveOrganization(); refetch = refetchOrganization; data = activeOrganization; isPending = organizationPending; isRefetching = organizationRefetching; } return useMemo( () => ({ data, isPending, isRefetching, refetch }), [data, isPending, isRefetching, refetch] ); } // src/lib/organization-refetcher.tsx var OrganizationRefetcher = () => { const { hooks: { useListOrganizations, useSession }, organization: organizationOptions, navigate, redirectTo } = useContext3(AuthUIContext); const { slug, pathMode, personalPath } = organizationOptions || {}; const { data: sessionData } = useSession(); const { data: organization, isPending: organizationPending, isRefetching: organizationRefetching, refetch: refetchOrganization } = useCurrentOrganization(); const { refetch: refetchListOrganizations } = useListOrganizations(); const { data: organizations } = useListOrganizations(); useEffect4(() => { if (!(sessionData == null ? void 0 : sessionData.user.id)) return; if (organization || organizations) { refetchOrganization == null ? void 0 : refetchOrganization(); refetchListOrganizations == null ? void 0 : refetchListOrganizations(); } }, [sessionData == null ? void 0 : sessionData.user.id]); useEffect4(() => { if (organizationRefetching || organizationPending) return; if (slug && pathMode === "slug" && !organization) { navigate(personalPath || redirectTo); } }, [ organization, organizationRefetching, organizationPending, slug, pathMode, personalPath, navigate, redirectTo ]); return null; }; // src/lib/auth-ui-provider.tsx import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime"; var DefaultLink = ({ href, className, children }) => /* @__PURE__ */ jsx2("a", { className, href, children }); var defaultNavigate = (href) => { window.location.href = href; }; var defaultReplace = (href) => { window.location.replace(href); }; var defaultToast = ({ variant = "default", message }) => { if (variant === "default") { toast(message); } else { toast[variant](message); } }; var AuthUIContext = createContext( {} ); var 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: toast2 = defaultToast, viewPaths: viewPathsProp, navigate, replace, Link = DefaultLink, emailVerification: emailVerificationProp, ...props }) => { const authClient = authClientProp; const avatar = useMemo2(() => { 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 = useMemo2(() => { if (!emailVerificationProp) return; if (emailVerificationProp === true) { return { otp: false }; } return { otp: emailVerificationProp.otp ?? false }; }, [emailVerificationProp]); const account = useMemo2(() => { var _a; if (accountProp === false) return; if (accountProp === true || accountProp === void 0) { return { basePath: "/account", fields: ["image", "name"], viewPaths: accountViewPaths }; } const basePath2 = ((_a = accountProp.basePath) == null ? void 0 : _a.endsWith("/")) ? accountProp.basePath.slice(0, -1) : accountProp.basePath; return { basePath: basePath2 ?? "/account", fields: accountProp.fields || ["image", "name"], viewPaths: { ...accountViewPaths, ...accountProp.viewPaths } }; }, [accountProp]); const deleteUser = useMemo2(() => { if (!deleteUserProp) return; if (deleteUserProp === true) { return {}; } return deleteUserProp; }, [deleteUserProp]); const social = useMemo2(() => { if (!socialProp) return; return socialProp; }, [socialProp]); const genericOAuth = useMemo2(() => { if (!genericOAuthProp) return; return genericOAuthProp; }, [genericOAuthProp]); const credentials = useMemo2(() => { if (credentialsProp === false) return; if (credentialsProp === true) { return { forgotPassword: true, usernameRequired: true }; } return { ...credentialsProp, forgotPassword: (credentialsProp == null ? void 0 : credentialsProp.forgotPassword) ?? true, usernameRequired: (credentialsProp == null ? void 0 : credentialsProp.usernameRequired) ?? true }; }, [credentialsProp]); const signUp = useMemo2(() => { if (signUpProp === false) return; if (signUpProp === true || signUpProp === void 0) { return { fields: ["name"] }; } return { fields: signUpProp.fields || ["name"] }; }, [signUpProp]); const organization = useMemo2(() => { var _a; if (!organizationProp) return; if (organizationProp === true) { return { basePath: "/organization", viewPaths: organizationViewPaths, customRoles: [] }; } let logo; 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) }; } const basePath2 = ((_a = organizationProp.basePath) == null ? void 0 : _a.endsWith("/")) ? organizationProp.basePath.slice(0, -1) : organizationProp.basePath; return { ...organizationProp, logo, basePath: basePath2 ?? "/organization", customRoles: organizationProp.customRoles || [], viewPaths: { ...organizationViewPaths, ...organizationProp.viewPaths } }; }, [organizationProp]); const teams = useMemo2(() => { var _a, _b; 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: ((_a = teamsProp.colors) == null ? void 0 : _a.count) ?? 5, prefix: ((_b = teamsProp.colors) == null ? void 0 : _b.prefix) ?? "team" } }; }, [teamsProp, organization]); const defaultMutators = useMemo2(() => { 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 } }) }; }, [authClient]); const defaultHooks = useMemo2(() => { 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: () => { var _a; return authClient.$fetch( `/organization/list-invitations?organizationId=${((_a = params == null ? void 0 : params.query) == null ? void 0 : _a.organizationId) || ""}` ); }, cacheKey: `listInvitations:${JSON.stringify(params)}` }), useListUserInvitations: () => useAuthData({ queryFn: () => authClient.$fetch( "/organization/list-user-invitations" ), cacheKey: `listUserInvitations` }), useListMembers: (params) => useAuthData({ queryFn: () => { var _a; return authClient.$fetch( `/organization/list-members?organizationId=${((_a = params == null ? void 0 : params.query) == null ? void 0 : _a.organizationId) || ""}` ); }, cacheKey: `listMembers:${JSON.stringify(params)}` }), useListTeams: (params) => useAuthData({ queryFn: () => authClient.$fetch( `/organization/list-teams?organizationId=${(params == null ? void 0 : params.organizationId) || ""}` ), cacheKey: `listTeams:${JSON.stringify(params)}` }), useListTeamMembers: (params) => useAuthData({ queryFn: () => authClient.$fetch("/organization/list-team-members", { method: "POST", body: (params == null ? void 0 : params.teamId) ? { query: { teamId: params.teamId } } : void 0 }), cacheKey: `listTeamMembers:${JSON.stringify(params)}` }), useListUserTeams: () => useAuthData({ queryFn: () => authClient.$fetch("/organization/list-user-teams"), cacheKey: "listUserTeams" }) }; }, [authClient]); const viewPaths = useMemo2(() => { return { ...authViewPaths, ...viewPathsProp }; }, [viewPathsProp]); const localization = useMemo2(() => { return { ...authLocalization, ...localizationProp }; }, [localizationProp]); const hooks = useMemo2(() => { return { ...defaultHooks, ...hooksProp }; }, [defaultHooks, hooksProp]); const mutators = useMemo2(() => { return { ...defaultMutators, ...mutatorsProp }; }, [defaultMutators, mutatorsProp]); baseURL = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL; basePath = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath; const { data: sessionData } = hooks.useSession(); return /* @__PURE__ */ jsxs2( AuthUIContext.Provider, { value: { authClient, avatar, basePath: basePath === "/" ? "" : basePath, baseURL, captcha, redirectTo, changeEmail, credentials, deleteUser, emailVerification, freshAge, genericOAuth, hooks, mutators, localization, localizeErrors, nameRequired, organization, teams, account, signUp, social, toast: toast2, navigate: navigate || defaultNavigate, replace: replace || navigate || defaultReplace, viewPaths, Link, ...props }, children: [ sessionData && organization && /* @__PURE__ */ jsx2(OrganizationRefetcher, {}), (captcha == null ? void 0 : captcha.provider) === "google-recaptcha-v3" ? /* @__PURE__ */ jsx2(RecaptchaV3, { children }) : children ] } ); }; // src/hooks/use-auth-data.ts function useAuthData({ queryFn, cacheKey, staleTime = 1e4 // Default 10 seconds }) { var _a; const { hooks: { useSession }, toast: toast2, localization, localizeErrors } = useContext4(AuthUIContext); const { data: sessionData, isPending: sessionPending } = useSession(); const queryFnRef = useRef(queryFn); queryFnRef.current = queryFn; const stableCacheKey = cacheKey || queryFn.toString(); const cacheEntry = useSyncExternalStore2( useCallback( (callback) => authDataCache.subscribe(stableCacheKey, callback), [stableCacheKey] ), useCallback( () => authDataCache.get(stableCacheKey), [stableCacheKey] ), useCallback( () => authDataCache.get(stableCacheKey), [stableCacheKey] ) ); const initialized = useRef(false); const previousUserId = useRef(void 0); const [error, setError] = useState3(null); const refetch = useCallback(async () => { const existingRequest = authDataCache.getInFlightRequest(stableCacheKey); if (existingRequest) { try { const result = await existingRequest; if (result.error) { setError(result.error); } else { setError(null); } } catch (err) { setError(err); } return; } if ((cacheEntry == null ? void 0 : cacheEntry.data) !== void 0) { authDataCache.setRefetching(stableCacheKey, true); } const fetchPromise = queryFnRef.current(); authDataCache.setInFlightRequest(stableCacheKey, fetchPromise); try { const { data, error: error2 } = await fetchPromise; if (error2) { setError(error2); toast2({ variant: "error", message: getLocalizedError({ error: error2, localization, localizeErrors }) }); } else { setError(null); } authDataCache.set(stableCacheKey, data); } catch (err) { const error2 = err; setError(error2); toast2({ variant: "error", message: getLocalizedError({ error: error2, localization, localizeErrors }) }); } finally { authDataCache.setRefetching(stableCacheKey, false); authDataCache.removeInFlightRequest(stableCacheKey); } }, [stableCacheKey, toast2, localization, localizeErrors, cacheEntry]); useEffect5(() => { var _a2; const currentUserId = (_a2 = sessionData == null ? void 0 : sessionData.user) == null ? void 0 : _a2.id; if (!sessionData) { authDataCache.setRefetching(stableCacheKey, false); authDataCache.clear(stableCacheKey); initialized.current = false; previousUserId.current = void 0; return; } const userIdChanged = previousUserId.current !== void 0 && previousUserId.current !== currentUserId; if (userIdChanged) { authDataCache.clear(stableCacheKey); } const hasCachedData = (cacheEntry == null ? void 0 : cacheEntry.data) !== void 0; const isStale = !cacheEntry || Date.now() - cacheEntry.timestamp > staleTime; if (!initialized.current || !hasCachedData || userIdChanged || hasCachedData && isStale) { if (!hasCachedData || isStale) { initialized.current = true; refetch(); } } previousUserId.current = currentUserId; }, [ sessionData, (_a = sessionData == null ? void 0 : sessionData.user) == null ? void 0 : _a.id, stableCacheKey, refetch, cacheEntry, staleTime ]); const isPending = sessionPending || (cacheEntry == null ? void 0 : cacheEntry.data) === void 0 && !error; return { data: (cacheEntry == null ? void 0 : cacheEntry.data) ?? null, isPending, isRefetching: (cacheEntry == null ? void 0 : cacheEntry.isRefetching) ?? false, error, refetch }; } export { useIsHydrated, useLang, useTheme, useAuthData, useCurrentOrganization, AuthUIContext, AuthUIProvider };