diff --git a/packages/vue-sdk/README.md b/packages/vue-sdk/README.md index 4bdb409f..3c4f6eb8 100644 --- a/packages/vue-sdk/README.md +++ b/packages/vue-sdk/README.md @@ -32,6 +32,8 @@ import { BucketProvider } from "@bucketco/vue-sdk"; ``` +If using Nuxt, wrap `` in ``. `` only renders client-side currently. + ### 2. Use `useFeature(key)` to get feature status ```vue diff --git a/packages/vue-sdk/dev/plain/components/Events.vue b/packages/vue-sdk/dev/plain/components/Events.vue new file mode 100644 index 00000000..864f979a --- /dev/null +++ b/packages/vue-sdk/dev/plain/components/Events.vue @@ -0,0 +1,49 @@ + + + diff --git a/packages/vue-sdk/src/BucketProvider.vue b/packages/vue-sdk/src/BucketProvider.vue new file mode 100644 index 00000000..31200d12 --- /dev/null +++ b/packages/vue-sdk/src/BucketProvider.vue @@ -0,0 +1,73 @@ + + + diff --git a/packages/vue-sdk/src/hooks.ts b/packages/vue-sdk/src/hooks.ts new file mode 100644 index 00000000..c6783bed --- /dev/null +++ b/packages/vue-sdk/src/hooks.ts @@ -0,0 +1,240 @@ +import { inject, InjectionKey, onBeforeUnmount, ref } from "vue"; + +import { RequestFeedbackData, UnassignedFeedback } from "@bucketco/browser-sdk"; + +import { + FeatureKey, + ProviderContextType, + RequestFeatureFeedbackOptions, +} from "./types"; + +export const ProviderSymbol: InjectionKey = + Symbol("BucketProvider"); + +export function useFeature(key: TKey) { + const client = useClient(); + const ctx = injectSafe(); + + const track = () => client?.value.track(key); + const requestFeedback = (opts: RequestFeatureFeedbackOptions) => + client.value.requestFeedback({ ...opts, featureKey: key }); + + function getFeature() { + const f = client.value.getFeature(key); + return { + isEnabled: f.isEnabled, + config: f.config, + track, + requestFeedback, + key, + isLoading: ctx.isLoading, + }; + } + + const feature = ref(getFeature()); + + function updateFeature() { + feature.value = getFeature(); + } + + client.value.on("featuresUpdated", updateFeature); + onBeforeUnmount(() => { + client.value.off("featuresUpdated", updateFeature); + }); + + return feature; +} + +/** + * Vue composable for tracking custom events. + * + * This composable returns a function that can be used to track custom events + * with the Bucket SDK. + * + * @example + * ```ts + * import { useTrack } from '@bucketco/vue-sdk'; + * + * const track = useTrack(); + * + * // Track a custom event + * track('button_clicked', { buttonName: 'Start Huddle' }); + * ``` + * + * @returns A function that tracks an event. The function accepts: + * - `eventName`: The name of the event to track. + * - `attributes`: (Optional) Additional attributes to associate with the event. + */ +export function useTrack() { + const client = useClient(); + return (eventName: string, attributes?: Record | null) => + client?.value.track(eventName, attributes); +} + +/** + * Vue composable for requesting user feedback. + * + * This composable returns a function that can be used to trigger the feedback + * collection flow with the Bucket SDK. You can use this to prompt users for + * feedback at any point in your application. + * + * @example + * ```ts + * import { useRequestFeedback } from '@bucketco/vue-sdk'; + * + * const requestFeedback = useRequestFeedback(); + * + * // Request feedback from the user + * requestFeedback({ + * prompt: "How was your experience?", + * metadata: { page: "dashboard" } + * }); + * ``` + * + * @returns A function that requests feedback from the user. The function accepts: + * - `options`: An object containing feedback request options. + */ +export function useRequestFeedback() { + const client = useClient(); + return (options: RequestFeedbackData) => + client?.value.requestFeedback(options); +} + +/** + * Vue composable for sending feedback. + * + * This composable returns a function that can be used to send feedback to the + * Bucket SDK. You can use this to send feedback from your application. + * + * @example + * ```ts + * import { useSendFeedback } from '@bucketco/vue-sdk'; + * + * const sendFeedback = useSendFeedback(); + * + * // Send feedback from the user + * sendFeedback({ + * feedback: "I love this feature!", + * metadata: { page: "dashboard" } + * }); + * ``` + * + * @returns A function that sends feedback to the Bucket SDK. The function accepts: + * - `options`: An object containing feedback options. + */ +export function useSendFeedback() { + const client = useClient(); + return (opts: UnassignedFeedback) => client?.value.feedback(opts); +} + +/** + * Vue composable for updating the user context. + * + * This composable returns a function that can be used to update the user context + * with the Bucket SDK. You can use this to update the user context at any point + * in your application. + * + * @example + * ```ts + * import { useUpdateUser } from '@bucketco/vue-sdk'; + * + * const updateUser = useUpdateUser(); + * + * // Update the user context + * updateUser({ id: "123", name: "John Doe" }); + * ``` + * + * @returns A function that updates the user context. The function accepts: + * - `opts`: An object containing the user context to update. + */ +export function useUpdateUser() { + const client = useClient(); + return (opts: { [key: string]: string | number | undefined }) => + client?.value.updateUser(opts); +} + +/** + * Vue composable for updating the company context. + * + * This composable returns a function that can be used to update the company + * context with the Bucket SDK. You can use this to update the company context + * at any point in your application. + * + * @example + * ```ts + * import { useUpdateCompany } from '@bucketco/vue-sdk'; + * + * const updateCompany = useUpdateCompany(); + * + * // Update the company context + * updateCompany({ id: "123", name: "Acme Inc." }); + * ``` + * + * @returns A function that updates the company context. The function accepts: + * - `opts`: An object containing the company context to update. + */ +export function useUpdateCompany() { + const client = useClient(); + return (opts: { [key: string]: string | number | undefined }) => + client?.value.updateCompany(opts); +} + +/** + * Vue composable for updating the other context. + * + * This composable returns a function that can be used to update the other + * context with the Bucket SDK. You can use this to update the other context + * at any point in your application. + * + * @example + * ```ts + * import { useUpdateOtherContext } from '@bucketco/vue-sdk'; + * + * const updateOtherContext = useUpdateOtherContext(); + * + * // Update the other context + * updateOtherContext({ id: "123", name: "Acme Inc." }); + * ``` + * + * @returns A function that updates the other context. The function accepts: + * - `opts`: An object containing the other context to update. + */ +export function useUpdateOtherContext() { + const client = useClient(); + return (opts: { [key: string]: string | number | undefined }) => + client?.value.updateOtherContext(opts); +} + +/** + * Vue composable for getting the Bucket client. + * + * This composable returns the Bucket client. You can use this to get the Bucket + * client at any point in your application. + * + * @returns The Bucket client. + */ +export function useClient() { + const ctx = injectSafe(); + return ctx.client; +} + +/** + * Vue composable for checking if the Bucket client is loading. + * + * This composable returns a boolean value that indicates whether the Bucket client is loading. + * You can use this to check if the Bucket client is loading at any point in your application. + */ +export function useIsLoading() { + const ctx = injectSafe(); + return ctx.isLoading; +} + +function injectSafe() { + const ctx = inject(ProviderSymbol); + if (!ctx?.provider) { + throw new Error( + `BucketProvider is missing. Please ensure your component is wrapped with a BucketProvider.`, + ); + } + return ctx; +} diff --git a/packages/vue-sdk/src/index.ts b/packages/vue-sdk/src/index.ts index b8f83be4..3cf01945 100644 --- a/packages/vue-sdk/src/index.ts +++ b/packages/vue-sdk/src/index.ts @@ -1,32 +1,19 @@ -import canonicalJson from "canonical-json"; -import { - App, - defineComponent, - inject, - InjectionKey, - onBeforeUnmount, - provide, - Ref, - ref, - type SetupContext, - shallowRef, - watch, -} from "vue"; +import { App } from "vue"; import { - BucketClient, - BucketContext, CheckEvent, CompanyContext, - InitOptions, RawFeatures, - RequestFeedbackData, TrackEvent, - UnassignedFeedback, UserContext, } from "@bucketco/browser-sdk"; -import { version } from "../package.json"; +import BucketProvider from "./BucketProvider.vue"; +import { BucketProps } from "./types"; + +export * from "./hooks"; + +export { BucketProvider }; export type { CheckEvent, @@ -36,376 +23,6 @@ export type { UserContext, }; -export type EmptyFeatureRemoteConfig = { key: undefined; payload: undefined }; - -export type FeatureType = { - config?: { - payload: any; - }; -}; - -export type FeatureRemoteConfig = - | { - key: string; - payload: any; - } - | EmptyFeatureRemoteConfig; - -export interface Feature< - TConfig extends FeatureType["config"] = EmptyFeatureRemoteConfig, -> { - key: string; - isEnabled: Ref; - isLoading: Ref; - config: ({ key: string } & TConfig) | EmptyFeatureRemoteConfig; - track(): Promise | undefined; - requestFeedback: (opts: RequestFeedbackOptions) => void; -} - -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -export interface Features {} - -export type TypedFeatures = keyof Features extends never - ? Record - : { - [TypedFeatureKey in keyof Features]: Features[TypedFeatureKey] extends FeatureType - ? Feature - : Feature; - }; - -export type FeatureKey = keyof TypedFeatures; - -const SDK_VERSION = `vue-sdk/${version}`; - -interface ProviderContextType { - client: Ref; - isLoading: Ref; - updatedCount: Ref; - provider: boolean; -} - -const ProviderSymbol: InjectionKey = - Symbol("BucketProvider"); - -export type BucketProps = BucketContext & - InitOptions & { - debug?: boolean; - newBucketClient?: ( - ...args: ConstructorParameters - ) => BucketClient; - }; - -export const BucketProvider = defineComponent({ - name: "BucketProvider", - props: { - publishableKey: { type: String, required: true }, - user: { type: Object as () => UserContext | undefined, default: undefined }, - company: { - type: Object as () => CompanyContext | undefined, - default: undefined, - }, - otherContext: { - type: Object as () => Record | undefined, - default: undefined, - }, - loadingComponent: { type: null as any, default: undefined }, - debug: { type: Boolean, default: false }, - newBucketClient: { - type: Function as unknown as () => BucketProps["newBucketClient"], - default: undefined, - }, - }, - setup(props: BucketProps, { slots }: SetupContext) { - const featuresLoading = ref(true); - const updatedCount = ref(0); - - function updateClient() { - const cnext = ( - props.newBucketClient ?? ((...args) => new BucketClient(...args)) - )({ - ...props, - logger: props.debug ? console : undefined, - sdkVersion: SDK_VERSION, - }); - featuresLoading.value = true; - cnext - .initialize() - .catch((e) => cnext.logger.error("failed to initialize client", e)) - .finally(() => { - featuresLoading.value = false; - }); - - return cnext; - } - - watch( - () => - canonicalJson( - // canonicalJson doesn't handle `undefined` values, so we stringify/parse to remove them - JSON.parse( - JSON.stringify({ - user: props.user, - company: props.company, - otherContext: props.otherContext, - }), - ), - ), - () => { - clientRef.value = updateClient(); - }, - ); - - const clientRef = shallowRef(updateClient()); - - const context = { - isLoading: featuresLoading, - updatedCount: updatedCount, - client: clientRef, - provider: true, - } satisfies ProviderContextType; - - provide(ProviderSymbol, context); - - return () => - featuresLoading.value && typeof slots.loading !== "undefined" - ? slots.loading() - : slots.default?.(); - }, -}); - -export type RequestFeedbackOptions = Omit< - RequestFeedbackData, - "featureKey" | "featureId" ->; - -export function useFeature(key: TKey) { - const client = useClient(); - const ctx = injectSafe(); - - const track = () => client?.value.track(key); - const requestFeedback = (opts: RequestFeedbackOptions) => - client.value.requestFeedback({ ...opts, featureKey: key }); - - function getFeature() { - const f = client.value.getFeature(key); - return { - isEnabled: f.isEnabled, - config: f.config, - track, - requestFeedback, - key, - isLoading: ctx.isLoading, - }; - } - - const feature = ref(getFeature()); - - function updateFeature() { - feature.value = getFeature(); - } - - client.value.on("featuresUpdated", updateFeature); - onBeforeUnmount(() => { - client.value.off("featuresUpdated", updateFeature); - }); - - return feature; -} - -/** - * Vue composable for tracking custom events. - * - * This composable returns a function that can be used to track custom events - * with the Bucket SDK. - * - * @example - * ```ts - * import { useTrack } from '@bucketco/vue-sdk'; - * - * const track = useTrack(); - * - * // Track a custom event - * track('button_clicked', { buttonName: 'Start Huddle' }); - * ``` - * - * @returns A function that tracks an event. The function accepts: - * - `eventName`: The name of the event to track. - * - `attributes`: (Optional) Additional attributes to associate with the event. - */ -export function useTrack() { - const client = useClient(); - return (eventName: string, attributes?: Record | null) => - client?.value.track(eventName, attributes); -} - -/** - * Vue composable for requesting user feedback. - * - * This composable returns a function that can be used to trigger the feedback - * collection flow with the Bucket SDK. You can use this to prompt users for - * feedback at any point in your application. - * - * @example - * ```ts - * import { useRequestFeedback } from '@bucketco/vue-sdk'; - * - * const requestFeedback = useRequestFeedback(); - * - * // Request feedback from the user - * requestFeedback({ - * prompt: "How was your experience?", - * metadata: { page: "dashboard" } - * }); - * ``` - * - * @returns A function that requests feedback from the user. The function accepts: - * - `options`: An object containing feedback request options. - */ -export function useRequestFeedback() { - const client = useClient(); - return (options: RequestFeedbackData) => - client?.value.requestFeedback(options); -} - -/** - * Vue composable for sending feedback. - * - * This composable returns a function that can be used to send feedback to the - * Bucket SDK. You can use this to send feedback from your application. - * - * @example - * ```ts - * import { useSendFeedback } from '@bucketco/vue-sdk'; - * - * const sendFeedback = useSendFeedback(); - * - * // Send feedback from the user - * sendFeedback({ - * feedback: "I love this feature!", - * metadata: { page: "dashboard" } - * }); - * ``` - * - * @returns A function that sends feedback to the Bucket SDK. The function accepts: - * - `options`: An object containing feedback options. - */ -export function useSendFeedback() { - const client = useClient(); - return (opts: UnassignedFeedback) => client?.value.feedback(opts); -} - -/** - * Vue composable for updating the user context. - * - * This composable returns a function that can be used to update the user context - * with the Bucket SDK. You can use this to update the user context at any point - * in your application. - * - * @example - * ```ts - * import { useUpdateUser } from '@bucketco/vue-sdk'; - * - * const updateUser = useUpdateUser(); - * - * // Update the user context - * updateUser({ id: "123", name: "John Doe" }); - * ``` - * - * @returns A function that updates the user context. The function accepts: - * - `opts`: An object containing the user context to update. - */ -export function useUpdateUser() { - const client = useClient(); - return (opts: { [key: string]: string | number | undefined }) => - client?.value.updateUser(opts); -} - -/** - * Vue composable for updating the company context. - * - * This composable returns a function that can be used to update the company - * context with the Bucket SDK. You can use this to update the company context - * at any point in your application. - * - * @example - * ```ts - * import { useUpdateCompany } from '@bucketco/vue-sdk'; - * - * const updateCompany = useUpdateCompany(); - * - * // Update the company context - * updateCompany({ id: "123", name: "Acme Inc." }); - * ``` - * - * @returns A function that updates the company context. The function accepts: - * - `opts`: An object containing the company context to update. - */ -export function useUpdateCompany() { - const client = useClient(); - return (opts: { [key: string]: string | number | undefined }) => - client?.value.updateCompany(opts); -} - -/** - * Vue composable for updating the other context. - * - * This composable returns a function that can be used to update the other - * context with the Bucket SDK. You can use this to update the other context - * at any point in your application. - * - * @example - * ```ts - * import { useUpdateOtherContext } from '@bucketco/vue-sdk'; - * - * const updateOtherContext = useUpdateOtherContext(); - * - * // Update the other context - * updateOtherContext({ id: "123", name: "Acme Inc." }); - * ``` - * - * @returns A function that updates the other context. The function accepts: - * - `opts`: An object containing the other context to update. - */ -export function useUpdateOtherContext() { - const client = useClient(); - return (opts: { [key: string]: string | number | undefined }) => - client?.value.updateOtherContext(opts); -} - -/** - * Vue composable for getting the Bucket client. - * - * This composable returns the Bucket client. You can use this to get the Bucket - * client at any point in your application. - * - * @returns The Bucket client. - */ -export function useClient() { - const ctx = injectSafe(); - return ctx.client; -} - -/** - * Vue composable for checking if the Bucket client is loading. - * - * This composable returns a boolean value that indicates whether the Bucket client is loading. - * You can use this to check if the Bucket client is loading at any point in your application. - */ -export function useIsLoading() { - const ctx = injectSafe(); - return ctx.isLoading; -} - -function injectSafe() { - const ctx = inject(ProviderSymbol); - if (!ctx?.provider) { - throw new Error( - `BucketProvider is missing. Please ensure your component is wrapped with a BucketProvider.`, - ); - } - return ctx; -} - export default { install(app: App, _options?: BucketProps) { app.component("BucketProvider", BucketProvider); diff --git a/packages/vue-sdk/src/types.d.ts b/packages/vue-sdk/src/types.d.ts deleted file mode 100644 index 59f438a8..00000000 --- a/packages/vue-sdk/src/types.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module "canonical-json"; diff --git a/packages/vue-sdk/src/types.ts b/packages/vue-sdk/src/types.ts new file mode 100644 index 00000000..7179674b --- /dev/null +++ b/packages/vue-sdk/src/types.ts @@ -0,0 +1,67 @@ +import type { Ref } from "vue"; + +import type { + BucketClient, + BucketContext, + InitOptions, + RequestFeedbackData, +} from "@bucketco/browser-sdk"; + +export type EmptyFeatureRemoteConfig = { key: undefined; payload: undefined }; + +export type FeatureType = { + config?: { + payload: any; + }; +}; + +export type FeatureRemoteConfig = + | { + key: string; + payload: any; + } + | EmptyFeatureRemoteConfig; + +export interface Feature< + TConfig extends FeatureType["config"] = EmptyFeatureRemoteConfig, +> { + key: string; + isEnabled: Ref; + isLoading: Ref; + config: ({ key: string } & TConfig) | EmptyFeatureRemoteConfig; + track(): Promise | undefined; + requestFeedback: (opts: RequestFeatureFeedbackOptions) => void; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface Features {} + +export type TypedFeatures = keyof Features extends never + ? Record + : { + [TypedFeatureKey in keyof Features]: Features[TypedFeatureKey] extends FeatureType + ? Feature + : Feature; + }; + +export type FeatureKey = keyof TypedFeatures; + +export interface ProviderContextType { + client: Ref; + isLoading: Ref; + updatedCount: Ref; + provider: boolean; +} + +export type BucketProps = BucketContext & + InitOptions & { + debug?: boolean; + newBucketClient?: ( + ...args: ConstructorParameters + ) => BucketClient; + }; + +export type RequestFeatureFeedbackOptions = Omit< + RequestFeedbackData, + "featureKey" | "featureId" +>; diff --git a/packages/vue-sdk/src/version.ts b/packages/vue-sdk/src/version.ts new file mode 100644 index 00000000..0cd6db99 --- /dev/null +++ b/packages/vue-sdk/src/version.ts @@ -0,0 +1,3 @@ +import { version } from "../package.json"; + +export const SDK_VERSION = `vue-sdk/${version}`; diff --git a/packages/vue-sdk/src/vue.d.ts b/packages/vue-sdk/src/vue.d.ts new file mode 100644 index 00000000..68ff5d93 --- /dev/null +++ b/packages/vue-sdk/src/vue.d.ts @@ -0,0 +1,10 @@ +declare module "*.vue" { + import type { DefineComponent } from "vue"; + + const component: DefineComponent< + Record, + Record, + unknown + >; + export default component; +} diff --git a/packages/vue-sdk/tsconfig.json b/packages/vue-sdk/tsconfig.json index 464bdb25..a3610742 100644 --- a/packages/vue-sdk/tsconfig.json +++ b/packages/vue-sdk/tsconfig.json @@ -6,6 +6,6 @@ "declarationDir": "./dist/types", "declarationMap": true }, - "include": ["src"], + "include": ["src", "src/**/*.d.ts"], "typeRoots": ["./node_modules/@types"] }