UNPKG

@frank-auth/react

Version:

Flexible and customizable React UI components for Frank Authentication

549 lines (481 loc) 13.9 kB
/** * @frank-auth/react - useConfig Hook * * Configuration hook that provides access to UI configuration, feature flags, * localization settings, and organization-specific configuration. */ import { useCallback, useMemo } from "react"; import type { AppearanceConfig, ComponentOverrides, FrankAuthUIConfig, LocalizationConfig, OrganizationConfig, Theme, } from "../config"; import { useConfig as useConfigProvider } from "../provider/config-provider"; import { useAuth } from "./use-auth"; import type { AuthFeatures, LinksPathConfig } from "../provider/types"; // ============================================================================ // Config Hook Interface // ============================================================================ export interface UseConfigReturn { // Core configuration config: FrankAuthUIConfig; publishableKey: string; apiUrl: string; userType: string; debug: boolean; // UI configuration theme: Theme; appearance: AppearanceConfig; localization: LocalizationConfig; components: ComponentOverrides; titleAlignment: "left" | "center" | "right"; linksPath?: LinksPathConfig; // Organization configuration organization: OrganizationConfig | undefined; organizationSettings: any; // Feature flags features: AuthFeatures; // Configuration methods updateConfig: (updates: Partial<FrankAuthUIConfig>) => void; setTheme: (theme: Partial<Theme>) => void; setAppearance: (appearance: Partial<AppearanceConfig>) => void; setLocale: (locale: string) => void; resetToDefaults: () => void; // Feature flag helpers hasFeature: (feature: keyof AuthFeatures) => boolean; requireFeature: (feature: keyof AuthFeatures) => void; // Validation isConfigValid: boolean; configErrors: string[]; // State helpers isLoaded: boolean; isMultiTenant: boolean; isCustomBranded: boolean; } // ============================================================================ // Main useConfig Hook // ============================================================================ /** * Configuration hook providing access to all UI configuration and settings * * @example Basic configuration access * ```tsx * import { useConfig } from '@frank-auth/react'; * * function ConfigDisplay() { * const { * theme, * features, * hasFeature, * organization, * setTheme * } = useConfig(); * * return ( * <div> * <p>Theme mode: {theme.mode}</p> * <p>MFA enabled: {hasFeature('mfa') ? 'Yes' : 'No'}</p> * {organization && <p>Organization: {organization.name}</p>} * * <button onClick={() => setTheme({ mode: 'dark' })}> * Switch to Dark Mode * </button> * </div> * ); * } * ``` * * @example Feature-based conditional rendering * ```tsx * function ConditionalFeatures() { * const { hasFeature, requireFeature } = useConfig(); * * // Conditional rendering * if (!hasFeature('mfa')) { * return <div>MFA not available</div>; * } * * // Feature requirement (throws if not available) * const handleMFASetup = () => { * requireFeature('mfa'); * // MFA setup logic... * }; * * return <button onClick={handleMFASetup}>Setup MFA</button>; * } * ``` * * @example Organization-specific configuration * ```tsx * function OrganizationConfig() { * const { * organization, * organizationSettings, * isMultiTenant, * isCustomBranded * } = useConfig(); * * if (!isMultiTenant) { * return <div>Single tenant mode</div>; * } * * return ( * <div> * <h3>{organization?.name}</h3> * {isCustomBranded && ( * <img src={organizationSettings?.branding?.logo} alt="Organization Logo" /> * )} * <p>MFA Required: {organizationSettings?.mfaRequired ? 'Yes' : 'No'}</p> * </div> * ); * } * ``` */ export function useConfig(): UseConfigReturn { const configProvider = useConfigProvider(); const { activeOrganization } = useAuth(); // Feature flag helpers const hasFeature = useCallback( (feature: keyof AuthFeatures): boolean => { return configProvider.features[feature]; }, [configProvider.features], ); const requireFeature = useCallback( (feature: keyof AuthFeatures): void => { if (!configProvider.features[feature]) { throw new Error(`Feature ${feature} is not enabled`); } }, [configProvider.features], ); // Configuration validation const { isConfigValid, configErrors } = useMemo(() => { const errors: string[] = []; // Validate required configuration if (!configProvider.publishableKey) { errors.push("Publishable key is required"); } if (!configProvider.userType) { errors.push("User type is required"); } // Validate publishable key format if ( configProvider.publishableKey && !/^pk_(test|live)_[a-zA-Z0-9_]+$/.test(configProvider.publishableKey) ) { errors.push("Invalid publishable key format"); } // Validate API URL format if (configProvider.apiUrl) { try { new URL(configProvider.apiUrl); } catch { errors.push("Invalid API URL format"); } } // Validate user type if ( configProvider.userType && !["internal", "external", "end_user"].includes(configProvider.userType) ) { errors.push("Invalid user type"); } return { isConfigValid: errors.length === 0, configErrors: errors, }; }, [ configProvider.publishableKey, configProvider.userType, configProvider.apiUrl, ]); // State helpers const isMultiTenant = useMemo( () => !!configProvider.organizationConfig, [configProvider.organizationConfig], ); const isCustomBranded = useMemo(() => { return !!( configProvider.organizationSettings?.branding?.logoUrl || configProvider.organizationSettings?.branding?.primaryColor || configProvider.organizationSettings?.branding?.customCss ); }, [configProvider.organizationSettings]); return { // Core configuration config: configProvider.config, publishableKey: configProvider.publishableKey, apiUrl: configProvider.apiUrl, userType: configProvider.userType, debug: configProvider.debug, // UI configuration titleAlignment: configProvider.config?.appearance?.titleAlignment ?? "left", theme: configProvider.theme, appearance: configProvider.appearance, localization: configProvider.localization, components: configProvider.components, linksPath: configProvider.linksPath, // Organization configuration organization: configProvider.organizationConfig, organizationSettings: configProvider.organizationSettings, // Feature flags features: configProvider.features, // Configuration methods updateConfig: configProvider.updateConfig, setTheme: configProvider.setTheme, setAppearance: configProvider.setAppearance, setLocale: configProvider.setLocale, resetToDefaults: configProvider.resetToDefaults, // Feature flag helpers hasFeature, requireFeature, // Validation isConfigValid, configErrors, // State helpers isLoaded: configProvider.isLoaded, isMultiTenant, isCustomBranded, }; } // ============================================================================ // Specialized Config Hooks // ============================================================================ /** * Hook for feature flags only */ export function useFeatureFlags() { const { features, hasFeature, requireFeature } = useConfig(); return { features, hasFeature, requireFeature, // Convenience methods for common features canSignUp: hasFeature("signUp"), canSignIn: hasFeature("signIn"), canResetPassword: hasFeature("passwordReset"), hasMFA: hasFeature("mfa"), hasPasskeys: hasFeature("passkeys"), hasOAuth: hasFeature("oauth"), hasMagicLink: hasFeature("magicLink"), hasSSO: hasFeature("sso"), hasOrganizationManagement: hasFeature("organizationManagement"), hasUserProfile: hasFeature("userProfile"), hasSessionManagement: hasFeature("sessionManagement"), }; } /** * Hook for theme configuration */ export function useThemeConfig() { const { theme, setTheme, appearance, setAppearance } = useConfig(); return { theme, appearance, setTheme, setAppearance, // Theme helpers mode: theme.mode, colors: theme.colors, typography: theme.typography, spacing: theme.spacing, borderRadius: theme.borderRadius, shadows: theme.shadows, // Appearance helpers layout: appearance.layout, components: appearance.components, branding: appearance.branding, // Quick theme updates setMode: (mode: "light" | "dark" | "system") => setTheme({ mode }), setPrimaryColor: (color: string) => setTheme({ colors: { ...theme.colors, primary: { ...theme.colors.primary, DEFAULT: color }, }, }), setSecondaryColor: (color: string) => setTheme({ colors: { ...theme.colors, secondary: { ...theme.colors.secondary, DEFAULT: color }, }, }), }; } /** * Hook for localization configuration */ export function useLocalizationConfig() { const { localization, setLocale } = useConfig(); return { localization, setLocale, // Localization helpers currentLocale: localization.defaultLocale, supportedLocales: localization.supportedLocales, dateFormat: localization.dateFormat, timeFormat: localization.timeFormat, direction: localization.direction, // Quick locale updates isRTL: localization.direction === "rtl", setEnglish: () => setLocale("en"), setSpanish: () => setLocale("es"), setFrench: () => setLocale("fr"), setGerman: () => setLocale("de"), }; } /** * Hook for organization-specific configuration */ export function useOrganizationConfiguration() { const { organization, organizationSettings, isMultiTenant, isCustomBranded, features, } = useConfig(); const organizationFeatures = useMemo(() => { if (!organization) return {}; return { sso: organization.features?.sso || false, mfa: organization.features?.mfa || false, auditLogs: organization.features?.auditLogs || false, customBranding: organization.features?.customBranding || false, apiAccess: organization.features?.apiAccess || false, }; }, [organization]); return { organization, organizationSettings, isMultiTenant, isCustomBranded, features: organizationFeatures, // Organization helpers organizationId: organization?.id || null, organizationName: organization?.name || null, organizationSlug: organization?.slug || null, // Settings helpers allowPublicSignup: organizationSettings?.allowPublicSignup || false, requireEmailVerification: organizationSettings?.requireEmailVerification || false, requirePhoneVerification: organizationSettings?.requirePhoneVerification || false, mfaRequired: organizationSettings?.mfaRequired || false, allowedMfaMethods: organizationSettings?.allowedMfaMethods || [], // Branding helpers branding: organizationSettings?.branding, logo: organizationSettings?.branding?.logo, primaryColor: organizationSettings?.branding?.primaryColor, secondaryColor: organizationSettings?.branding?.secondaryColor, customCSS: organizationSettings?.branding?.customCSS, // Limits limits: organization?.limits, maxUsers: organization?.limits?.maxUsers || 0, maxSessions: organization?.limits?.maxSessions || 0, apiRequestLimit: organization?.limits?.apiRequestLimit || 0, }; } /** * Hook for component overrides */ export function useComponentConfiguration() { const { components } = useConfig(); const getComponent = useCallback( <T extends keyof ComponentOverrides>( componentName: T, defaultComponent: any, ) => { return components[componentName] || defaultComponent; }, [components], ); const hasOverride = useCallback( (componentName: keyof ComponentOverrides) => { return !!components[componentName]; }, [components], ); return { components, getComponent, hasOverride, // Component availability checks hasCustomLayout: hasOverride("Layout"), hasCustomHeader: hasOverride("Header"), hasCustomFooter: hasOverride("Footer"), hasCustomSignInForm: hasOverride("SignInForm"), hasCustomSignUpForm: hasOverride("SignUpForm"), hasCustomUserProfile: hasOverride("UserProfile"), hasCustomButton: hasOverride("Button"), hasCustomInput: hasOverride("Input"), hasCustomCard: hasOverride("Card"), hasCustomModal: hasOverride("Modal"), }; } /** * Hook for configuration validation and debugging */ export function useConfigValidation() { const { isConfigValid, configErrors, debug, publishableKey, apiUrl, userType, } = useConfig(); const warnings = useMemo(() => { const warnings: string[] = []; // Development warnings if ( publishableKey?.startsWith("pk_test_") && typeof window !== "undefined" && window.location.hostname !== "localhost" && !window.location.hostname.includes("127.0.0.1") ) { warnings.push("Using test publishable key in production environment"); } if ( apiUrl?.startsWith("http://") && typeof window !== "undefined" && window.location.protocol === "https:" ) { warnings.push("Using HTTP API URL on HTTPS site"); } return warnings; }, [publishableKey, apiUrl]); return { isValid: isConfigValid, errors: configErrors, warnings, debug, // Helper methods hasErrors: configErrors.length > 0, hasWarnings: warnings.length > 0, isProduction: !publishableKey?.startsWith("pk_test_"), isTestMode: publishableKey?.startsWith("pk_test_"), // Validation helpers validatePublishableKey: () => { if (!publishableKey) return false; return /^pk_(test|live|standalone)_[a-zA-Z0-9_]+$/.test(publishableKey); }, validateApiUrl: () => { if (!apiUrl) return true; // Optional try { new URL(apiUrl); return true; } catch { return false; } }, validateUserType: () => { return ["internal", "external", "end_user"].includes(userType); }, }; }