Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/vue-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import { BucketProvider } from "@bucketco/vue-sdk";
</BucketProvider>
```

If using Nuxt, wrap `<BucketProvider>` in `<ClientOnly>`. `<BucketProvider>` only renders client-side currently.

### 2. Use `useFeature(key)` to get feature status

```vue
Expand Down
49 changes: 49 additions & 0 deletions packages/vue-sdk/dev/plain/components/Events.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from "vue";

import { CheckEvent, TrackEvent, useClient } from "../../../src";

import Section from "./Section.vue";

const client = useClient();

const events = ref<string[]>([]);

function checkEvent(evt: CheckEvent) {
events.value = [
...events.value,
`Check event: The feature ${evt.key} is ${evt.value} for user.`,
];
}

function featuresUpdatedEvent() {
events.value = [...events.value, `Features Updated!`];
}

function trackEvent(evt: TrackEvent) {
events.value = [...events.value, `Track event: ${evt.eventName}`];
}

onMounted(() => {
client.value.on("check", checkEvent);
client.value.on("featuresUpdated", featuresUpdatedEvent);
client.value.on("track", trackEvent);
});
onUnmounted(() => {
client.value.off("check", checkEvent);
client.value.off("featuresUpdated", featuresUpdatedEvent);
client.value.off("track", trackEvent);
});
</script>

<template>
<Section title="Events">
<div
style="display: flex; gap: 10px; flex-wrap: wrap; flex-direction: column"
>
<div v-for="event in events" :key="event">
{{ event }}
</div>
</div>
</Section>
</template>
73 changes: 73 additions & 0 deletions packages/vue-sdk/src/BucketProvider.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<script setup lang="ts">
import canonicalJson from "canonical-json";
import { provide, ref, shallowRef, watch } from "vue";

import { BucketClient } from "@bucketco/browser-sdk";

import { ProviderSymbol } from "./hooks";
import { BucketProps, ProviderContextType } from "./types";
import { SDK_VERSION } from "./version";

const featuresLoading = ref(true);
const updatedCount = ref<number>(0);

// any optional prop which has boolean as part of the type, will default to false
// instead of `undefined`, so we use `withDefaults` here to pass the undefined
// down into the client.
const props = withDefaults(defineProps<BucketProps>(), {
enableTracking: undefined,
toolbar: undefined,
});

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<BucketClient>(updateClient());

const context = {
isLoading: featuresLoading,
updatedCount: updatedCount,
client: clientRef,
provider: true,
} satisfies ProviderContextType;

provide(ProviderSymbol, context);
</script>

<template>
<slot v-if="featuresLoading && $slots.loading" name="loading" />
<slot v-else />
</template>
240 changes: 240 additions & 0 deletions packages/vue-sdk/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -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<ProviderContextType> =
Symbol("BucketProvider");

export function useFeature<TKey extends FeatureKey>(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<string, any> | 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;
}
Loading