import React, { createContext, useCallback, useEffect, useState } from 'react' export interface Toggle { /** The application name that owns this toggle */ application: string /** Whether the toggle is currently enabled or disabled */ enabled: boolean /** Unique identifier for the toggle */ key: string /** Human-readable name for the toggle */ name: string } interface ToggleProviderContextValue { /** Array of available toggles, undefined while loading */ toggles: Toggle[] | undefined /** Function to set a toggle override */ setToggleOverride?: (key: string, value: boolean) => void } export const ToggleProviderContext = createContext({ toggles: [], setToggleOverride: () => {}, }) interface ToggleProviderProps { /** Child components that will have access to the toggle context */ children: React.ReactNode /** Distribution key used to identify which toggles to fetch from the CDN */ distributionKey: string /** Environment to fetch toggles for (determines which CDN endpoint to use) */ environment: 'development' | 'staging' | 'production' /** Optional callback function that gets called if there's an error fetching toggles */ errorCallback?: (error: string | Record) => void /** Optional callback function that gets called when toggle loading is finished (success or error) */ finishedLoadingCallback?: (isLoaded: boolean) => void } // uncomment this to test against staging // const TOGGLE_API_URL = 'https://stage-toggle-api.usepredict.com' const TOGGLE_API_URL = 'https://toggle-api.usepredict.com' const ToggleProvider = ({ children, distributionKey, environment, errorCallback, finishedLoadingCallback, }: ToggleProviderProps): React.JSX.Element => { const [togglesFromCDN, setTogglesFromCDN] = useState() const [toggleOverrides, setToggleOverrides] = useState< Record >({}) const setToggleOverride = useCallback((key: string, value: boolean) => { // Update cookie document.cookie = `${key}=${value.toString()}; path=/;` // Update local state setToggleOverrides((prev) => ({ ...prev, [key]: value, })) }, []) useEffect(() => { const controller = new AbortController() const { signal } = controller fetch(`${TOGGLE_API_URL}/cdn/${distributionKey}/${environment}`, { signal }) .then((response) => response.json()) .then((responseJson) => { setTogglesFromCDN(responseJson) finishedLoadingCallback?.(true) }) .catch((error) => { if (error?.name === 'AbortError') return console.error({ error }) if (errorCallback) { errorCallback(error) } setTogglesFromCDN([]) // If the query is cancelled, don't set finished loading to true. This fixes issues in strict mode if (!error?.message.toLowerCase().includes('canceled')) { finishedLoadingCallback?.(true) } }) return () => { controller.abort() } }, [distributionKey, environment, errorCallback, finishedLoadingCallback]) // Merge backend toggles with local overrides const mergedToggles = togglesFromCDN?.map((toggle) => ({ ...toggle, enabled: Object.prototype.hasOwnProperty.call(toggleOverrides, toggle.key) ? toggleOverrides[toggle.key] : toggle.enabled, })) return ( {children} ) } export default ToggleProvider