diff --git a/package.json b/package.json index 0fdad0e..d7f4714 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/payload-feature-flags", - "version": "0.0.12", + "version": "0.0.13", "description": "Feature flags plugin for Payload CMS - manage feature toggles, A/B tests, and gradual rollouts", "license": "MIT", "type": "module", diff --git a/src/exports/client.ts b/src/exports/client.ts index b3cbd5f..f4e109d 100644 --- a/src/exports/client.ts +++ b/src/exports/client.ts @@ -7,4 +7,5 @@ export { useRolloutCheck, withFeatureFlag, type FeatureFlag, + type FeatureFlagOptions, } from '../hooks/client.js' diff --git a/src/hooks/client.ts b/src/hooks/client.ts index da0b323..7e762a3 100644 --- a/src/hooks/client.ts +++ b/src/hooks/client.ts @@ -1,6 +1,5 @@ 'use client' import React, { useCallback, useEffect, useState } from 'react' -import { useConfig } from '@payloadcms/ui' export interface FeatureFlag { name: string @@ -14,18 +13,36 @@ export interface FeatureFlag { metadata?: any } +export interface FeatureFlagOptions { + serverURL?: string + apiPath?: string + collectionSlug?: string +} + +// Helper to get config from options or defaults +function getConfig(options?: FeatureFlagOptions) { + const serverURL = options?.serverURL || + (typeof window !== 'undefined' ? window.location.origin : '') || + '' + const apiPath = options?.apiPath || '/api' + const collectionSlug = options?.collectionSlug || 'feature-flags' + + return { serverURL, apiPath, collectionSlug } +} + /** * Hook to fetch all active feature flags from the API */ export function useFeatureFlags( - initialFlags: Partial[] + initialFlags: Partial[], + options?: FeatureFlagOptions ): { flags: Partial[] loading: boolean error: string | null refetch: () => Promise } { - const { config } = useConfig() + const { serverURL, apiPath, collectionSlug } = getConfig(options) const [flags, setFlags] = useState[]>(initialFlags) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -41,7 +58,7 @@ export function useFeatureFlags( ? `?where[name][in]=${names.join(',')}&limit=1000` : '?limit=1000' - const response = await fetch(`${config.serverURL}${config.routes.api}/feature-flags${query}`) + const response = await fetch(`${serverURL}${apiPath}/${collectionSlug}${query}`) if (!response.ok) { throw new Error(`Failed to fetch feature flags: ${response.statusText}`) @@ -77,7 +94,7 @@ export function useFeatureFlags( } finally { setLoading(false) } - }, [config.serverURL, config.routes.api, initialFlags]) + }, [serverURL, apiPath, collectionSlug, initialFlags]) useEffect(() => { void fetchFlags() @@ -89,13 +106,16 @@ export function useFeatureFlags( /** * Hook to check if a specific feature flag is enabled */ -export function useFeatureFlag(flagName: string): { +export function useFeatureFlag( + flagName: string, + options?: FeatureFlagOptions +): { isEnabled: boolean flag: Partial | null loading: boolean error: string | null } { - const { flags, loading, error } = useFeatureFlags([{ name: flagName }]) + const { flags, loading, error } = useFeatureFlags([{ name: flagName }], options) const flag = flags.find(f => f.name === flagName) || null const isEnabled = flag?.enabled || false @@ -106,13 +126,16 @@ export function useFeatureFlag(flagName: string): { /** * Hook to fetch a specific feature flag from the API */ -export function useSpecificFeatureFlag(flagName: string): { +export function useSpecificFeatureFlag( + flagName: string, + options?: FeatureFlagOptions +): { flag: FeatureFlag | null loading: boolean error: string | null refetch: () => Promise } { - const { config } = useConfig() + const { serverURL, apiPath, collectionSlug } = getConfig(options) const [flag, setFlag] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -124,7 +147,7 @@ export function useSpecificFeatureFlag(flagName: string): { // Use Payload's native collection API with query filter const response = await fetch( - `${config.serverURL}${config.routes.api}/feature-flags?where[name][equals]=${flagName}&limit=1` + `${serverURL}${apiPath}/${collectionSlug}?where[name][equals]=${flagName}&limit=1` ) if (!response.ok) { @@ -153,7 +176,7 @@ export function useSpecificFeatureFlag(flagName: string): { } finally { setLoading(false) } - }, [config.serverURL, config.routes.api, flagName]) + }, [serverURL, apiPath, collectionSlug, flagName]) useEffect(() => { void fetchFlag() @@ -167,14 +190,15 @@ export function useSpecificFeatureFlag(flagName: string): { */ export function useVariantSelection( flagName: string, - userId: string + userId: string, + options?: FeatureFlagOptions ): { variant: string | null flag: FeatureFlag | null loading: boolean error: string | null } { - const { flag, loading, error } = useSpecificFeatureFlag(flagName) + const { flag, loading, error } = useSpecificFeatureFlag(flagName, options) const variant = flag?.enabled && flag.variants ? selectVariantForUser(userId, flag.variants) @@ -188,14 +212,15 @@ export function useVariantSelection( */ export function useRolloutCheck( flagName: string, - userId: string + userId: string, + options?: FeatureFlagOptions ): { isInRollout: boolean flag: FeatureFlag | null loading: boolean error: string | null } { - const { flag, loading, error } = useSpecificFeatureFlag(flagName) + const { flag, loading, error } = useSpecificFeatureFlag(flagName, options) const isInRollout = flag?.enabled ? checkUserInRollout(userId, flag.rolloutPercentage || 100) @@ -253,13 +278,14 @@ function checkUserInRollout(userId: string, percentage: number): boolean { */ export function withFeatureFlag

>( flagName: string, - FallbackComponent?: React.ComponentType

+ FallbackComponent?: React.ComponentType

, + options?: FeatureFlagOptions ) { return function FeatureFlagWrapper( WrappedComponent: React.ComponentType

): React.ComponentType

{ return function WithFeatureFlagComponent(props: P): React.ReactElement | null { - const { isEnabled, loading } = useFeatureFlag(flagName) + const { isEnabled, loading } = useFeatureFlag(flagName, options) if (loading) { return null // or a loading spinner