diff --git a/packages/browser-sdk/src/client.ts b/packages/browser-sdk/src/client.ts index fb3a68c4..dcb3eac7 100644 --- a/packages/browser-sdk/src/client.ts +++ b/packages/browser-sdk/src/client.ts @@ -319,6 +319,7 @@ export type FeatureRemoteConfig = export interface Feature { /** * Result of feature flag evaluation. + * Note: Does not take local overrides into account. */ isEnabled: boolean; @@ -338,6 +339,17 @@ export interface Feature { requestFeedback: ( options: Omit, ) => void; + + /** + * The current override status of isEnabled for the feature. + */ + isEnabledOverride: boolean | null; + + /** + * Set the override status for isEnabled for the feature. + * Set to `null` to remove the override. + */ + setIsEnabledOverride(isEnabled: boolean | null): void; } function shouldShowToolbar(opts: InitOptions) { @@ -778,23 +790,13 @@ export class BucketClient { ...options, }); }, + isEnabledOverride: this.featuresClient.getFeatureOverride(key), + setIsEnabledOverride(isEnabled: boolean | null) { + self.featuresClient.setFeatureOverride(key, isEnabled); + }, }; } - /** - * @internal - */ - setFeatureOverride(key: string, isEnabled: boolean | null) { - this.featuresClient.setFeatureOverride(key, isEnabled); - } - - /** - * @internal - */ - getFeatureOverride(key: string): boolean | null { - return this.featuresClient.getFeatureOverride(key); - } - private sendCheckEvent(checkEvent: CheckEvent) { return this.featuresClient.sendCheckEvent(checkEvent, () => { this.hooks.trigger( diff --git a/packages/browser-sdk/src/toolbar/Features.tsx b/packages/browser-sdk/src/toolbar/Features.tsx index 2a8ea856..a5d6eb09 100644 --- a/packages/browser-sdk/src/toolbar/Features.tsx +++ b/packages/browser-sdk/src/toolbar/Features.tsx @@ -9,15 +9,15 @@ import { FeatureItem } from "./Toolbar"; export function FeaturesTable({ features, searchQuery, - setEnabledOverride, appBaseUrl, isOpen, + setIsEnabledOverride, }: { features: FeatureItem[]; searchQuery: string | null; - setEnabledOverride: (key: string, value: boolean | null) => void; appBaseUrl: string; isOpen: boolean; + setIsEnabledOverride: (key: string, isEnabled: boolean | null) => void; }) { const searchedFeatures = searchQuery === null @@ -43,7 +43,9 @@ export function FeaturesTable({ .includes(searchQuery.toLocaleLowerCase()) } isOpen={isOpen} - setEnabledOverride={setEnabledOverride} + setEnabledOverride={(override) => + setIsEnabledOverride(feature.key, override) + } /> ))} @@ -61,7 +63,7 @@ function FeatureRow({ }: { feature: FeatureItem; appBaseUrl: string; - setEnabledOverride: (key: string, value: boolean | null) => void; + setEnabledOverride: (isEnabled: boolean | null) => void; isOpen: boolean; index: number; isNotVisible: boolean; @@ -94,24 +96,17 @@ function FeatureRow({ {feature.localOverride !== null ? ( - + ) : null} { const isChecked = e.currentTarget.checked; const isOverridden = isChecked !== feature.isEnabled; - setEnabledOverride(feature.key, isOverridden ? isChecked : null); + setEnabledOverride(isOverridden ? isChecked : null); }} /> @@ -138,11 +133,9 @@ export function FeatureSearch({ function Reset({ setEnabledOverride, - featureKey, ...props }: { - setEnabledOverride: (key: string, value: boolean | null) => void; - featureKey: string; + setEnabledOverride: (isEnabled: boolean | null) => void; } & h.JSX.HTMLAttributes) { return ( { e.preventDefault(); - setEnabledOverride(featureKey, null); + setEnabledOverride(null); }} {...props} > diff --git a/packages/browser-sdk/src/toolbar/Toolbar.tsx b/packages/browser-sdk/src/toolbar/Toolbar.tsx index e0450ad2..a1f0d7f6 100644 --- a/packages/browser-sdk/src/toolbar/Toolbar.tsx +++ b/packages/browser-sdk/src/toolbar/Toolbar.tsx @@ -48,7 +48,7 @@ export default function Toolbar({ (feature) => ({ key: feature.key, - localOverride: bucketClient.getFeatureOverride(feature?.key), + localOverride: feature.isEnabledOverride, isEnabled: feature.isEnabled, }) satisfies FeatureItem, ), @@ -108,9 +108,9 @@ export default function Toolbar({ features={sortedFeatures} isOpen={isOpen} searchQuery={search} - setEnabledOverride={bucketClient.setFeatureOverride.bind( - bucketClient, - )} + setIsEnabledOverride={(key, isEnabled) => + bucketClient.getFeature(key).setIsEnabledOverride(isEnabled) + } /> diff --git a/packages/browser-sdk/test/client.test.ts b/packages/browser-sdk/test/client.test.ts index cf306cd9..b4c2e464 100644 --- a/packages/browser-sdk/test/client.test.ts +++ b/packages/browser-sdk/test/client.test.ts @@ -70,7 +70,7 @@ describe("BucketClient", () => { await client.initialize(); expect(featuresResult["featureA"].isEnabled).toBe(true); expect(client.getFeature("featureA").isEnabled).toBe(true); - client.setFeatureOverride("featureA", false); + client.getFeature("featureA").setIsEnabledOverride(false); expect(client.getFeature("featureA").isEnabled).toBe(false); }); }); diff --git a/packages/browser-sdk/test/usage.test.ts b/packages/browser-sdk/test/usage.test.ts index a51fbdf4..5cbd1f21 100644 --- a/packages/browser-sdk/test/usage.test.ts +++ b/packages/browser-sdk/test/usage.test.ts @@ -80,6 +80,8 @@ describe("usage", () => { track: expect.any(Function), requestFeedback: expect.any(Function), config: { key: undefined, payload: undefined }, + isEnabledOverride: null, + setIsEnabledOverride: expect.any(Function), }); }); @@ -413,6 +415,8 @@ describe(`sends "check" events `, () => { config: { key: undefined, payload: undefined }, track: expect.any(Function), requestFeedback: expect.any(Function), + isEnabledOverride: null, + setIsEnabledOverride: expect.any(Function), }); expect(client.getFeature("featureB")).toStrictEqual({ @@ -426,6 +430,8 @@ describe(`sends "check" events `, () => { }, track: expect.any(Function), requestFeedback: expect.any(Function), + isEnabledOverride: null, + setIsEnabledOverride: expect.any(Function), }); expect(client.getFeature("featureC")).toStrictEqual({ @@ -433,6 +439,8 @@ describe(`sends "check" events `, () => { config: { key: undefined, payload: undefined }, track: expect.any(Function), requestFeedback: expect.any(Function), + isEnabledOverride: null, + setIsEnabledOverride: expect.any(Function), }); }); @@ -567,6 +575,8 @@ describe(`sends "check" events `, () => { track: expect.any(Function), requestFeedback: expect.any(Function), config: { key: undefined, payload: undefined }, + isEnabledOverride: null, + setIsEnabledOverride: expect.any(Function), }); vi.spyOn(client, "track"); @@ -586,6 +596,8 @@ describe(`sends "check" events `, () => { track: expect.any(Function), requestFeedback: expect.any(Function), config: { key: undefined, payload: undefined }, + isEnabledOverride: null, + setIsEnabledOverride: expect.any(Function), }); vi.spyOn(client, "requestFeedback"); diff --git a/packages/react-sdk/dev/plain/app.tsx b/packages/react-sdk/dev/plain/app.tsx index dd22267b..a393b89e 100644 --- a/packages/react-sdk/dev/plain/app.tsx +++ b/packages/react-sdk/dev/plain/app.tsx @@ -9,6 +9,7 @@ import { useUpdateCompany, useUpdateOtherContext, useUpdateUser, + useClient, } from "../../src"; // Extending the Features interface to define the available features @@ -187,6 +188,7 @@ function Demos() { + ); } @@ -223,6 +225,49 @@ function FeatureOptIn({ ); } +function CustomToolbar() { + const client = useClient(); + + if (!client) { + return null; + } + + return ( +
+

Custom toolbar

+
    + {Object.entries(client.getFeatures()).map(([featureKey, feature]) => ( +
  • + {featureKey} - + {(feature.isEnabledOverride ?? feature.isEnabled) + ? "Enabled" + : "Disabled"}{" "} + {feature.isEnabledOverride !== null && ( + + )} + { + // this uses slightly simplified logic compared to the Bucket Toolbar + client + .getFeature(featureKey) + .setIsEnabledOverride(e.target.checked ?? false); + }} + /> +
  • + ))} +
+
+ ); +} + export function App() { return ( + {!publishableKey && ( +
+ No publishable key set. Please set the VITE_PUBLISHABLE_KEY + environment variable. +
+ )} - {}
); }