UNPKG

better-auth-feature-flags

Version:

Ship features safely with feature flags, A/B testing, and progressive rollouts - Better Auth plugin for modern release management

415 lines (413 loc) 12.6 kB
import { __name } from "./chunk-SHUYVCID.js"; // src/react.tsx import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; import { Fragment, jsx } from "react/jsx-runtime"; var FeatureFlagsContext = createContext( null ); var useFeatureFlagsContext = /* @__PURE__ */ __name(() => { const context = useContext(FeatureFlagsContext); if (!context) { throw new Error( "Feature flags hooks must be used within FeatureFlagsProvider" ); } return context; }, "useFeatureFlagsContext"); function FeatureFlagsProvider({ client, children, initialFlags = {}, fetchOnMount = true, context: additionalContext }) { const [flags, setFlags] = useState(initialFlags); const [loading, setLoading] = useState(fetchOnMount); const [error, setError] = useState(null); const [needsRefresh, setNeedsRefresh] = useState(false); const [lastSessionId, setLastSessionId] = useState(null); useEffect(() => { if (additionalContext) { client.featureFlags.setContext(additionalContext); } }, [client, additionalContext]); useEffect(() => { const unsubscribe = client.featureFlags.subscribe( (newFlags) => { if (Object.keys(newFlags).length === 0 && Object.keys(flags).length > 0) { setNeedsRefresh(true); } else { setFlags(newFlags); } } ); return unsubscribe; }, [client, flags]); useEffect(() => { if (needsRefresh) { setNeedsRefresh(false); setLoading(true); client.featureFlags.bootstrap().then((fetchedFlags) => { setFlags(fetchedFlags); setError(null); }).catch((err) => { setError(err); }).finally(() => { setLoading(false); }); } }, [needsRefresh, client]); useEffect(() => { if (fetchOnMount) { setLoading(true); client.featureFlags.bootstrap().then((fetchedFlags) => { setFlags(fetchedFlags); setError(null); }).catch((err) => { setError(err); }).finally(() => { setLoading(false); }); } }, [client, fetchOnMount]); useEffect(() => { if ("$sessionSignal" in client && client.$sessionSignal) { const unsubscribe = client.$sessionSignal.subscribe(() => { const currentSession = client.session; const currentSessionId = currentSession?.session?.id || null; if (currentSessionId !== lastSessionId) { setLastSessionId(currentSessionId); setLoading(true); client.featureFlags.refresh().then(() => { setError(null); }).catch((err) => { setError(err); }).finally(() => { setLoading(false); }); } }); return unsubscribe; } }, [client, lastSessionId]); const refresh = useCallback(async () => { setLoading(true); try { await client.featureFlags.refresh(); setError(null); } catch (err) { setError(err); } finally { setLoading(false); } }, [client]); const value = useMemo( () => ({ client, flags, loading, error, refresh }), [client, flags, loading, error, refresh] ); return /* @__PURE__ */ jsx(FeatureFlagsContext.Provider, { value, children }); } __name(FeatureFlagsProvider, "FeatureFlagsProvider"); function useFeatureFlag(flag, defaultValue = false) { const { client, flags } = useFeatureFlagsContext(); const [value, setValue] = useState( flags[flag] !== void 0 ? Boolean(flags[flag]) : defaultValue ); useEffect(() => { if (flags[flag] !== void 0) { setValue(Boolean(flags[flag])); } else { client.featureFlags.isEnabled(flag, defaultValue).then(setValue).catch(() => setValue(defaultValue)); } }, [client, flag, flags, defaultValue]); return value; } __name(useFeatureFlag, "useFeatureFlag"); function useFeatureFlagValue(flag, defaultValue) { const { client, flags } = useFeatureFlagsContext(); const [value, setValue] = useState( flags[flag] !== void 0 ? flags[flag] : defaultValue ); useEffect(() => { if (flags[flag] !== void 0) { setValue(flags[flag]); } else { client.featureFlags.getValue(flag, defaultValue).then(setValue).catch(() => setValue(defaultValue)); } }, [client, flag, flags, defaultValue]); return value; } __name(useFeatureFlagValue, "useFeatureFlagValue"); function useFeatureFlags() { const { flags } = useFeatureFlagsContext(); return flags; } __name(useFeatureFlags, "useFeatureFlags"); function useVariant(flag) { const { client, flags } = useFeatureFlagsContext(); const [variant, setVariant] = useState(null); useEffect(() => { client.featureFlags.getVariant(flag).then(setVariant).catch(() => setVariant(null)); }, [client, flag, flags]); return variant; } __name(useVariant, "useVariant"); function useTrackEvent() { const { client } = useFeatureFlagsContext(); return useCallback( (flag, event, value) => { return client.featureFlags.track(flag, event, value); }, [client] ); } __name(useTrackEvent, "useTrackEvent"); function useFeatureFlagsState() { const { loading, error, refresh } = useFeatureFlagsContext(); return { loading, error, refresh }; } __name(useFeatureFlagsState, "useFeatureFlagsState"); function useFeatureFlagsCacheInfo() { const { client } = useFeatureFlagsContext(); const [cacheInfo, setCacheInfo] = useState({ cacheEnabled: false, flagCount: 0 }); useEffect(() => { const updateCacheInfo = /* @__PURE__ */ __name(() => { client.featureFlags.bootstrap().then((flags) => { setCacheInfo({ cacheEnabled: true, flagCount: Object.keys(flags).length }); }); }, "updateCacheInfo"); updateCacheInfo(); const unsubscribe = client.featureFlags.subscribe(updateCacheInfo); return unsubscribe; }, [client]); return cacheInfo; } __name(useFeatureFlagsCacheInfo, "useFeatureFlagsCacheInfo"); function useFeatureFlagSuspense(flag, defaultValue = false) { const { client, flags } = useFeatureFlagsContext(); if (flags[flag] !== void 0) { return Boolean(flags[flag]); } throw client.featureFlags.isEnabled(flag, defaultValue).then(() => { return Boolean(flags[flag] ?? defaultValue); }); } __name(useFeatureFlagSuspense, "useFeatureFlagSuspense"); function useFeatureFlagValueSuspense(flag, defaultValue) { const { client, flags } = useFeatureFlagsContext(); if (flags[flag] !== void 0) { return flags[flag]; } throw client.featureFlags.getValue(flag, defaultValue).then(() => { return flags[flag] ?? defaultValue; }); } __name(useFeatureFlagValueSuspense, "useFeatureFlagValueSuspense"); function useFeatureFlagsSuspense() { const { client, flags, loading } = useFeatureFlagsContext(); if (!loading && Object.keys(flags).length > 0) { return flags; } throw client.featureFlags.bootstrap().then((loadedFlags) => { return loadedFlags; }); } __name(useFeatureFlagsSuspense, "useFeatureFlagsSuspense"); function useTrackEventWithIdempotency() { const { client } = useFeatureFlagsContext(); return useCallback( (flag, event, value, options) => { return client.featureFlags.track(flag, event, value, options); }, [client] ); } __name(useTrackEventWithIdempotency, "useTrackEventWithIdempotency"); function useTrackEventBatch() { const { client } = useFeatureFlagsContext(); return useCallback( (events, batchId) => { return client.featureFlags.trackBatch(events, { idempotencyKey: batchId }); }, [client] ); } __name(useTrackEventBatch, "useTrackEventBatch"); function Feature({ flag, fallback = null, validateAccess, children }) { const isEnabled = useFeatureFlag(flag); const flags = useFeatureFlags(); const hasAccess = useMemo(() => { if (!isEnabled) return false; if (validateAccess) return validateAccess(flags); return true; }, [isEnabled, validateAccess, flags]); return /* @__PURE__ */ jsx(Fragment, { children: hasAccess ? children : fallback }); } __name(Feature, "Feature"); var Variant = React.memo(/* @__PURE__ */ __name(function Variant2({ flag, children }) { const variant = useVariant(flag); const selectedChild = useMemo(() => { let defaultChild = null; let matchedChild = null; React.Children.forEach(children, (child) => { if (!React.isValidElement(child)) return; if (child.type === VariantCase) { const caseProps = child.props; if (variant && caseProps.variant === variant) { matchedChild = caseProps.children; } } else if (child.type === VariantDefault) { defaultChild = child.props.children; } }); return matchedChild || defaultChild; }, [children, variant]); return /* @__PURE__ */ jsx(Fragment, { children: selectedChild }); }, "Variant")); function VariantCase({ children }) { return /* @__PURE__ */ jsx(Fragment, { children }); } __name(VariantCase, "VariantCase"); function VariantDefault({ children }) { return /* @__PURE__ */ jsx(Fragment, { children }); } __name(VariantDefault, "VariantDefault"); Variant.Case = VariantCase; Variant.Default = VariantDefault; function FeatureSuspense({ flag, fallback = null, validateAccess, children }) { const isEnabled = useFeatureFlagSuspense(flag); const flags = useFeatureFlagsSuspense(); const hasAccess = useMemo(() => { if (!isEnabled) return false; if (validateAccess) return validateAccess(flags); return true; }, [isEnabled, validateAccess, flags]); return /* @__PURE__ */ jsx(Fragment, { children: hasAccess ? children : fallback }); } __name(FeatureSuspense, "FeatureSuspense"); var VariantSuspense = React.memo(/* @__PURE__ */ __name(function VariantSuspense2({ flag, children }) { const variant = useVariant(flag); const selectedChild = useMemo(() => { let defaultChild = null; let matchedChild = null; React.Children.forEach(children, (child) => { if (!React.isValidElement(child)) return; if (child.type === VariantCase) { const caseProps = child.props; if (variant && caseProps.variant === variant) { matchedChild = caseProps.children; } } else if (child.type === VariantDefault) { defaultChild = child.props.children; } }); return matchedChild || defaultChild; }, [children, variant]); return /* @__PURE__ */ jsx(Fragment, { children: selectedChild }); }, "VariantSuspense")); VariantSuspense.Case = VariantCase; VariantSuspense.Default = VariantDefault; var FeatureFlagErrorBoundary = class extends React.Component { static { __name(this, "FeatureFlagErrorBoundary"); } constructor(props) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error) { return { hasError: true, error }; } componentDidCatch(error) { this.props.onError?.(error); } render() { if (this.state.hasError) { const { fallback } = this.props; if (typeof fallback === "function") { const FallbackComponent = fallback; return /* @__PURE__ */ jsx(FallbackComponent, { error: this.state.error }); } return fallback; } return this.props.children; } }; function withFeatureFlags(Component) { return /* @__PURE__ */ __name(function WithFeatureFlagsComponent(props) { const flags = useFeatureFlags(); return /* @__PURE__ */ jsx(Component, { ...props, featureFlags: flags }); }, "WithFeatureFlagsComponent"); } __name(withFeatureFlags, "withFeatureFlags"); function withFeatureFlag(flag, fallback) { return function(Component) { return /* @__PURE__ */ __name(function WithFeatureFlagComponent(props) { const isEnabled = useFeatureFlag(flag); if (!isEnabled && fallback) { const FallbackComponent = fallback; return /* @__PURE__ */ jsx(FallbackComponent, { ...props }); } return isEnabled ? /* @__PURE__ */ jsx(Component, { ...props }) : null; }, "WithFeatureFlagComponent"); }; } __name(withFeatureFlag, "withFeatureFlag"); export { Feature, FeatureFlagErrorBoundary, FeatureFlagsProvider, FeatureSuspense, Variant, VariantSuspense, useFeatureFlag, useFeatureFlagSuspense, useFeatureFlagValue, useFeatureFlagValueSuspense, useFeatureFlags, useFeatureFlagsCacheInfo, useFeatureFlagsState, useFeatureFlagsSuspense, useTrackEvent, useTrackEventBatch, useTrackEventWithIdempotency, useVariant, withFeatureFlag, withFeatureFlags };