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
JavaScript
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
};