From 4710bc24ce19a0328357160a5217243f3af9189b Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Thu, 20 Feb 2025 12:28:18 +0700 Subject: [PATCH 1/2] feat(react-sdk): add onFeaturesUpdated prop and useClient hook - Introduce `onFeaturesUpdated` callback in BucketProvider to allow custom handling of feature updates - Add new `useClient()` hook to access the BucketClient directly - Refactor hooks to use `useClient()` for simplified client access - Update README with documentation for new features --- packages/react-sdk/README.md | 22 ++++++++++++++ packages/react-sdk/package.json | 2 +- packages/react-sdk/src/index.tsx | 52 +++++++++++++++++++++----------- 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/packages/react-sdk/README.md b/packages/react-sdk/README.md index 32a735b5..31d6d870 100644 --- a/packages/react-sdk/README.md +++ b/packages/react-sdk/README.md @@ -145,6 +145,7 @@ Example with all options: - `appBaseUrl`: Optional base URL for the Bucket application. Use this to override the default app URL, - `sseBaseUrl`: Optional base URL for Server-Sent Events. Use this to override the default SSE endpoint, - `debug`: Set to `true` to enable debug logging to the console, +- `onFeaturesUpdated`: Provide a callback that is called when features are fetched from Bucket, - `toolbar`: Optional configuration for the Bucket toolbar, - `feedback`: Optional configuration for feedback collection: @@ -396,6 +397,27 @@ function FeatureOptIn() { Note: To change the `user.id` or `company.id`, you need to update the props passed to `BucketProvider` instead of using these hooks. +### `useClient()` + +Returns the `BucketClient` used by the `BucketProvider`. The client offers more functionality that +is not directly accessible thorough the other hooks. + +```tsx +import { useClient } from "@bucketco/react-sdk"; + +function LoggingWrapper({ children }: { children: ReactNode }) { + const client = useClient(); + + useEffect(() => { + client.on("enabledCheck", (evt) => { + console.log(`The feature ${evt.key} is ${evt.value} for user.`); + }); + }, [client]); + + return children; +} +``` + ## Content Security Policy (CSP) See [CSP](https://github.com/bucketco/bucket-javascript-sdk/blob/main/packages/browser-sdk/README.md#content-security-policy-csp) for info on using Bucket React SDK with CSP diff --git a/packages/react-sdk/package.json b/packages/react-sdk/package.json index 3a632c28..867a55dc 100644 --- a/packages/react-sdk/package.json +++ b/packages/react-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@bucketco/react-sdk", - "version": "3.0.0-alpha.6", + "version": "3.0.0-alpha.7", "license": "MIT", "repository": { "type": "git", diff --git a/packages/react-sdk/src/index.tsx b/packages/react-sdk/src/index.tsx index f90b725a..c32131fa 100644 --- a/packages/react-sdk/src/index.tsx +++ b/packages/react-sdk/src/index.tsx @@ -68,6 +68,11 @@ export type BucketProps = BucketContext & */ debug?: boolean; + /** + * Callback to be called when features are updated. + */ + onFeaturesUpdated?: (features: RawFeatures) => void; + /** * New BucketClient constructor. * @@ -86,6 +91,7 @@ export function BucketProvider({ user, company, otherContext, + onFeaturesUpdated, loadingComponent, newBucketClient = (...args) => new BucketClient(...args), ...config @@ -124,7 +130,12 @@ export function BucketProvider({ clientRef.current = client; - client.on("featuresUpdated", setRawFeatures); + client.on("featuresUpdated", () => { + const features = client.getFeatures(); + + setRawFeatures(features); + onFeaturesUpdated?.(features); + }); client .initialize() @@ -195,12 +206,10 @@ type Feature = { export function useFeature( key: TKey, ): Feature { + const client = useClient(); const { features: { isLoading }, - client, - provider, } = useContext(ProviderContext); - ensureProvider(provider); const track = () => client?.track(key); const requestFeedback = (opts: RequestFeedbackOptions) => @@ -241,8 +250,7 @@ export function useFeature( * ``` */ export function useTrack() { - const { client, provider } = useContext(ProviderContext); - ensureProvider(provider); + const client = useClient(); return (eventName: string, attributes?: Record | null) => client?.track(eventName, attributes); } @@ -262,8 +270,7 @@ export function useTrack() { * ``` */ export function useRequestFeedback() { - const { client, provider } = useContext(ProviderContext); - ensureProvider(provider); + const client = useClient(); return (options: RequestFeedbackData) => client?.requestFeedback(options); } @@ -284,8 +291,7 @@ export function useRequestFeedback() { * ``` */ export function useSendFeedback() { - const { client, provider } = useContext(ProviderContext); - ensureProvider(provider); + const client = useClient(); return (opts: UnassignedFeedback) => client?.feedback(opts); } @@ -303,8 +309,7 @@ export function useSendFeedback() { * ``` */ export function useUpdateUser() { - const { client, provider } = useContext(ProviderContext); - ensureProvider(provider); + const client = useClient(); return (opts: { [key: string]: string | number | undefined }) => client?.updateUser(opts); } @@ -323,8 +328,7 @@ export function useUpdateUser() { * ``` */ export function useUpdateCompany() { - const { client, provider } = useContext(ProviderContext); - ensureProvider(provider); + const client = useClient(); return (opts: { [key: string]: string | number | undefined }) => client?.updateCompany(opts); @@ -345,16 +349,30 @@ export function useUpdateCompany() { * ``` */ export function useUpdateOtherContext() { - const { client, provider } = useContext(ProviderContext); - ensureProvider(provider); + const client = useClient(); return (opts: { [key: string]: string | number | undefined }) => client?.updateOtherContext(opts); } -function ensureProvider(provider: boolean) { +/** + * Returns the current `BucketClient` used by the `BucketProvider`. + * + * This is useful if you need to access the `BucketClient` outside of the `BucketProvider`. + * + * ```ts + * const client = useClient(); + * client.on("configCheck", () => { + * console.log("configCheck hook called"); + * }); + * ``` + */ +export function useClient() { + const { client, provider } = useContext(ProviderContext); if (!provider) { throw new Error( "BucketProvider is missing. Please ensure your component is wrapped with a BucketProvider.", ); } + + return client; } From 3df87df12720eac858549e57c0d66c7076c23fda Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Thu, 20 Feb 2025 18:41:31 +0700 Subject: [PATCH 2/2] refactor(react-sdk): remove onFeaturesUpdated prop and simplify feature update handling - Remove `onFeaturesUpdated` callback from BucketProvider - Simplify feature update event listener to directly set raw features - Expose `Feature` type for external use - Add `useClient()` hook test to ensure proper functionality --- packages/react-sdk/README.md | 1 - packages/react-sdk/src/index.tsx | 15 ++------------- packages/react-sdk/test/usage.test.tsx | 18 +++++++++++++++++- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/packages/react-sdk/README.md b/packages/react-sdk/README.md index 31d6d870..06c24922 100644 --- a/packages/react-sdk/README.md +++ b/packages/react-sdk/README.md @@ -145,7 +145,6 @@ Example with all options: - `appBaseUrl`: Optional base URL for the Bucket application. Use this to override the default app URL, - `sseBaseUrl`: Optional base URL for Server-Sent Events. Use this to override the default SSE endpoint, - `debug`: Set to `true` to enable debug logging to the console, -- `onFeaturesUpdated`: Provide a callback that is called when features are fetched from Bucket, - `toolbar`: Optional configuration for the Bucket toolbar, - `feedback`: Optional configuration for feedback collection: diff --git a/packages/react-sdk/src/index.tsx b/packages/react-sdk/src/index.tsx index c32131fa..711f82cd 100644 --- a/packages/react-sdk/src/index.tsx +++ b/packages/react-sdk/src/index.tsx @@ -68,11 +68,6 @@ export type BucketProps = BucketContext & */ debug?: boolean; - /** - * Callback to be called when features are updated. - */ - onFeaturesUpdated?: (features: RawFeatures) => void; - /** * New BucketClient constructor. * @@ -91,7 +86,6 @@ export function BucketProvider({ user, company, otherContext, - onFeaturesUpdated, loadingComponent, newBucketClient = (...args) => new BucketClient(...args), ...config @@ -130,12 +124,7 @@ export function BucketProvider({ clientRef.current = client; - client.on("featuresUpdated", () => { - const features = client.getFeatures(); - - setRawFeatures(features); - onFeaturesUpdated?.(features); - }); + client.on("featuresUpdated", setRawFeatures); client .initialize() @@ -177,7 +166,7 @@ type EmptyConfig = { payload: undefined; }; -type Feature = { +export type Feature = { isEnabled: boolean; isLoading: boolean; config: MaterializedFeatures[TKey] extends boolean diff --git a/packages/react-sdk/test/usage.test.tsx b/packages/react-sdk/test/usage.test.tsx index 4194bbd0..bc47dce4 100644 --- a/packages/react-sdk/test/usage.test.tsx +++ b/packages/react-sdk/test/usage.test.tsx @@ -19,6 +19,7 @@ import { version } from "../package.json"; import { BucketProps, BucketProvider, + useClient, useFeature, useRequestFeedback, useSendFeedback, @@ -138,6 +139,7 @@ beforeAll(() => }, }), ); + afterEach(() => server.resetHandlers()); afterAll(() => server.close()); @@ -311,7 +313,7 @@ describe("useSendFeedback", () => { await waitFor(async () => { await result.current({ - featureId: "123", + featureKey: "huddles", score: 5, }); expect(events).toStrictEqual(["FEEDBACK"]); @@ -432,3 +434,17 @@ describe("useUpdateOtherContext", () => { unmount(); }); }); + +describe("useClient", () => { + test("gets the client", async () => { + const { result: clientFn, unmount } = renderHook(() => useClient(), { + wrapper: ({ children }) => getProvider({ children }), + }); + + await waitFor(async () => { + expect(clientFn.current).toBeDefined(); + }); + + unmount(); + }); +});