---
packages/browser-sdk/example/browser.html | 43 ---
.../browser-sdk/example/typescript/app.ts | 51 ++++
.../browser-sdk/example/typescript/index.html | 23 ++
packages/browser-sdk/index.html | 71 +++++
packages/browser-sdk/src/client.ts | 62 ++++-
packages/browser-sdk/src/config.ts | 1 +
packages/browser-sdk/src/feature/features.ts | 60 ++--
packages/browser-sdk/src/feedback/feedback.ts | 11 +-
.../src/feedback/ui/FeedbackDialog.css | 99 -------
.../src/feedback/ui/FeedbackDialog.tsx | 244 +++-------------
.../src/feedback/ui/FeedbackForm.tsx | 5 +-
packages/browser-sdk/src/feedback/ui/Plug.tsx | 4 +-
.../src/feedback/ui/StarRating.tsx | 17 +-
.../ui/config/defaultTranslations.tsx | 1 -
packages/browser-sdk/src/feedback/ui/index.ts | 24 +-
packages/browser-sdk/src/feedback/ui/types.ts | 27 +-
packages/browser-sdk/src/index.ts | 10 +-
packages/browser-sdk/src/toolbar/Features.css | 74 +++++
packages/browser-sdk/src/toolbar/Features.tsx | 113 ++++++++
packages/browser-sdk/src/toolbar/Switch.css | 22 ++
packages/browser-sdk/src/toolbar/Switch.tsx | 50 ++++
packages/browser-sdk/src/toolbar/Toolbar.css | 163 +++++++++++
packages/browser-sdk/src/toolbar/Toolbar.tsx | 146 ++++++++++
packages/browser-sdk/src/toolbar/index.css | 3 +
packages/browser-sdk/src/toolbar/index.ts | 23 ++
packages/browser-sdk/src/ui/Dialog.css | 113 ++++++++
packages/browser-sdk/src/ui/Dialog.tsx | 263 ++++++++++++++++++
.../src/{feedback => }/ui/constants.ts | 1 +
.../src/{feedback => }/ui/icons/Check.tsx | 0
.../{feedback => }/ui/icons/CheckCircle.tsx | 0
.../src/{feedback => }/ui/icons/Close.tsx | 0
.../{feedback => }/ui/icons/Dissatisfied.tsx | 0
.../src/{feedback => }/ui/icons/Logo.tsx | 4 +-
.../src/{feedback => }/ui/icons/Neutral.tsx | 0
.../src/{feedback => }/ui/icons/Satisfied.tsx | 0
.../ui/icons/VeryDissatisfied.tsx | 0
.../{feedback => }/ui/icons/VerySatisfied.tsx | 0
.../packages/floating-ui-preact-dom/README.md | 0
.../packages/floating-ui-preact-dom/arrow.ts | 0
.../packages/floating-ui-preact-dom/index.ts | 0
.../packages/floating-ui-preact-dom/types.ts | 0
.../floating-ui-preact-dom/useFloating.ts | 0
.../floating-ui-preact-dom/utils/deepEqual.ts | 0
.../floating-ui-preact-dom/utils/getDPR.ts | 0
.../utils/roundByDPR.ts | 0
.../utils/useLatestRef.ts | 0
packages/browser-sdk/src/ui/types.ts | 33 +++
packages/browser-sdk/src/ui/utils.ts | 65 +++++
.../test/e2e/feedback-widget.browser.spec.ts | 63 ++++-
.../test/e2e/give-feedback-button.html | 13 +
packages/browser-sdk/test/features.test.ts | 11 +-
packages/react-sdk/src/index.tsx | 13 +-
packages/react-sdk/test/usage.test.tsx | 2 +
53 files changed, 1497 insertions(+), 431 deletions(-)
delete mode 100644 packages/browser-sdk/example/browser.html
create mode 100644 packages/browser-sdk/example/typescript/app.ts
create mode 100644 packages/browser-sdk/example/typescript/index.html
create mode 100644 packages/browser-sdk/index.html
create mode 100644 packages/browser-sdk/src/toolbar/Features.css
create mode 100644 packages/browser-sdk/src/toolbar/Features.tsx
create mode 100644 packages/browser-sdk/src/toolbar/Switch.css
create mode 100644 packages/browser-sdk/src/toolbar/Switch.tsx
create mode 100644 packages/browser-sdk/src/toolbar/Toolbar.css
create mode 100644 packages/browser-sdk/src/toolbar/Toolbar.tsx
create mode 100644 packages/browser-sdk/src/toolbar/index.css
create mode 100644 packages/browser-sdk/src/toolbar/index.ts
create mode 100644 packages/browser-sdk/src/ui/Dialog.css
create mode 100644 packages/browser-sdk/src/ui/Dialog.tsx
rename packages/browser-sdk/src/{feedback => }/ui/constants.ts (95%)
rename packages/browser-sdk/src/{feedback => }/ui/icons/Check.tsx (100%)
rename packages/browser-sdk/src/{feedback => }/ui/icons/CheckCircle.tsx (100%)
rename packages/browser-sdk/src/{feedback => }/ui/icons/Close.tsx (100%)
rename packages/browser-sdk/src/{feedback => }/ui/icons/Dissatisfied.tsx (100%)
rename packages/browser-sdk/src/{feedback => }/ui/icons/Logo.tsx (98%)
rename packages/browser-sdk/src/{feedback => }/ui/icons/Neutral.tsx (100%)
rename packages/browser-sdk/src/{feedback => }/ui/icons/Satisfied.tsx (100%)
rename packages/browser-sdk/src/{feedback => }/ui/icons/VeryDissatisfied.tsx (100%)
rename packages/browser-sdk/src/{feedback => }/ui/icons/VerySatisfied.tsx (100%)
rename packages/browser-sdk/src/{feedback => }/ui/packages/floating-ui-preact-dom/README.md (100%)
rename packages/browser-sdk/src/{feedback => }/ui/packages/floating-ui-preact-dom/arrow.ts (100%)
rename packages/browser-sdk/src/{feedback => }/ui/packages/floating-ui-preact-dom/index.ts (100%)
rename packages/browser-sdk/src/{feedback => }/ui/packages/floating-ui-preact-dom/types.ts (100%)
rename packages/browser-sdk/src/{feedback => }/ui/packages/floating-ui-preact-dom/useFloating.ts (100%)
rename packages/browser-sdk/src/{feedback => }/ui/packages/floating-ui-preact-dom/utils/deepEqual.ts (100%)
rename packages/browser-sdk/src/{feedback => }/ui/packages/floating-ui-preact-dom/utils/getDPR.ts (100%)
rename packages/browser-sdk/src/{feedback => }/ui/packages/floating-ui-preact-dom/utils/roundByDPR.ts (100%)
rename packages/browser-sdk/src/{feedback => }/ui/packages/floating-ui-preact-dom/utils/useLatestRef.ts (100%)
create mode 100644 packages/browser-sdk/src/ui/types.ts
create mode 100644 packages/browser-sdk/src/ui/utils.ts
create mode 100644 packages/browser-sdk/test/e2e/give-feedback-button.html
diff --git a/packages/browser-sdk/example/browser.html b/packages/browser-sdk/example/browser.html
deleted file mode 100644
index 175497d1..00000000
--- a/packages/browser-sdk/example/browser.html
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-
-
- Bucket feature management
-
-
- Loading...
-
-
-
- Click me
-
- Give feedback!
-
-
-
-
-
-
-
diff --git a/packages/browser-sdk/example/typescript/app.ts b/packages/browser-sdk/example/typescript/app.ts
new file mode 100644
index 00000000..72ee20cd
--- /dev/null
+++ b/packages/browser-sdk/example/typescript/app.ts
@@ -0,0 +1,51 @@
+import { BucketClient } from "../../src";
+
+const urlParams = new URLSearchParams(window.location.search);
+const publishableKey = urlParams.get("publishableKey");
+const featureKey = urlParams.get("featureKey") ?? "huddles";
+
+const featureList = ["huddles"];
+
+if (!publishableKey) {
+ throw Error("publishableKey is missing");
+}
+
+const bucket = new BucketClient({
+ publishableKey,
+ user: { id: "42" },
+ company: { id: "1" },
+ toolbar: {
+ show: true,
+ position: { placement: "bottom-right" },
+ },
+ featureList,
+});
+
+document
+ .getElementById("startHuddle")
+ ?.addEventListener("click", () => bucket.track(featureKey));
+document.getElementById("giveFeedback")?.addEventListener("click", (event) =>
+ bucket.requestFeedback({
+ featureKey,
+ position: { type: "POPOVER", anchor: event.currentTarget as HTMLElement },
+ }),
+);
+
+bucket.initialize().then(() => {
+ console.log("Bucket initialized");
+ const loadingElem = document.getElementById("loading");
+ if (loadingElem) loadingElem.style.display = "none";
+});
+
+bucket.onFeaturesUpdated(() => {
+ const { isEnabled } = bucket.getFeature("huddles");
+
+ const startHuddleElem = document.getElementById("start-huddle");
+ if (isEnabled) {
+ // show the start-huddle button
+ if (startHuddleElem) startHuddleElem.style.display = "block";
+ } else {
+ // hide the start-huddle button
+ if (startHuddleElem) startHuddleElem.style.display = "none";
+ }
+});
diff --git a/packages/browser-sdk/example/typescript/index.html b/packages/browser-sdk/example/typescript/index.html
new file mode 100644
index 00000000..cec72d21
--- /dev/null
+++ b/packages/browser-sdk/example/typescript/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+ Bucket feature management
+
+
+ Loading...
+
+
+ Start huddle!
+ Click me
+ Give feedback!
+
+
+
+
diff --git a/packages/browser-sdk/index.html b/packages/browser-sdk/index.html
new file mode 100644
index 00000000..597eadec
--- /dev/null
+++ b/packages/browser-sdk/index.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+ Bucket Browser SDK
+
+
+
+ Loading...
+
+
+
+
+
+ Give feedback!
+
+ Start huddle
+
+
+
+
+
diff --git a/packages/browser-sdk/src/client.ts b/packages/browser-sdk/src/client.ts
index 9e16a076..90561c6b 100644
--- a/packages/browser-sdk/src/client.ts
+++ b/packages/browser-sdk/src/client.ts
@@ -14,10 +14,12 @@ import {
RequestFeedbackOptions,
} from "./feedback/feedback";
import * as feedbackLib from "./feedback/ui";
-import { API_BASE_URL, SSE_REALTIME_BASE_URL } from "./config";
+import { ToolbarPosition } from "./toolbar/Toolbar";
+import { API_BASE_URL, APP_BASE_URL, SSE_REALTIME_BASE_URL } from "./config";
import { BucketContext, CompanyContext, UserContext } from "./context";
import { HttpClient } from "./httpClient";
import { Logger, loggerWithPrefix, quietConsoleLogger } from "./logger";
+import { showToolbarToggle } from "./toolbar";
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
const isNode = typeof document === "undefined"; // deno supports "window" but not "document" according to https://remix.run/docs/en/main/guides/gotchas
@@ -91,10 +93,20 @@ export type PayloadContext = {
interface Config {
apiBaseUrl: string;
+ appBaseUrl: string;
sseBaseUrl: string;
enableTracking: boolean;
}
+export type ToolbarOptions =
+ | boolean
+ | {
+ show?: boolean;
+ position?: ToolbarPosition;
+ };
+
+export type FeatureDefinitions = Readonly>;
+
/**
* BucketClient initialization options.
*/
@@ -140,6 +152,11 @@ export interface InitOptions {
*/
apiBaseUrl?: string;
+ /**
+ * Base URL of the Bucket web app. Links open ín this app by default.
+ */
+ appBaseUrl?: string;
+
/**
* @deprecated
* Use `sseBaseUrl` instead.
@@ -166,10 +183,22 @@ export interface InitOptions {
*/
sdkVersion?: string;
enableTracking?: boolean;
+
+ /**
+ * Toolbar configuration (alpha)
+ * @ignore
+ */
+ toolbar?: ToolbarOptions;
+ /**
+ * Local-first definition of features (alpha)
+ * @ignore
+ */
+ featureList?: FeatureDefinitions;
}
const defaultConfig: Config = {
apiBaseUrl: API_BASE_URL,
+ appBaseUrl: APP_BASE_URL,
sseBaseUrl: SSE_REALTIME_BASE_URL,
enableTracking: true,
};
@@ -189,6 +218,17 @@ export interface Feature {
options: Omit,
) => void;
}
+
+function shouldShowToolbar(opts: InitOptions) {
+ const toolbarOpts = opts.toolbar;
+ if (typeof toolbarOpts === "boolean") return toolbarOpts;
+ if (typeof toolbarOpts?.show === "boolean") return toolbarOpts.show;
+
+ return (
+ opts.featureList !== undefined && window?.location?.hostname === "localhost"
+ );
+}
+
/**
* BucketClient lets you interact with the Bucket API.
*
@@ -220,9 +260,10 @@ export class BucketClient {
this.config = {
apiBaseUrl: opts?.apiBaseUrl ?? opts?.host ?? defaultConfig.apiBaseUrl,
+ appBaseUrl: opts?.appBaseUrl ?? opts?.host ?? defaultConfig.appBaseUrl,
sseBaseUrl: opts?.sseBaseUrl ?? opts?.sseHost ?? defaultConfig.sseBaseUrl,
enableTracking: opts?.enableTracking ?? defaultConfig.enableTracking,
- } satisfies Config;
+ };
const feedbackOpts = handleDeprecatedFeedbackOptions(opts?.feedback);
@@ -244,6 +285,7 @@ export class BucketClient {
company: this.context.company,
other: this.context.otherContext,
},
+ opts?.featureList || [],
this.logger,
opts?.features,
);
@@ -269,6 +311,15 @@ export class BucketClient {
);
}
}
+
+ if (shouldShowToolbar(opts)) {
+ this.logger.info("opening toolbar toggler");
+ showToolbarToggle({
+ bucketClient: this as unknown as BucketClient,
+ position:
+ typeof opts.toolbar === "object" ? opts.toolbar.position : undefined,
+ });
+ }
}
/**
@@ -299,6 +350,13 @@ export class BucketClient {
}
}
+ /**
+ * Get the current configuration.
+ */
+ getConfig() {
+ return this.config;
+ }
+
/**
* Update the user context.
* Performs a shallow merge with the existing user context.
diff --git a/packages/browser-sdk/src/config.ts b/packages/browser-sdk/src/config.ts
index e1baeec7..fd116c7f 100644
--- a/packages/browser-sdk/src/config.ts
+++ b/packages/browser-sdk/src/config.ts
@@ -1,6 +1,7 @@
import { version } from "../package.json";
export const API_BASE_URL = "https://front.bucket.co";
+export const APP_BASE_URL = "https://app.bucket.co";
export const SSE_REALTIME_BASE_URL = "https://livemessaging.bucket.co";
export const SDK_VERSION_HEADER_NAME = "bucket-sdk-version";
diff --git a/packages/browser-sdk/src/feature/features.ts b/packages/browser-sdk/src/feature/features.ts
index c50c723d..32deebfd 100644
--- a/packages/browser-sdk/src/feature/features.ts
+++ b/packages/browser-sdk/src/feature/features.ts
@@ -46,17 +46,24 @@ export type FeaturesOptions = {
fallbackFeatures?: string[];
/**
- * Timeout in milliseconds
+ * Timeout in milliseconds when fetching features
*/
timeoutMs?: number;
/**
- * If set to true client will return cached value when its stale
- * but refetching
+ * If set to true stale features will be returned while refetching features
*/
staleWhileRevalidate?: boolean;
- staleTimeMs?: number;
+
+ /**
+ * If set, features will be cached between page loads for this duration
+ */
expireTimeMs?: number;
+
+ /**
+ * Stale features will be returned if staleWhileRevalidate is true if no new features can be fetched
+ */
+ staleTimeMs?: number;
};
type Config = {
@@ -149,25 +156,22 @@ export const FEATURES_EXPIRE_MS = 30 * 24 * 60 * 60 * 1000; // expire entirely a
const localStorageFetchedFeaturesKey = `__bucket_fetched_features`;
const localStorageOverridesKey = `__bucket_overrides`;
-type OverridesFeatures = Record;
+type OverridesFeatures = Record;
function setOverridesCache(overrides: OverridesFeatures) {
localStorage.setItem(localStorageOverridesKey, JSON.stringify(overrides));
}
function getOverridesCache(): OverridesFeatures {
- try {
- const cachedOverrides = JSON.parse(
- localStorage.getItem(localStorageOverridesKey) || "{}",
- );
+ const cachedOverrides = JSON.parse(
+ localStorage.getItem(localStorageOverridesKey) || "{}",
+ );
- if (!isObject(cachedOverrides)) {
- return {};
- }
- return cachedOverrides;
- } catch (e) {
+ if (!isObject(cachedOverrides)) {
return {};
}
+
+ return cachedOverrides;
}
/**
@@ -176,7 +180,7 @@ function getOverridesCache(): OverridesFeatures {
export class FeaturesClient {
private cache: FeatureCache;
private fetchedFeatures: FetchedFeatures;
- private featureOverrides: OverridesFeatures;
+ private featureOverrides: OverridesFeatures = {};
private features: RawFeatures = {};
@@ -190,6 +194,7 @@ export class FeaturesClient {
constructor(
private httpClient: HttpClient,
private context: context,
+ private featureDefinitions: Readonly,
logger: Logger,
options?: FeaturesOptions & {
cache?: FeatureCache;
@@ -213,7 +218,18 @@ export class FeaturesClient {
this.rateLimiter =
options?.rateLimiter ??
new RateLimiter(FEATURE_EVENTS_PER_MIN, this.logger);
- this.featureOverrides = getOverridesCache();
+
+ try {
+ const storedFeatureOverrides = getOverridesCache();
+ for (const key in storedFeatureOverrides) {
+ if (this.featureDefinitions.includes(key)) {
+ this.featureOverrides[key] = storedFeatureOverrides[key];
+ }
+ }
+ } catch (e) {
+ this.logger.warn("error getting feature overrides from cache", e);
+ this.featureOverrides = {};
+ }
}
async initialize() {
@@ -258,6 +274,10 @@ export class FeaturesClient {
return this.features;
}
+ getFetchedFeatures(): FetchedFeatures {
+ return this.fetchedFeatures;
+ }
+
public async fetchFeatures(): Promise {
const params = this.fetchParams();
try {
@@ -341,13 +361,13 @@ export class FeaturesClient {
};
}
- // add any overrides that aren't in the fetched features
- for (const key in this.featureOverrides) {
- if (!this.features[key]) {
+ // add any features that aren't in the fetched features
+ for (const key of this.featureDefinitions) {
+ if (!mergedFeatures[key]) {
mergedFeatures[key] = {
key,
isEnabled: false,
- isEnabledOverride: this.featureOverrides[key],
+ isEnabledOverride: this.featureOverrides[key] ?? null,
};
}
}
diff --git a/packages/browser-sdk/src/feedback/feedback.ts b/packages/browser-sdk/src/feedback/feedback.ts
index b83428a9..c649b283 100644
--- a/packages/browser-sdk/src/feedback/feedback.ts
+++ b/packages/browser-sdk/src/feedback/feedback.ts
@@ -1,9 +1,9 @@
import { HttpClient } from "../httpClient";
import { Logger } from "../logger";
import { AblySSEChannel, openAblySSEChannel } from "../sse";
+import { Position } from "../ui/types";
import {
- FeedbackPosition,
FeedbackSubmission,
FeedbackTranslations,
OpenFeedbackFormOptions,
@@ -37,7 +37,7 @@ export type FeedbackOptions = {
/**
* Control the placement and behavior of the feedback form.
*/
- position?: FeedbackPosition;
+ position?: Position;
/**
* Add your own custom translations for the feedback form.
@@ -69,7 +69,7 @@ export function handleDeprecatedFeedbackOptions(
};
}
-export type FeatureIdentifier =
+type FeatureIdentifier =
| {
/**
* Bucket feature ID.
@@ -100,6 +100,9 @@ export type RequestFeedbackData = Omit<
*
* This can be used for side effects, such as storing a
* copy of the feedback in your own application or CRM.
+ *
+ * @param {Object} data
+ * @param data.
*/
onAfterSubmit?: (data: FeedbackSubmission) => void;
} & FeatureIdentifier;
@@ -294,7 +297,7 @@ export class AutoFeedback {
private httpClient: HttpClient,
private feedbackPromptHandler: FeedbackPromptHandler = createDefaultFeedbackPromptHandler(),
private userId: string,
- private position: FeedbackPosition = DEFAULT_POSITION,
+ private position: Position = DEFAULT_POSITION,
private feedbackTranslations: Partial = {},
) {}
diff --git a/packages/browser-sdk/src/feedback/ui/FeedbackDialog.css b/packages/browser-sdk/src/feedback/ui/FeedbackDialog.css
index 91a99ec7..96cb6f02 100644
--- a/packages/browser-sdk/src/feedback/ui/FeedbackDialog.css
+++ b/packages/browser-sdk/src/feedback/ui/FeedbackDialog.css
@@ -1,39 +1,3 @@
-@keyframes scale {
- from {
- transform: scale(0.9);
- }
- to {
- transform: scale(1);
- }
-}
-
-@keyframes floatUp {
- from {
- transform: translateY(15%);
- }
- to {
- transform: translateY(0%);
- }
-}
-
-@keyframes floatDown {
- from {
- transform: translateY(-15%);
- }
- to {
- transform: translateY(0%);
- }
-}
-
-@keyframes fade {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
-}
-
.dialog {
position: fixed;
width: 210px;
@@ -69,12 +33,8 @@
}
.arrow {
- position: absolute;
- width: 8px;
- height: 8px;
background-color: var(--bucket-feedback-dialog-background-color, #fff);
box-shadow: var(--bucket-feedback-dialog-border, #d8d9df) -1px -1px 1px 0px;
- transform: rotate(45deg);
&.bottom {
box-shadow: var(--bucket-feedback-dialog-border, #d8d9df) -1px -1px 1px 0px;
@@ -134,62 +94,3 @@
.plug a:hover {
opacity: 0.7;
}
-
-/* Modal */
-
-.dialog.modal {
- margin: auto;
- margin-top: 4rem;
-
- &[open] {
- animation: /* easeOutQuint */
- scale 200ms cubic-bezier(0.22, 1, 0.36, 1),
- fade 200ms cubic-bezier(0.22, 1, 0.36, 1);
-
- &::backdrop {
- animation: fade 200ms cubic-bezier(0.22, 1, 0.36, 1);
- }
- }
-}
-
-/* Anchored */
-
-.dialog.anchored {
- position: absolute;
-
- &[open] {
- animation: /* easeOutQuint */
- scale 200ms cubic-bezier(0.22, 1, 0.36, 1),
- fade 200ms cubic-bezier(0.22, 1, 0.36, 1);
- }
- &.bottom {
- transform-origin: top center;
- }
- &.top {
- transform-origin: bottom center;
- }
- &.left {
- transform-origin: right center;
- }
- &.right {
- transform-origin: left center;
- }
-}
-
-/* Unanchored */
-
-.dialog[open].unanchored {
- &.unanchored-bottom-left,
- &.unanchored-bottom-right {
- animation: /* easeOutQuint */
- floatUp 300ms cubic-bezier(0.22, 1, 0.36, 1),
- fade 300ms cubic-bezier(0.22, 1, 0.36, 1);
- }
-
- &.unanchored-top-left,
- &.unanchored-top-right {
- animation: /* easeOutQuint */
- floatDown 300ms cubic-bezier(0.22, 1, 0.36, 1),
- fade 300ms cubic-bezier(0.22, 1, 0.36, 1);
- }
-}
diff --git a/packages/browser-sdk/src/feedback/ui/FeedbackDialog.tsx b/packages/browser-sdk/src/feedback/ui/FeedbackDialog.tsx
index 8a0f771a..2893ebbe 100644
--- a/packages/browser-sdk/src/feedback/ui/FeedbackDialog.tsx
+++ b/packages/browser-sdk/src/feedback/ui/FeedbackDialog.tsx
@@ -1,33 +1,22 @@
import { Fragment, FunctionComponent, h } from "preact";
-import { useCallback, useEffect, useRef, useState } from "preact/hooks";
+import { useCallback, useState } from "preact/hooks";
+
+import { feedbackContainerId } from "../../ui/constants";
+import { Dialog, useDialog } from "../../ui/Dialog";
+import { Close } from "../../ui/icons/Close";
import { DEFAULT_TRANSLATIONS } from "./config/defaultTranslations";
import { useTimer } from "./hooks/useTimer";
-import { Close } from "./icons/Close";
-import {
- arrow,
- autoUpdate,
- flip,
- offset,
- shift,
- useFloating,
-} from "./packages/floating-ui-preact-dom";
-import { feedbackContainerId } from "./constants";
import { FeedbackForm } from "./FeedbackForm";
import styles from "./index.css?inline";
import { RadialProgress } from "./RadialProgress";
import {
FeedbackScoreSubmission,
FeedbackSubmission,
- Offset,
OpenFeedbackFormOptions,
WithRequired,
} from "./types";
-type Position = Partial<
- Record<"top" | "left" | "right" | "bottom", number | string>
->;
-
export type FeedbackDialogProps = WithRequired<
OpenFeedbackFormOptions,
"onSubmit" | "position"
@@ -47,97 +36,6 @@ export const FeedbackDialog: FunctionComponent = ({
onSubmit,
onScoreSubmit,
}) => {
- const arrowRef = useRef(null);
- const anchor = position.type === "POPOVER" ? position.anchor : null;
- const {
- refs,
- floatingStyles,
- middlewareData,
- placement: actualPlacement,
- } = useFloating({
- elements: {
- reference: anchor,
- },
- transform: false,
- whileElementsMounted: autoUpdate,
- middleware: [
- flip({
- padding: 10,
- mainAxis: true,
- crossAxis: true,
- fallbackAxisSideDirection: "end",
- }),
- shift(),
- offset(8),
- arrow({
- element: arrowRef,
- }),
- ],
- });
-
- let unanchoredPosition: Position = {};
- if (position.type === "DIALOG") {
- const offsetY = parseOffset(position.offset?.y);
- const offsetX = parseOffset(position.offset?.x);
-
- switch (position.placement) {
- case "top-left":
- unanchoredPosition = {
- top: offsetY,
- left: offsetX,
- };
- break;
- case "top-right":
- unanchoredPosition = {
- top: offsetY,
- right: offsetX,
- };
- break;
- case "bottom-left":
- unanchoredPosition = {
- bottom: offsetY,
- left: offsetX,
- };
- break;
- case "bottom-right":
- unanchoredPosition = {
- bottom: offsetY,
- right: offsetX,
- };
- break;
- }
- }
-
- const { x: arrowX, y: arrowY } = middlewareData.arrow ?? {};
-
- const staticSide =
- {
- top: "bottom",
- right: "left",
- bottom: "top",
- left: "right",
- }[actualPlacement.split("-")[0]] || "bottom";
-
- const arrowStyles = {
- left: arrowX != null ? `${arrowX}px` : "",
- top: arrowY != null ? `${arrowY}px` : "",
- right: "",
- bottom: "",
- [staticSide]: "-4px",
- };
-
- const close = useCallback(() => {
- const dialog = refs.floating.current as HTMLDialogElement | null;
- dialog?.close();
- autoClose.stop();
- onClose?.();
- }, [onClose]);
-
- const dismiss = useCallback(() => {
- close();
- onDismiss?.();
- }, [close, onDismiss]);
-
const [feedbackId, setFeedbackId] = useState(undefined);
const [scoreState, setScoreState] = useState<
"idle" | "submitting" | "submitted"
@@ -164,112 +62,54 @@ export const FeedbackDialog: FunctionComponent = ({
[feedbackId, onSubmit],
);
+ const { isOpen, close } = useDialog({ onClose, initialValue: true });
+
const autoClose = useTimer({
enabled: position.type === "DIALOG",
initialDuration: INACTIVE_DURATION_MS,
onEnd: close,
});
- useEffect(() => {
- // Only enable 'quick dismiss' for popovers
- if (position.type === "MODAL" || position.type === "DIALOG") return;
-
- const escapeHandler = (e: KeyboardEvent) => {
- if (e.key == "Escape") {
- dismiss();
- }
- };
-
- const clickOutsideHandler = (e: MouseEvent) => {
- if (
- !(e.target instanceof Element) ||
- !e.target.closest(`#${feedbackContainerId}`)
- ) {
- dismiss();
- }
- };
-
- const observer = new MutationObserver((mutations) => {
- if (position.anchor === null) return;
-
- mutations.forEach((mutation) => {
- const removedNodes = Array.from(mutation.removedNodes);
- const hasBeenRemoved = removedNodes.some((node) => {
- return node === position.anchor || node.contains(position.anchor);
- });
-
- if (hasBeenRemoved) {
- close();
- }
- });
- });
-
- window.addEventListener("mousedown", clickOutsideHandler);
- window.addEventListener("keydown", escapeHandler);
- observer.observe(document.body, {
- subtree: true,
- childList: true,
- });
-
- return () => {
- window.removeEventListener("mousedown", clickOutsideHandler);
- window.removeEventListener("keydown", escapeHandler);
- observer.disconnect();
- };
- }, [position.type, close]);
+ const dismiss = useCallback(() => {
+ autoClose.stop();
+ close();
+ onDismiss?.();
+ }, [autoClose, close, onDismiss]);
return (
<>
-
-
-
-
- {!autoClose.stopped && autoClose.elapsedFraction > 0 && (
-
- )}
-
-
-
- {anchor && (
-
- )}
-
+ <>
+
+
+
+ {!autoClose.stopped && autoClose.elapsedFraction > 0 && (
+
+ )}
+
+
+ >
+
>
);
};
-
-function parseOffset(offsetInput?: Offset["x"] | Offset["y"]) {
- if (offsetInput === undefined) return "1rem";
- if (typeof offsetInput === "number") return offsetInput + "px";
-
- return offsetInput;
-}
diff --git a/packages/browser-sdk/src/feedback/ui/FeedbackForm.tsx b/packages/browser-sdk/src/feedback/ui/FeedbackForm.tsx
index 949ce658..f1b7b446 100644
--- a/packages/browser-sdk/src/feedback/ui/FeedbackForm.tsx
+++ b/packages/browser-sdk/src/feedback/ui/FeedbackForm.tsx
@@ -1,8 +1,9 @@
import { FunctionComponent, h } from "preact";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
-import { Check } from "./icons/Check";
-import { CheckCircle } from "./icons/CheckCircle";
+import { Check } from "../../ui/icons/Check";
+import { CheckCircle } from "../../ui/icons/CheckCircle";
+
import { Button } from "./Button";
import { Plug } from "./Plug";
import { StarRating } from "./StarRating";
diff --git a/packages/browser-sdk/src/feedback/ui/Plug.tsx b/packages/browser-sdk/src/feedback/ui/Plug.tsx
index dc8add02..f315708f 100644
--- a/packages/browser-sdk/src/feedback/ui/Plug.tsx
+++ b/packages/browser-sdk/src/feedback/ui/Plug.tsx
@@ -1,12 +1,12 @@
import { FunctionComponent, h } from "preact";
-import { Logo } from "./icons/Logo";
+import { Logo } from "../../ui/icons/Logo";
export const Plug: FunctionComponent = () => {
return (
);
diff --git a/packages/browser-sdk/src/feedback/ui/StarRating.tsx b/packages/browser-sdk/src/feedback/ui/StarRating.tsx
index ffe6ec1a..e8e439a5 100644
--- a/packages/browser-sdk/src/feedback/ui/StarRating.tsx
+++ b/packages/browser-sdk/src/feedback/ui/StarRating.tsx
@@ -1,12 +1,17 @@
import { Fragment, FunctionComponent, h } from "preact";
import { useRef } from "preact/hooks";
-import { Dissatisfied } from "./icons/Dissatisfied";
-import { Neutral } from "./icons/Neutral";
-import { Satisfied } from "./icons/Satisfied";
-import { VeryDissatisfied } from "./icons/VeryDissatisfied";
-import { VerySatisfied } from "./icons/VerySatisfied";
-import { arrow, offset, useFloating } from "./packages/floating-ui-preact-dom";
+import { Dissatisfied } from "../../ui/icons/Dissatisfied";
+import { Neutral } from "../../ui/icons/Neutral";
+import { Satisfied } from "../../ui/icons/Satisfied";
+import { VeryDissatisfied } from "../../ui/icons/VeryDissatisfied";
+import { VerySatisfied } from "../../ui/icons/VerySatisfied";
+import {
+ arrow,
+ offset,
+ useFloating,
+} from "../../ui/packages/floating-ui-preact-dom";
+
import { FeedbackTranslations } from "./types";
const scores = [
diff --git a/packages/browser-sdk/src/feedback/ui/config/defaultTranslations.tsx b/packages/browser-sdk/src/feedback/ui/config/defaultTranslations.tsx
index 9bf80413..c7edc9a2 100644
--- a/packages/browser-sdk/src/feedback/ui/config/defaultTranslations.tsx
+++ b/packages/browser-sdk/src/feedback/ui/config/defaultTranslations.tsx
@@ -1,5 +1,4 @@
import { FeedbackTranslations } from "../types";
-
/**
* {@includeCode ./defaultTranslations.tsx}
*/
diff --git a/packages/browser-sdk/src/feedback/ui/index.ts b/packages/browser-sdk/src/feedback/ui/index.ts
index f1e02922..21ead923 100644
--- a/packages/browser-sdk/src/feedback/ui/index.ts
+++ b/packages/browser-sdk/src/feedback/ui/index.ts
@@ -1,10 +1,12 @@
import { h, render } from "preact";
-import { feedbackContainerId, propagatedEvents } from "./constants";
+import { feedbackContainerId, propagatedEvents } from "../../ui/constants";
+import { Position } from "../../ui/types";
+
import { FeedbackDialog } from "./FeedbackDialog";
-import { FeedbackPosition, OpenFeedbackFormOptions } from "./types";
+import { OpenFeedbackFormOptions } from "./types";
-export const DEFAULT_POSITION: FeedbackPosition = {
+export const DEFAULT_POSITION: Position = {
type: "DIALOG",
placement: "bottom-right",
};
@@ -31,6 +33,11 @@ function attachDialogContainer() {
return container.shadowRoot!;
}
+// this is a counter that increases every time the feedback form is opened
+// and since it's passed as a key to the FeedbackDialog component,
+// it forces a re-render on every form open
+let openInstances = 0;
+
export function openFeedbackForm(options: OpenFeedbackFormOptions): void {
const shadowRoot = attachDialogContainer();
const position = options.position || DEFAULT_POSITION;
@@ -53,11 +60,10 @@ export function openFeedbackForm(options: OpenFeedbackFormOptions): void {
}
}
- render(h(FeedbackDialog, { ...options, position }), shadowRoot);
+ openInstances++;
- const dialog = shadowRoot.querySelector("dialog");
-
- if (dialog && !dialog.hasAttribute("open")) {
- dialog[position.type === "MODAL" ? "showModal" : "show"]();
- }
+ render(
+ h(FeedbackDialog, { ...options, position, key: openInstances.toString() }),
+ shadowRoot,
+ );
}
diff --git a/packages/browser-sdk/src/feedback/ui/types.ts b/packages/browser-sdk/src/feedback/ui/types.ts
index c3b92a1b..79d7d591 100644
--- a/packages/browser-sdk/src/feedback/ui/types.ts
+++ b/packages/browser-sdk/src/feedback/ui/types.ts
@@ -1,26 +1,6 @@
-export type WithRequired = T & { [P in K]-?: T[P] };
-
-export type FeedbackPlacement =
- | "bottom-right"
- | "bottom-left"
- | "top-right"
- | "top-left";
-
-export type Offset = {
- /**
- * Offset from the nearest horizontal screen edge after placement is resolved
- */
- x?: string | number;
- /**
- * Offset from the nearest vertical screen edge after placement is resolved
- */
- y?: string | number;
-};
+import { Position } from "../../ui/types";
-export type FeedbackPosition =
- | { type: "MODAL" }
- | { type: "DIALOG"; placement: FeedbackPlacement; offset?: Offset }
- | { type: "POPOVER"; anchor: HTMLElement | null };
+export type WithRequired = T & { [P in K]-?: T[P] };
export interface FeedbackSubmission {
question: string;
@@ -46,7 +26,7 @@ export interface OpenFeedbackFormOptions {
/**
* Control the placement and behavior of the feedback form.
*/
- position?: FeedbackPosition;
+ position?: Position;
/**
* Add your own custom translations for the feedback form.
@@ -67,7 +47,6 @@ export interface OpenFeedbackFormOptions {
onClose?: () => void;
onDismiss?: () => void;
}
-
/**
* You can use this to override text values in the feedback form
* with desired language translation
diff --git a/packages/browser-sdk/src/index.ts b/packages/browser-sdk/src/index.ts
index f8cd11db..7b1186cb 100644
--- a/packages/browser-sdk/src/index.ts
+++ b/packages/browser-sdk/src/index.ts
@@ -1,4 +1,6 @@
-export type { Feature, InitOptions } from "./client";
+// import "preact/debug";
+
+export type { Feature, InitOptions, ToolbarOptions } from "./client";
export { BucketClient } from "./client";
export type { BucketContext, CompanyContext, UserContext } from "./context";
export type {
@@ -8,7 +10,6 @@ export type {
RawFeatures,
} from "./feature/features";
export type {
- FeatureIdentifier,
Feedback,
FeedbackOptions,
FeedbackPrompt,
@@ -22,15 +23,12 @@ export type {
UnassignedFeedback,
} from "./feedback/feedback";
export type { DEFAULT_TRANSLATIONS } from "./feedback/ui/config/defaultTranslations";
-export { feedbackContainerId, propagatedEvents } from "./feedback/ui/constants";
export type {
- FeedbackPlacement,
- FeedbackPosition,
FeedbackScoreSubmission,
FeedbackSubmission,
FeedbackTranslations,
- Offset,
OnScoreSubmitResult,
OpenFeedbackFormOptions,
} from "./feedback/ui/types";
export type { Logger } from "./logger";
+export { feedbackContainerId, propagatedEvents } from "./ui/constants";
diff --git a/packages/browser-sdk/src/toolbar/Features.css b/packages/browser-sdk/src/toolbar/Features.css
new file mode 100644
index 00000000..ee17134f
--- /dev/null
+++ b/packages/browser-sdk/src/toolbar/Features.css
@@ -0,0 +1,74 @@
+.search-input {
+ background: transparent;
+ border: none;
+ color: white;
+ width: 100%;
+ font-size: var(--text-size);
+
+ &::placeholder {
+ color: var(--gray500);
+ }
+
+ &::-webkit-search-cancel-button {
+ -webkit-appearance: none;
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ margin-left: 10px;
+ background: linear-gradient(
+ 45deg,
+ rgba(0, 0, 0, 0) 0%,
+ rgba(0, 0, 0, 0) 43%,
+ #fff 45%,
+ #fff 55%,
+ rgba(0, 0, 0, 0) 57%,
+ rgba(0, 0, 0, 0) 100%
+ ),
+ linear-gradient(
+ 135deg,
+ transparent 0%,
+ transparent 43%,
+ #fff 45%,
+ #fff 55%,
+ transparent 57%,
+ transparent 100%
+ );
+ cursor: pointer;
+ }
+}
+
+.features-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.feature-name-cell {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ width: auto;
+ padding: 6px 6px 6px 0;
+}
+
+.feature-link {
+ color: var(--text-color);
+ text-decoration: none;
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+.feature-reset-cell {
+ width: 30px;
+ padding: 6px 0;
+ text-align: right;
+}
+
+.reset {
+ color: var(--brand300);
+}
+
+.feature-switch-cell {
+ padding: 6px 0 6px 6px;
+ width: 0;
+}
diff --git a/packages/browser-sdk/src/toolbar/Features.tsx b/packages/browser-sdk/src/toolbar/Features.tsx
new file mode 100644
index 00000000..9867a57d
--- /dev/null
+++ b/packages/browser-sdk/src/toolbar/Features.tsx
@@ -0,0 +1,113 @@
+import { h } from "preact";
+
+import { Switch } from "./Switch";
+import { FeatureItem } from "./Toolbar";
+
+export function FeaturesTable({
+ features,
+ setEnabledOverride,
+ appBaseUrl,
+}: {
+ features: FeatureItem[];
+ setEnabledOverride: (key: string, value: boolean | null) => void;
+ appBaseUrl: string;
+}) {
+ if (features.length === 0) {
+ return No features found
;
+ }
+ return (
+
+
+ {features.map((feature) => (
+
+ ))}
+
+
+ );
+}
+
+function FeatureRow({
+ setEnabledOverride,
+ appBaseUrl,
+ feature,
+}: {
+ feature: FeatureItem;
+ appBaseUrl: string;
+ setEnabledOverride: (key: string, value: boolean | null) => void;
+}) {
+ return (
+
+
+
+ {feature.key}
+
+
+
+ {feature.localOverride !== null ? (
+
+ ) : null}
+
+
+ {
+ const isChecked = e.currentTarget.checked;
+ const isOverridden = isChecked !== feature.isEnabled;
+ setEnabledOverride(feature.key, isOverridden ? isChecked : null);
+ }}
+ />
+
+
+ );
+}
+
+export function FeatureSearch({
+ onSearch,
+}: {
+ onSearch: (val: string) => void;
+}) {
+ return (
+ onSearch(s.currentTarget.value)}
+ autoFocus
+ class="search-input"
+ />
+ );
+}
+
+function Reset({
+ setEnabledOverride,
+ featureKey,
+}: {
+ setEnabledOverride: (key: string, value: boolean | null) => void;
+ featureKey: string;
+}) {
+ return (
+ {
+ e.preventDefault();
+ setEnabledOverride(featureKey, null);
+ }}
+ >
+ reset
+
+ );
+}
diff --git a/packages/browser-sdk/src/toolbar/Switch.css b/packages/browser-sdk/src/toolbar/Switch.css
new file mode 100644
index 00000000..335d1b71
--- /dev/null
+++ b/packages/browser-sdk/src/toolbar/Switch.css
@@ -0,0 +1,22 @@
+.switch {
+ cursor: pointer;
+}
+
+.switch-track {
+ position: relative;
+ transition: background 0.1s ease;
+ background: #606476;
+}
+
+.switch[data-enabled="true"] .switch-track {
+ background: #847cfb;
+}
+
+.switch-dot {
+ background: white;
+ border-radius: 50%;
+ position: absolute;
+ top: 1px;
+ transition: transform 0.1s ease-in-out;
+ box-shadow: 0 0px 5px rgba(0, 0, 0, 0.2);
+}
diff --git a/packages/browser-sdk/src/toolbar/Switch.tsx b/packages/browser-sdk/src/toolbar/Switch.tsx
new file mode 100644
index 00000000..deb212c9
--- /dev/null
+++ b/packages/browser-sdk/src/toolbar/Switch.tsx
@@ -0,0 +1,50 @@
+import { Fragment, h } from "preact";
+
+interface SwitchProps extends h.JSX.HTMLAttributes {
+ checked: boolean;
+ width?: number;
+ height?: number;
+}
+
+const gutter = 1;
+
+export function Switch({
+ checked,
+ width = 24,
+ height = 14,
+ ...props
+}: SwitchProps) {
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/packages/browser-sdk/src/toolbar/Toolbar.css b/packages/browser-sdk/src/toolbar/Toolbar.css
new file mode 100644
index 00000000..312b4441
--- /dev/null
+++ b/packages/browser-sdk/src/toolbar/Toolbar.css
@@ -0,0 +1,163 @@
+/* Animations */
+
+@keyframes bounceInUp {
+ from,
+ 60%,
+ 75%,
+ 90%,
+ to {
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ }
+
+ from {
+ opacity: 0;
+ transform: translate3d(0, 1000px, 0) scaleY(5);
+ }
+
+ 60% {
+ opacity: 1;
+ transform: translate3d(0, -10px, 0) scaleY(0.9);
+ }
+
+ 75% {
+ transform: translate3d(0, 5px, 0) scaleY(0.95);
+ }
+
+ 90% {
+ transform: translate3d(0, -3px, 0) scaleY(0.985);
+ }
+
+ to {
+ transform: translate3d(0, 0, 0);
+ }
+}
+
+@keyframes gelatine {
+ from,
+ to {
+ transform: scale(1, 1);
+ }
+ 25% {
+ transform: scale(0.9, 1.1);
+ }
+ 50% {
+ transform: scale(1.1, 0.9);
+ }
+ 75% {
+ transform: scale(0.95, 1.05);
+ }
+}
+
+/* Toolbar */
+
+.toolbar {
+ --brand300: #a39dfc;
+ --brand400: #847cfb;
+ --gray500: #787c91;
+ --black: #1e1f24;
+
+ --bg-color: #1e1f24;
+ --border-color: #292b32;
+ --logo-color: white;
+ --text-color: white;
+ --text-size: 13px;
+ font-family:
+ system-ui,
+ -apple-system,
+ BlinkMacSystemFont,
+ "Segoe UI",
+ Roboto,
+ Oxygen,
+ Ubuntu,
+ Cantarell,
+ "Open Sans",
+ "Helvetica Neue",
+ sans-serif;
+ font-size: var(--text-size);
+}
+
+:focus {
+ outline: none;
+}
+
+.dialog {
+ color: #ffffff;
+ box-sizing: border-box;
+ background-color: var(--black);
+ border: 1px solid var(--border-color);
+ box-shadow:
+ 0px 10px 15px -3px rgba(0, 0, 0, 0.1),
+ 0px 4px 6px -2px rgba(0, 0, 0, 0.05);
+ border-radius: 7px;
+ z-index: 999999;
+ min-width: 200px;
+ padding: 0;
+}
+
+.toolbar-toggle {
+ width: 34px;
+ height: 34px;
+ position: fixed;
+ z-index: 999999;
+ padding: 0;
+ margin: 0;
+ box-sizing: border-box;
+
+ color: var(--logo-color);
+ background: var(--bg-color);
+ border: 1px solid var(--border-color);
+ box-shadow:
+ 0px 10px 15px -3px rgba(0, 0, 0, 0.15),
+ 0px 4px 6px -2px rgba(0, 0, 0, 0.1);
+ border-radius: 10px;
+
+ cursor: pointer;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ animation: bounceInUp 1s ease-out;
+
+ transition: color 0.1s ease;
+
+ &.open {
+ color: var(--gray500);
+ }
+
+ & .override-indicator {
+ position: absolute;
+ top: -3px;
+ right: -3px;
+ width: 8px;
+ height: 8px;
+ background-color: var(--brand400);
+ border-radius: 50%;
+ opacity: 0;
+ transition: opacity 0.1s ease-in-out;
+ border: 1px solid var(--brand300);
+
+ &.show {
+ opacity: 1;
+ animation: gelatine 0.5s;
+ }
+ }
+}
+
+.arrow {
+ background-color: var(--black);
+ box-shadow: var(--border-color) -1px -1px 1px 0px;
+
+ &.bottom {
+ box-shadow: var(--border-color) -1px -1px 1px 0px;
+ }
+ &.top {
+ box-shadow: var(--border-color) 1px 1px 1px 0px;
+ }
+ &.left {
+ box-shadow: var(--border-color) 1px -1px 1px 0px;
+ }
+ &.right {
+ box-shadow: var(--border-color) -1px 1px 1px 0px;
+ }
+}
diff --git a/packages/browser-sdk/src/toolbar/Toolbar.tsx b/packages/browser-sdk/src/toolbar/Toolbar.tsx
new file mode 100644
index 00000000..b30efa0f
--- /dev/null
+++ b/packages/browser-sdk/src/toolbar/Toolbar.tsx
@@ -0,0 +1,146 @@
+import { h } from "preact";
+import { useEffect, useMemo, useRef, useState } from "preact/hooks";
+
+import { BucketClient } from "../client";
+import { toolbarContainerId } from "../ui/constants";
+import { Dialog, DialogContent, DialogHeader, useDialog } from "../ui/Dialog";
+import { Logo } from "../ui/icons/Logo";
+import { DialogPlacement, Offset } from "../ui/types";
+import { parseUnanchoredPosition } from "../ui/utils";
+
+import { FeatureSearch, FeaturesTable } from "./Features";
+import styles from "./index.css?inline";
+
+export type FeatureItem = {
+ key: string;
+ localOverride: boolean | null;
+ isEnabled: boolean;
+};
+export interface ToolbarPosition {
+ placement: DialogPlacement;
+ offset?: Offset;
+}
+
+type Feature = {
+ key: string;
+ isEnabled: boolean;
+ localOverride: boolean | null;
+};
+
+export default function Toolbar({
+ bucketClient,
+ position,
+}: {
+ bucketClient: BucketClient;
+ position: ToolbarPosition;
+}) {
+ const toggleToolbarRef = useRef(null);
+ const [features, setFeatures] = useState([]);
+
+ function updateFeatures() {
+ const rawFeatures = bucketClient.getFeatures();
+ setFeatures(
+ Object.values(rawFeatures)
+ .filter((f) => f !== undefined)
+ .map(
+ (feature) =>
+ ({
+ key: feature.key,
+ localOverride: bucketClient.getFeatureOverride(feature?.key),
+ isEnabled: feature.isEnabled,
+ }) satisfies FeatureItem,
+ ),
+ );
+ }
+
+ const hasAnyOverrides = useMemo(() => {
+ return features.some((f) => f.localOverride !== null);
+ }, [features]);
+
+ useEffect(() => {
+ updateFeatures();
+ return bucketClient.onFeaturesUpdated(updateFeatures);
+ }, [bucketClient]);
+
+ const [search, setSearch] = useState(null);
+ const onSearch = (val: string) => {
+ setSearch(val === "" ? null : val);
+ };
+
+ const searchedFeatures =
+ search === null ? features : features.filter((f) => f.key.includes(search));
+
+ const appBaseUrl = bucketClient.getConfig().appBaseUrl;
+
+ const { isOpen, close, toggle } = useDialog();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function ToolbarToggle({
+ isOpen,
+ position,
+ onClick,
+ innerRef,
+ hasAnyOverrides,
+}: {
+ isOpen: boolean;
+ position: ToolbarPosition;
+ onClick: () => void;
+ innerRef: React.RefObject;
+ hasAnyOverrides: boolean;
+ children?: preact.VNode;
+}) {
+ const offsets = parseUnanchoredPosition(position);
+
+ const toggleClasses = ["toolbar-toggle", isOpen ? "open" : undefined].join(
+ " ",
+ );
+
+ const indicatorClasses = [
+ "override-indicator",
+ hasAnyOverrides ? "show" : undefined,
+ ].join(" ");
+
+ return (
+
+ );
+}
diff --git a/packages/browser-sdk/src/toolbar/index.css b/packages/browser-sdk/src/toolbar/index.css
new file mode 100644
index 00000000..7028c976
--- /dev/null
+++ b/packages/browser-sdk/src/toolbar/index.css
@@ -0,0 +1,3 @@
+@import url(./Toolbar.css);
+@import url(./Features.css);
+@import url(./Switch.css);
diff --git a/packages/browser-sdk/src/toolbar/index.ts b/packages/browser-sdk/src/toolbar/index.ts
new file mode 100644
index 00000000..51d43d20
--- /dev/null
+++ b/packages/browser-sdk/src/toolbar/index.ts
@@ -0,0 +1,23 @@
+import { h, render } from "preact";
+
+import { BucketClient } from "../client";
+import { toolbarContainerId } from "../ui/constants";
+import { attachContainer } from "../ui/utils";
+
+import Toolbar, { ToolbarPosition } from "./Toolbar";
+
+type showToolbarToggleOptions = {
+ bucketClient: BucketClient;
+ position?: ToolbarPosition;
+};
+
+export const DEFAULT_PLACEMENT = "bottom-right" as const;
+
+export function showToolbarToggle(options: showToolbarToggleOptions) {
+ const shadowRoot = attachContainer(toolbarContainerId);
+ const position: ToolbarPosition = options.position ?? {
+ placement: DEFAULT_PLACEMENT,
+ };
+
+ render(h(Toolbar, { ...options, position }), shadowRoot);
+}
diff --git a/packages/browser-sdk/src/ui/Dialog.css b/packages/browser-sdk/src/ui/Dialog.css
new file mode 100644
index 00000000..dcb6c24c
--- /dev/null
+++ b/packages/browser-sdk/src/ui/Dialog.css
@@ -0,0 +1,113 @@
+/* Animations */
+
+@keyframes scale {
+ from {
+ transform: scale(0.9);
+ }
+ to {
+ transform: scale(1);
+ }
+}
+
+@keyframes floatUp {
+ from {
+ transform: translateY(15%);
+ }
+ to {
+ transform: translateY(0%);
+ }
+}
+
+@keyframes floatDown {
+ from {
+ transform: translateY(-15%);
+ }
+ to {
+ transform: translateY(0%);
+ }
+}
+
+@keyframes fade {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+/* Modal */
+
+.dialog.modal {
+ margin: auto;
+ margin-top: 4rem;
+
+ &[open] {
+ animation: /* easeOutQuint */
+ scale 200ms cubic-bezier(0.22, 1, 0.36, 1),
+ fade 200ms cubic-bezier(0.22, 1, 0.36, 1);
+
+ &::backdrop {
+ animation: fade 200ms cubic-bezier(0.22, 1, 0.36, 1);
+ }
+ }
+}
+
+/* Anchored */
+
+.dialog.anchored {
+ position: absolute;
+ margin: 0;
+
+ &[open] {
+ animation: /* easeOutQuint */
+ scale 200ms cubic-bezier(0.22, 1, 0.36, 1),
+ fade 200ms cubic-bezier(0.22, 1, 0.36, 1);
+ }
+ &.bottom {
+ transform-origin: top center;
+ }
+ &.top {
+ transform-origin: bottom center;
+ }
+ &.left {
+ transform-origin: right center;
+ }
+ &.right {
+ transform-origin: left center;
+ }
+}
+
+/* Unanchored */
+
+.dialog[open].unanchored {
+ &.unanchored-bottom-left,
+ &.unanchored-bottom-right {
+ animation: /* easeOutQuint */
+ floatUp 300ms cubic-bezier(0.22, 1, 0.36, 1),
+ fade 300ms cubic-bezier(0.22, 1, 0.36, 1);
+ }
+
+ &.unanchored-top-left,
+ &.unanchored-top-right {
+ animation: /* easeOutQuint */
+ floatDown 300ms cubic-bezier(0.22, 1, 0.36, 1),
+ fade 300ms cubic-bezier(0.22, 1, 0.36, 1);
+ }
+}
+
+.dialog .arrow {
+ position: absolute;
+ width: 8px;
+ height: 8px;
+ transform: rotate(45deg);
+}
+
+.dialog-header {
+ border-bottom: 1px solid var(--border-color);
+ padding: 7px 12px;
+}
+
+.dialog-content {
+ padding: 7px 12px;
+}
diff --git a/packages/browser-sdk/src/ui/Dialog.tsx b/packages/browser-sdk/src/ui/Dialog.tsx
new file mode 100644
index 00000000..ca9da76b
--- /dev/null
+++ b/packages/browser-sdk/src/ui/Dialog.tsx
@@ -0,0 +1,263 @@
+import { MiddlewareData, Placement } from "@floating-ui/dom";
+import { Fragment, FunctionComponent, h, Ref } from "preact";
+import { useCallback, useEffect, useRef, useState } from "preact/hooks";
+
+import {
+ arrow,
+ autoUpdate,
+ flip,
+ offset,
+ shift,
+ useFloating,
+} from "./packages/floating-ui-preact-dom";
+import styles from "./Dialog.css?inline";
+import { Position } from "./types";
+import { parseUnanchoredPosition } from "./utils";
+
+type CssPosition = Partial<
+ Record<"top" | "left" | "right" | "bottom", number | string>
+>;
+
+export interface OpenDialogOptions {
+ /**
+ * Control the placement and behavior of the dialog.
+ */
+ position: Position;
+
+ strategy?: "fixed" | "absolute";
+
+ isOpen: boolean;
+ close: () => void;
+ onDismiss?: () => void;
+
+ containerId: string;
+
+ children?: preact.ComponentChildren;
+}
+
+export function useDialog({
+ onClose,
+ onOpen,
+ initialValue = false,
+}: {
+ onClose?: () => void;
+ onOpen?: () => void;
+ initialValue?: boolean;
+} = {}) {
+ const [isOpen, setIsOpen] = useState(initialValue);
+ return {
+ isOpen,
+ open: () => {
+ setIsOpen(true);
+ onOpen?.();
+ },
+ close: () => {
+ setIsOpen(false);
+ onClose?.();
+ },
+ toggle: () => {
+ if (isOpen) onClose?.();
+ else onOpen?.();
+ setIsOpen((prev) => !prev);
+ },
+ };
+}
+
+export const Dialog: FunctionComponent = ({
+ position,
+ isOpen,
+ close,
+ onDismiss,
+ containerId,
+ strategy,
+ children,
+}) => {
+ const arrowRef = useRef(null);
+ const dialogRef = useRef(null);
+
+ const anchor = position.type === "POPOVER" ? position.anchor : null;
+ const placement =
+ position.type === "POPOVER" ? position.placement : undefined;
+
+ const {
+ refs,
+ floatingStyles,
+ middlewareData,
+ placement: actualPlacement,
+ } = useFloating({
+ elements: {
+ reference: anchor,
+ },
+ strategy,
+ transform: false,
+ placement,
+ whileElementsMounted: autoUpdate,
+ middleware: [
+ flip({
+ padding: 10,
+ mainAxis: true,
+ crossAxis: true,
+ }),
+ shift(),
+ offset(8),
+ arrow({
+ element: arrowRef,
+ }),
+ ],
+ });
+
+ let unanchoredPosition: CssPosition = {};
+ if (position.type === "DIALOG") {
+ unanchoredPosition = parseUnanchoredPosition(position);
+ }
+
+ const dismiss = useCallback(() => {
+ close();
+ onDismiss?.();
+ }, [close, onDismiss]);
+
+ useEffect(() => {
+ // Only enable 'quick dismiss' for popovers
+ if (position.type === "MODAL" || position.type === "DIALOG") return;
+
+ const escapeHandler = (e: KeyboardEvent) => {
+ if (e.key == "Escape") {
+ dismiss();
+ }
+ };
+
+ const clickOutsideHandler = (e: MouseEvent) => {
+ if (
+ !(e.target instanceof Element) ||
+ !e.target.closest(`#${containerId}`)
+ ) {
+ dismiss();
+ }
+ };
+
+ const observer = new MutationObserver((mutations) => {
+ if (position.anchor === null) return;
+
+ mutations.forEach((mutation) => {
+ const removedNodes = Array.from(mutation.removedNodes);
+ const hasBeenRemoved = removedNodes.some((node) => {
+ return node === position.anchor || node.contains(position.anchor);
+ });
+
+ if (hasBeenRemoved) {
+ close();
+ }
+ });
+ });
+
+ window.addEventListener("mousedown", clickOutsideHandler);
+ window.addEventListener("keydown", escapeHandler);
+ observer.observe(document.body, {
+ subtree: true,
+ childList: true,
+ });
+
+ return () => {
+ window.removeEventListener("mousedown", clickOutsideHandler);
+ window.removeEventListener("keydown", escapeHandler);
+ observer.disconnect();
+ };
+ }, [position.type, close]);
+
+ function setDiagRef(node: HTMLDialogElement | null) {
+ refs.setFloating(node);
+ dialogRef.current = node;
+ }
+
+ useEffect(() => {
+ if (!dialogRef.current) return;
+ if (isOpen && !dialogRef.current.hasAttribute("open")) {
+ dialogRef.current[position.type === "MODAL" ? "showModal" : "show"]();
+ }
+ if (!isOpen && dialogRef.current.hasAttribute("open")) {
+ dialogRef.current.close();
+ }
+ }, [dialogRef, isOpen, position.type]);
+
+ const classes = [
+ "dialog",
+ position.type === "MODAL"
+ ? "modal"
+ : position.type === "POPOVER"
+ ? "anchored"
+ : `unanchored unanchored-${position.placement}`,
+ actualPlacement,
+ ].join(" ");
+
+ return (
+ <>
+
+
+ {children && {children} }
+
+ {anchor && (
+
+ )}
+
+ >
+ );
+};
+
+function DialogArrow({
+ arrowData,
+ arrowRef,
+ placement,
+}: {
+ arrowData: MiddlewareData["arrow"];
+ arrowRef: Ref;
+ placement: Placement;
+}) {
+ const { x: arrowX, y: arrowY } = arrowData ?? {};
+
+ const staticSide =
+ {
+ top: "bottom",
+ right: "left",
+ bottom: "top",
+ left: "right",
+ }[placement.split("-")[0]] || "bottom";
+
+ const arrowStyles = {
+ left: arrowX != null ? `${arrowX}px` : "",
+ top: arrowY != null ? `${arrowY}px` : "",
+ right: "",
+ bottom: "",
+ [staticSide]: "-4px",
+ };
+ return (
+
+ );
+}
+
+export function DialogHeader({
+ children,
+}: {
+ children: preact.ComponentChildren;
+}) {
+ return ;
+}
+
+export function DialogContent({
+ children,
+}: {
+ children: preact.ComponentChildren;
+}) {
+ return {children}
;
+}
diff --git a/packages/browser-sdk/src/feedback/ui/constants.ts b/packages/browser-sdk/src/ui/constants.ts
similarity index 95%
rename from packages/browser-sdk/src/feedback/ui/constants.ts
rename to packages/browser-sdk/src/ui/constants.ts
index 0dc6af35..8d430e46 100644
--- a/packages/browser-sdk/src/feedback/ui/constants.ts
+++ b/packages/browser-sdk/src/ui/constants.ts
@@ -2,6 +2,7 @@
* ID of HTML DIV element which contains the feedback dialog
*/
export const feedbackContainerId = "bucket-feedback-dialog-container";
+export const toolbarContainerId = "bucket-toolbar-dialog-container";
/**
* These events will be propagated to the feedback dialog
diff --git a/packages/browser-sdk/src/feedback/ui/icons/Check.tsx b/packages/browser-sdk/src/ui/icons/Check.tsx
similarity index 100%
rename from packages/browser-sdk/src/feedback/ui/icons/Check.tsx
rename to packages/browser-sdk/src/ui/icons/Check.tsx
diff --git a/packages/browser-sdk/src/feedback/ui/icons/CheckCircle.tsx b/packages/browser-sdk/src/ui/icons/CheckCircle.tsx
similarity index 100%
rename from packages/browser-sdk/src/feedback/ui/icons/CheckCircle.tsx
rename to packages/browser-sdk/src/ui/icons/CheckCircle.tsx
diff --git a/packages/browser-sdk/src/feedback/ui/icons/Close.tsx b/packages/browser-sdk/src/ui/icons/Close.tsx
similarity index 100%
rename from packages/browser-sdk/src/feedback/ui/icons/Close.tsx
rename to packages/browser-sdk/src/ui/icons/Close.tsx
diff --git a/packages/browser-sdk/src/feedback/ui/icons/Dissatisfied.tsx b/packages/browser-sdk/src/ui/icons/Dissatisfied.tsx
similarity index 100%
rename from packages/browser-sdk/src/feedback/ui/icons/Dissatisfied.tsx
rename to packages/browser-sdk/src/ui/icons/Dissatisfied.tsx
diff --git a/packages/browser-sdk/src/feedback/ui/icons/Logo.tsx b/packages/browser-sdk/src/ui/icons/Logo.tsx
similarity index 98%
rename from packages/browser-sdk/src/feedback/ui/icons/Logo.tsx
rename to packages/browser-sdk/src/ui/icons/Logo.tsx
index 2164fb89..c520b8eb 100644
--- a/packages/browser-sdk/src/feedback/ui/icons/Logo.tsx
+++ b/packages/browser-sdk/src/ui/icons/Logo.tsx
@@ -1,11 +1,9 @@
import { FunctionComponent, h } from "preact";
export const Logo: FunctionComponent> = (
- props,
+ props = { height: "10px", width: "10px" },
) => (
= {},
+) {
+ await page.goto("http://localhost:8001/test/e2e/give-feedback-button.html");
+
+ // Mock API calls
+ await page.route(`${API_HOST}/user`, async (route) => {
+ await route.fulfill({ status: 200 });
+ });
+
+ await page.route(`${API_HOST}/features/enabled*`, async (route) => {
+ await route.fulfill({
+ status: 200,
+ body: JSON.stringify({
+ success: true,
+ features: {},
+ }),
+ });
+ });
+
+ // Golden path requests
+ await page.evaluate(`
+ ;(async () => {
+ const { BucketClient } = await import("/dist/bucket-browser-sdk.mjs");
+ const bucket = new BucketClient({publishableKey: "${KEY}", user: {id: "foo"}, company: {id: "bar"}, ...${JSON.stringify(initOptions ?? {})}});
+ await bucket.initialize();
+ console.log("setup clicky", document.querySelector("#give-feedback-button"))
+ document.querySelector("#give-feedback-button")?.addEventListener("click", () => {
+ console.log("cliked!");
+ bucket.requestFeedback({
+ featureId: "featureId1",
+ title: "baz",
+ });
+ });
+ })()
+ `);
+
+ return page.locator(`#${feedbackContainerId}`);
+}
+
async function setScore(container: Locator, score: number) {
await new Promise((resolve) => setTimeout(resolve, 50)); // allow react to update its state
await container
@@ -107,6 +145,23 @@ test("Opens a feedback widget", async ({ page }) => {
await expect(container.locator("dialog")).toHaveAttribute("open", "");
});
+test("Opens a feedback widget multiple times in same session", async ({
+ page,
+}) => {
+ const container = await getGiveFeedbackPageContainer(page);
+
+ await page.getByTestId("give-feedback-button").click();
+ await expect(container).toBeAttached();
+ await expect(container.locator("dialog")).toHaveAttribute("open", "");
+
+ await container.locator("dialog .close").click();
+ await expect(container.locator("dialog")).not.toHaveAttribute("open", "");
+
+ await page.getByTestId("give-feedback-button").click();
+ await expect(container).toBeAttached();
+ await expect(container.locator("dialog")).toHaveAttribute("open", "");
+});
+
test("Opens a feedback widget in the bottom right by default", async ({
page,
}) => {
diff --git a/packages/browser-sdk/test/e2e/give-feedback-button.html b/packages/browser-sdk/test/e2e/give-feedback-button.html
new file mode 100644
index 00000000..51839292
--- /dev/null
+++ b/packages/browser-sdk/test/e2e/give-feedback-button.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Bucket Test
+
+
+
+ Give Feedback
+
+
+
diff --git a/packages/browser-sdk/test/features.test.ts b/packages/browser-sdk/test/features.test.ts
index ccc1b1ab..37be3245 100644
--- a/packages/browser-sdk/test/features.test.ts
+++ b/packages/browser-sdk/test/features.test.ts
@@ -1,6 +1,7 @@
import { afterAll, beforeEach, describe, expect, test, vi } from "vitest";
import { version } from "../package.json";
+import { FeatureDefinitions } from "../src/client";
import {
FEATURES_EXPIRE_MS,
FeaturesClient,
@@ -36,6 +37,7 @@ function featuresClientFactory() {
newFeaturesClient: function newFeaturesClient(
options?: FeaturesOptions,
context?: any,
+ featureList: FeatureDefinitions = [],
) {
return new FeaturesClient(
httpClient,
@@ -45,6 +47,7 @@ function featuresClientFactory() {
other: { eventId: "big-conference1" },
...context,
},
+ featureList,
testLogger,
{
cache,
@@ -301,8 +304,9 @@ describe("FeaturesClient unit tests", () => {
test("handled overrides for features not returned by API", async () => {
// change the response so we can validate that we'll serve the stale cache
const { newFeaturesClient } = featuresClientFactory();
+
// localStorage.clear();
- const client = newFeaturesClient();
+ const client = newFeaturesClient(undefined, undefined, ["featureB"]);
await client.initialize();
let updated = false;
@@ -310,7 +314,10 @@ describe("FeaturesClient unit tests", () => {
updated = true;
});
- expect(client.getFeatures().featureB).toBeUndefined();
+ expect(client.getFeatures().featureB.isEnabled).toBe(false);
+ expect(client.getFeatures().featureB.isEnabledOverride).toBe(null);
+
+ expect(client.getFetchedFeatures()?.featureB).toBeUndefined();
client.setFeatureOverride("featureB", true);
diff --git a/packages/react-sdk/src/index.tsx b/packages/react-sdk/src/index.tsx
index cfc2f7e7..6fffad73 100644
--- a/packages/react-sdk/src/index.tsx
+++ b/packages/react-sdk/src/index.tsx
@@ -17,6 +17,7 @@ import {
FeedbackOptions,
RawFeatures,
RequestFeedbackData,
+ ToolbarOptions,
UnassignedFeedback,
} from "@bucketco/browser-sdk";
@@ -59,6 +60,7 @@ export type BucketProps = BucketContext & {
*/
host?: string;
apiBaseUrl?: string;
+ appBaseUrl?: string;
/**
* @deprecated
@@ -69,6 +71,10 @@ export type BucketProps = BucketContext & {
debug?: boolean;
enableTracking?: boolean;
+ featureList?: Readonly;
+
+ toolbar?: ToolbarOptions;
+
// for testing
newBucketClient?: (
...args: ConstructorParameters
@@ -83,6 +89,7 @@ export function BucketProvider({
publishableKey,
featureOptions,
loadingComponent,
+ featureList,
newBucketClient = (...args) => new BucketClient(...args),
...config
}: BucketProps) {
@@ -116,6 +123,7 @@ export function BucketProvider({
host: config.host,
apiBaseUrl: config.apiBaseUrl,
+ appBaseUrl: config.appBaseUrl,
sseHost: config.sseHost,
sseBaseUrl: config.sseBaseUrl,
@@ -127,6 +135,7 @@ export function BucketProvider({
feedback: config.feedback,
logger: config.debug ? console : undefined,
sdkVersion: SDK_VERSION,
+ featureList,
});
clientRef.current = client;
@@ -146,7 +155,7 @@ export function BucketProvider({
const context: ProviderContextType = {
features: {
- features,
+ features: features,
isLoading: featuresLoading,
},
client: clientRef.current,
@@ -196,7 +205,7 @@ export function useFeature(key: FeatureKey) {
}
const feature = features[key];
- const enabled = feature?.isEnabled ?? false;
+ const enabled = feature?.isEnabledOverride ?? feature?.isEnabled ?? false;
return {
isLoading,
diff --git a/packages/react-sdk/test/usage.test.tsx b/packages/react-sdk/test/usage.test.tsx
index 317a0bcb..97a705d8 100644
--- a/packages/react-sdk/test/usage.test.tsx
+++ b/packages/react-sdk/test/usage.test.tsx
@@ -182,12 +182,14 @@ describe(" ", () => {
test: "test",
},
apiBaseUrl: "https://test.com",
+ appBaseUrl: undefined,
host: undefined,
logger: undefined,
sseBaseUrl: "https://test.com",
sseHost: undefined,
enableTracking: false,
feedback: undefined,
+ featureList: undefined,
features: {},
sdkVersion: `react-sdk/${version}`,
},
From a4a7bfd8839346cf057d7f06ecb52c0fe35f100f Mon Sep 17 00:00:00 2001
From: Lasse Boisen Andersen
Date: Mon, 27 Jan 2025 15:06:03 +0100
Subject: [PATCH 06/24] Bump browser and react SDK to 3.0.0-alpha.0 (#294)
Alpha pre-release of the new Toolbar, see
https://github.com/bucketco/bucket-javascript-sdk/pull/290
---
packages/browser-sdk/package.json | 2 +-
packages/react-sdk/package.json | 4 ++--
yarn.lock | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/packages/browser-sdk/package.json b/packages/browser-sdk/package.json
index b39d0e07..b6117ec5 100644
--- a/packages/browser-sdk/package.json
+++ b/packages/browser-sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@bucketco/browser-sdk",
- "version": "2.5.1",
+ "version": "3.0.0-alpha.0",
"packageManager": "yarn@4.1.1",
"license": "MIT",
"repository": {
diff --git a/packages/react-sdk/package.json b/packages/react-sdk/package.json
index 9fd0b3cf..82884eaa 100644
--- a/packages/react-sdk/package.json
+++ b/packages/react-sdk/package.json
@@ -1,6 +1,6 @@
{
"name": "@bucketco/react-sdk",
- "version": "2.5.2",
+ "version": "3.0.0-alpha.0",
"license": "MIT",
"repository": {
"type": "git",
@@ -34,7 +34,7 @@
}
},
"dependencies": {
- "@bucketco/browser-sdk": "2.5.1",
+ "@bucketco/browser-sdk": "3.0.0-alpha.0",
"canonical-json": "^0.0.4"
},
"peerDependencies": {
diff --git a/yarn.lock b/yarn.lock
index 812c4c4b..718a5435 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -894,7 +894,7 @@ __metadata:
languageName: node
linkType: hard
-"@bucketco/browser-sdk@npm:2.5.1, @bucketco/browser-sdk@workspace:packages/browser-sdk":
+"@bucketco/browser-sdk@npm:3.0.0-alpha.0, @bucketco/browser-sdk@workspace:packages/browser-sdk":
version: 0.0.0-use.local
resolution: "@bucketco/browser-sdk@workspace:packages/browser-sdk"
dependencies:
@@ -1030,7 +1030,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@bucketco/react-sdk@workspace:packages/react-sdk"
dependencies:
- "@bucketco/browser-sdk": "npm:2.5.1"
+ "@bucketco/browser-sdk": "npm:3.0.0-alpha.0"
"@bucketco/eslint-config": "workspace:^"
"@bucketco/tsconfig": "workspace:^"
"@testing-library/react": "npm:^15.0.7"
From fb38c71b8ed2c083a9a104c7e4fc7c5f6871284e Mon Sep 17 00:00:00 2001
From: Lasse Boisen Andersen
Date: Tue, 28 Jan 2025 10:24:36 +0100
Subject: [PATCH 07/24] Fix various minor dependency resolution warnings (#296)
---
package.json | 3 +-
.../example/package.json | 1 +
packages/react-sdk/package.json | 3 +-
yarn.lock | 249 +++++++++++++++++-
4 files changed, 253 insertions(+), 3 deletions(-)
diff --git a/package.json b/package.json
index 2becdc8f..3cfea24c 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"typedoc": "0.27.6",
"typedoc-plugin-frontmatter": "^1.1.2",
"typedoc-plugin-markdown": "^4.4.1",
- "typedoc-plugin-mdn-links": "^4.0.7"
+ "typedoc-plugin-mdn-links": "^4.0.7",
+ "typescript": "^5.7.3"
}
}
diff --git a/packages/openfeature-browser-provider/example/package.json b/packages/openfeature-browser-provider/example/package.json
index 80f0e2ab..32cbb0da 100644
--- a/packages/openfeature-browser-provider/example/package.json
+++ b/packages/openfeature-browser-provider/example/package.json
@@ -10,6 +10,7 @@
},
"dependencies": {
"@bucketco/react-sdk": "workspace:^",
+ "@openfeature/core": "1.3.0",
"@openfeature/react-sdk": "^0.4.5",
"@openfeature/web-sdk": "^1.2.3",
"next": "14.2.21",
diff --git a/packages/react-sdk/package.json b/packages/react-sdk/package.json
index 82884eaa..c8d9a1fa 100644
--- a/packages/react-sdk/package.json
+++ b/packages/react-sdk/package.json
@@ -35,7 +35,8 @@
},
"dependencies": {
"@bucketco/browser-sdk": "3.0.0-alpha.0",
- "canonical-json": "^0.0.4"
+ "canonical-json": "^0.0.4",
+ "rollup": "^4.2.0"
},
"peerDependencies": {
"react": "*",
diff --git a/yarn.lock b/yarn.lock
index 718a5435..e79fc1d5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -894,7 +894,19 @@ __metadata:
languageName: node
linkType: hard
-"@bucketco/browser-sdk@npm:3.0.0-alpha.0, @bucketco/browser-sdk@workspace:packages/browser-sdk":
+"@bucketco/browser-sdk@npm:3.0.0-alpha.0":
+ version: 3.0.0-alpha.0
+ resolution: "@bucketco/browser-sdk@npm:3.0.0-alpha.0"
+ dependencies:
+ "@floating-ui/dom": "npm:^1.6.8"
+ canonical-json: "npm:^0.0.4"
+ js-cookie: "npm:^3.0.5"
+ preact: "npm:^10.22.1"
+ checksum: 10c0/eba1a2f15c49c1aeca37c6d2b218e50d92e2034c51a9acdd6884ce143156dc7f285f9141f5e6cfcc2aed0186c0b8fc61bb9939aca737a1e76d1a2d9d65a7b74c
+ languageName: node
+ linkType: hard
+
+"@bucketco/browser-sdk@workspace:packages/browser-sdk":
version: 0.0.0-use.local
resolution: "@bucketco/browser-sdk@workspace:packages/browser-sdk"
dependencies:
@@ -1046,6 +1058,7 @@ __metadata:
prettier: "npm:^3.3.3"
react: "npm:*"
react-dom: "npm:*"
+ rollup: "npm:^4.2.0"
rollup-preserve-directives: "npm:^1.1.2"
ts-node: "npm:^10.9.2"
typescript: "npm:^5.4.5"
@@ -2852,6 +2865,13 @@ __metadata:
languageName: node
linkType: hard
+"@openfeature/core@npm:1.3.0":
+ version: 1.3.0
+ resolution: "@openfeature/core@npm:1.3.0"
+ checksum: 10c0/48760b65d259d73d80ed5b3e03d5f4f604dfbe4a86561c0fb9c1b56d8a659ddead3c60260259ddca50d70c82d5dc181da5499d8a129b7bdcfeec0892e9865a0c
+ languageName: node
+ linkType: hard
+
"@openfeature/core@npm:1.5.0, @openfeature/core@npm:^1.5.0":
version: 1.5.0
resolution: "@openfeature/core@npm:1.5.0"
@@ -3025,6 +3045,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-android-arm-eabi@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-android-arm-eabi@npm:4.32.0"
+ conditions: os=android & cpu=arm
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-android-arm64@npm:4.21.3":
version: 4.21.3
resolution: "@rollup/rollup-android-arm64@npm:4.21.3"
@@ -3039,6 +3066,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-android-arm64@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-android-arm64@npm:4.32.0"
+ conditions: os=android & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-darwin-arm64@npm:4.21.3":
version: 4.21.3
resolution: "@rollup/rollup-darwin-arm64@npm:4.21.3"
@@ -3053,6 +3087,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-darwin-arm64@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-darwin-arm64@npm:4.32.0"
+ conditions: os=darwin & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-darwin-x64@npm:4.21.3":
version: 4.21.3
resolution: "@rollup/rollup-darwin-x64@npm:4.21.3"
@@ -3067,6 +3108,27 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-darwin-x64@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-darwin-x64@npm:4.32.0"
+ conditions: os=darwin & cpu=x64
+ languageName: node
+ linkType: hard
+
+"@rollup/rollup-freebsd-arm64@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-freebsd-arm64@npm:4.32.0"
+ conditions: os=freebsd & cpu=arm64
+ languageName: node
+ linkType: hard
+
+"@rollup/rollup-freebsd-x64@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-freebsd-x64@npm:4.32.0"
+ conditions: os=freebsd & cpu=x64
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-arm-gnueabihf@npm:4.21.3":
version: 4.21.3
resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.21.3"
@@ -3081,6 +3143,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-arm-gnueabihf@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.32.0"
+ conditions: os=linux & cpu=arm & libc=glibc
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-arm-musleabihf@npm:4.21.3":
version: 4.21.3
resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.21.3"
@@ -3095,6 +3164,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-arm-musleabihf@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.32.0"
+ conditions: os=linux & cpu=arm & libc=musl
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-arm64-gnu@npm:4.21.3":
version: 4.21.3
resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.21.3"
@@ -3109,6 +3185,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-arm64-gnu@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.32.0"
+ conditions: os=linux & cpu=arm64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-arm64-musl@npm:4.21.3":
version: 4.21.3
resolution: "@rollup/rollup-linux-arm64-musl@npm:4.21.3"
@@ -3123,6 +3206,20 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-arm64-musl@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-linux-arm64-musl@npm:4.32.0"
+ conditions: os=linux & cpu=arm64 & libc=musl
+ languageName: node
+ linkType: hard
+
+"@rollup/rollup-linux-loongarch64-gnu@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.32.0"
+ conditions: os=linux & cpu=loong64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-powerpc64le-gnu@npm:4.21.3":
version: 4.21.3
resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.21.3"
@@ -3137,6 +3234,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-powerpc64le-gnu@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.32.0"
+ conditions: os=linux & cpu=ppc64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-riscv64-gnu@npm:4.21.3":
version: 4.21.3
resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.21.3"
@@ -3151,6 +3255,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-riscv64-gnu@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.32.0"
+ conditions: os=linux & cpu=riscv64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-s390x-gnu@npm:4.21.3":
version: 4.21.3
resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.21.3"
@@ -3165,6 +3276,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-s390x-gnu@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.32.0"
+ conditions: os=linux & cpu=s390x & libc=glibc
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-x64-gnu@npm:4.21.3":
version: 4.21.3
resolution: "@rollup/rollup-linux-x64-gnu@npm:4.21.3"
@@ -3179,6 +3297,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-x64-gnu@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-linux-x64-gnu@npm:4.32.0"
+ conditions: os=linux & cpu=x64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-x64-musl@npm:4.21.3":
version: 4.21.3
resolution: "@rollup/rollup-linux-x64-musl@npm:4.21.3"
@@ -3193,6 +3318,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-x64-musl@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-linux-x64-musl@npm:4.32.0"
+ conditions: os=linux & cpu=x64 & libc=musl
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-win32-arm64-msvc@npm:4.21.3":
version: 4.21.3
resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.21.3"
@@ -3207,6 +3339,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-win32-arm64-msvc@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.32.0"
+ conditions: os=win32 & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-win32-ia32-msvc@npm:4.21.3":
version: 4.21.3
resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.21.3"
@@ -3221,6 +3360,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-win32-ia32-msvc@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.32.0"
+ conditions: os=win32 & cpu=ia32
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-win32-x64-msvc@npm:4.21.3":
version: 4.21.3
resolution: "@rollup/rollup-win32-x64-msvc@npm:4.21.3"
@@ -3235,6 +3381,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-win32-x64-msvc@npm:4.32.0":
+ version: 4.32.0
+ resolution: "@rollup/rollup-win32-x64-msvc@npm:4.32.0"
+ conditions: os=win32 & cpu=x64
+ languageName: node
+ linkType: hard
+
"@rushstack/eslint-patch@npm:^1.3.3":
version: 1.10.3
resolution: "@rushstack/eslint-patch@npm:1.10.3"
@@ -11133,6 +11286,7 @@ __metadata:
resolution: "nextjs-openfeature-example@workspace:packages/openfeature-browser-provider/example"
dependencies:
"@bucketco/react-sdk": "workspace:^"
+ "@openfeature/core": "npm:1.3.0"
"@openfeature/react-sdk": "npm:^0.4.5"
"@openfeature/web-sdk": "npm:^1.2.3"
"@types/node": "npm:^20"
@@ -13642,6 +13796,78 @@ __metadata:
languageName: node
linkType: hard
+"rollup@npm:^4.2.0":
+ version: 4.32.0
+ resolution: "rollup@npm:4.32.0"
+ dependencies:
+ "@rollup/rollup-android-arm-eabi": "npm:4.32.0"
+ "@rollup/rollup-android-arm64": "npm:4.32.0"
+ "@rollup/rollup-darwin-arm64": "npm:4.32.0"
+ "@rollup/rollup-darwin-x64": "npm:4.32.0"
+ "@rollup/rollup-freebsd-arm64": "npm:4.32.0"
+ "@rollup/rollup-freebsd-x64": "npm:4.32.0"
+ "@rollup/rollup-linux-arm-gnueabihf": "npm:4.32.0"
+ "@rollup/rollup-linux-arm-musleabihf": "npm:4.32.0"
+ "@rollup/rollup-linux-arm64-gnu": "npm:4.32.0"
+ "@rollup/rollup-linux-arm64-musl": "npm:4.32.0"
+ "@rollup/rollup-linux-loongarch64-gnu": "npm:4.32.0"
+ "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.32.0"
+ "@rollup/rollup-linux-riscv64-gnu": "npm:4.32.0"
+ "@rollup/rollup-linux-s390x-gnu": "npm:4.32.0"
+ "@rollup/rollup-linux-x64-gnu": "npm:4.32.0"
+ "@rollup/rollup-linux-x64-musl": "npm:4.32.0"
+ "@rollup/rollup-win32-arm64-msvc": "npm:4.32.0"
+ "@rollup/rollup-win32-ia32-msvc": "npm:4.32.0"
+ "@rollup/rollup-win32-x64-msvc": "npm:4.32.0"
+ "@types/estree": "npm:1.0.6"
+ fsevents: "npm:~2.3.2"
+ dependenciesMeta:
+ "@rollup/rollup-android-arm-eabi":
+ optional: true
+ "@rollup/rollup-android-arm64":
+ optional: true
+ "@rollup/rollup-darwin-arm64":
+ optional: true
+ "@rollup/rollup-darwin-x64":
+ optional: true
+ "@rollup/rollup-freebsd-arm64":
+ optional: true
+ "@rollup/rollup-freebsd-x64":
+ optional: true
+ "@rollup/rollup-linux-arm-gnueabihf":
+ optional: true
+ "@rollup/rollup-linux-arm-musleabihf":
+ optional: true
+ "@rollup/rollup-linux-arm64-gnu":
+ optional: true
+ "@rollup/rollup-linux-arm64-musl":
+ optional: true
+ "@rollup/rollup-linux-loongarch64-gnu":
+ optional: true
+ "@rollup/rollup-linux-powerpc64le-gnu":
+ optional: true
+ "@rollup/rollup-linux-riscv64-gnu":
+ optional: true
+ "@rollup/rollup-linux-s390x-gnu":
+ optional: true
+ "@rollup/rollup-linux-x64-gnu":
+ optional: true
+ "@rollup/rollup-linux-x64-musl":
+ optional: true
+ "@rollup/rollup-win32-arm64-msvc":
+ optional: true
+ "@rollup/rollup-win32-ia32-msvc":
+ optional: true
+ "@rollup/rollup-win32-x64-msvc":
+ optional: true
+ fsevents:
+ optional: true
+ bin:
+ rollup: dist/bin/rollup
+ checksum: 10c0/3e365a57a366fec5af8ef68b366ddffbff7ecaf426a9ffe3e20bbc1d848cbbb0f384556097efd8e70dec4155d7b56d5808df7f95c75751974aeeac825604b58a
+ languageName: node
+ linkType: hard
+
"rollup@npm:^4.20.0":
version: 4.21.3
resolution: "rollup@npm:4.21.3"
@@ -15297,6 +15523,16 @@ __metadata:
languageName: node
linkType: hard
+"typescript@npm:^5.7.3":
+ version: 5.7.3
+ resolution: "typescript@npm:5.7.3"
+ bin:
+ tsc: bin/tsc
+ tsserver: bin/tsserver
+ checksum: 10c0/b7580d716cf1824736cc6e628ab4cd8b51877408ba2be0869d2866da35ef8366dd6ae9eb9d0851470a39be17cbd61df1126f9e211d8799d764ea7431d5435afa
+ languageName: node
+ linkType: hard
+
"typescript@patch:typescript@npm%3A5.3.3#optional!builtin, typescript@patch:typescript@npm%3A^5.3.3#optional!builtin":
version: 5.3.3
resolution: "typescript@patch:typescript@npm%3A5.3.3#optional!builtin::version=5.3.3&hash=e012d7"
@@ -15337,6 +15573,16 @@ __metadata:
languageName: node
linkType: hard
+"typescript@patch:typescript@npm%3A^5.7.3#optional!builtin":
+ version: 5.7.3
+ resolution: "typescript@patch:typescript@npm%3A5.7.3#optional!builtin::version=5.7.3&hash=5adc0c"
+ bin:
+ tsc: bin/tsc
+ tsserver: bin/tsserver
+ checksum: 10c0/3b56d6afa03d9f6172d0b9cdb10e6b1efc9abc1608efd7a3d2f38773d5d8cfb9bbc68dfb72f0a7de5e8db04fc847f4e4baeddcd5ad9c9feda072234f0d788896
+ languageName: node
+ linkType: hard
+
"uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0":
version: 2.1.0
resolution: "uc.micro@npm:2.1.0"
@@ -16300,6 +16546,7 @@ __metadata:
typedoc-plugin-frontmatter: "npm:^1.1.2"
typedoc-plugin-markdown: "npm:^4.4.1"
typedoc-plugin-mdn-links: "npm:^4.0.7"
+ typescript: "npm:^5.7.3"
languageName: unknown
linkType: soft
From 912ef9789d846549abb167a4b97d4bd55acd9171 Mon Sep 17 00:00:00 2001
From: Alexandru Ciobanu
Date: Tue, 28 Jan 2025 13:45:27 +0100
Subject: [PATCH 08/24] feat(browser-sdk,node-sdk): add avatar support for user
and company contexts (#297)
- Updated README files to document new `avatar` attribute
- Added `avatar` to user and company context types in browser and node
SDKs
- Expanded documentation for special attributes in context objects
- Added type definitions for avatar in relevant type files
---
.vscode/settings.json | 5 +-
packages/browser-sdk/README.md | 19 +++--
packages/browser-sdk/src/client.ts | 112 +++++++++++++++++++++++++----
packages/node-sdk/README.md | 5 +-
packages/node-sdk/src/types.ts | 37 ++++++++++
5 files changed, 160 insertions(+), 18 deletions(-)
diff --git a/.vscode/settings.json b/.vscode/settings.json
index e94603f9..b1230452 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -38,5 +38,8 @@
"**/node_modules": true,
"**/*.lock": true
},
- "typescript.tsdk": "node_modules/typescript/lib"
+ "typescript.tsdk": "node_modules/typescript/lib",
+ "cSpell.words": [
+ "bucketco"
+ ]
}
diff --git a/packages/browser-sdk/README.md b/packages/browser-sdk/README.md
index a2d6265e..f76ce5e2 100644
--- a/packages/browser-sdk/README.md
+++ b/packages/browser-sdk/README.md
@@ -109,15 +109,26 @@ If you supply `user` or `company` objects, they must include at least the `id` p
In addition to the `id`, you must also supply anything additional that you want to be able to evaluate feature targeting rules against.
Attributes cannot be nested (multiple levels) and must be either strings, integers or booleans.
+Some attributes are special and used in Bucket UI:
-- `name` is a special attribute and is used to display name for user/company
-- for `user`, `email` is also special and will be highlighted in the Bucket UI if available
+- `name` is used to display name for `user`/`company`,
+- `email` is accepted for `user`s and will be highlighted in the Bucket UI if available,
+- `avatar` can be provided for both `user` and `company` and should be an URL to an image.
```ts
const bucketClient = new BucketClient({
publishableKey,
- user: { id: "user_123", name: "John Doe", email: "john@acme.com" },
- company: { id: "company_123", name: "Acme, Inc" },
+ user: {
+ id: "user_123",
+ name: "John Doe",
+ email: "john@acme.com"
+ avatar: "https://example.com/images/udsy6363"
+ },
+ company: {
+ id: "company_123",
+ name: "Acme, Inc",
+ avatar: "https://example.com/images/31232ds"
+ },
});
```
diff --git a/packages/browser-sdk/src/client.ts b/packages/browser-sdk/src/client.ts
index 90561c6b..24399556 100644
--- a/packages/browser-sdk/src/client.ts
+++ b/packages/browser-sdk/src/client.ts
@@ -24,80 +24,152 @@ import { showToolbarToggle } from "./toolbar";
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
const isNode = typeof document === "undefined"; // deno supports "window" but not "document" according to https://remix.run/docs/en/main/guides/gotchas
+/**
+ * (Internal) User context.
+ *
+ * @internal
+ */
export type User = {
/**
- * Identifier of the user
+ * Identifier of the user.
*/
userId: string;
/**
- * User attributes
+ * User attributes.
*/
attributes?: {
+ /**
+ * Name of the user.
+ */
name?: string;
+
+ /**
+ * Email of the user.
+ */
+ email?: string;
+
+ /**
+ * Avatar URL of the user.
+ */
+ avatar?: string;
+
+ /**
+ * Custom attributes of the user.
+ */
[key: string]: any;
};
+ /**
+ * Custom context of the user.
+ */
context?: PayloadContext;
};
+/**
+ * (Internal) Company context.
+ *
+ * @internal
+ */
export type Company = {
/**
- * User identifier
+ * User identifier.
*/
userId: string;
/**
- * Company identifier
+ * Company identifier.
*/
companyId: string;
/**
- * Company attributes
+ * Company attributes.
*/
attributes?: {
+ /**
+ * Name of the company.
+ */
name?: string;
+
+ /**
+ * Custom attributes of the company.
+ */
[key: string]: any;
};
context?: PayloadContext;
};
+/**
+ * Tracked event.
+ */
export type TrackedEvent = {
/**
- * Event name
+ * Event name.
*/
event: string;
/**
- * User identifier
+ * User identifier.
*/
userId: string;
/**
- * Company identifier
+ * Company identifier.
*/
companyId?: string;
/**
- * Event attributes
+ * Event attributes.
*/
attributes?: Record;
+ /**
+ * Custom context of the event.
+ */
context?: PayloadContext;
};
+/**
+ * (Internal) Custom context of the event.
+ *
+ * @internal
+ */
export type PayloadContext = {
+ /**
+ * Whether the company and user associated with the event are active.
+ */
active?: boolean;
};
+/**
+ * BucketClient configuration.
+ */
interface Config {
+ /**
+ * Base URL of Bucket servers.
+ */
apiBaseUrl: string;
+
+ /**
+ * Base URL of the Bucket web app.
+ */
appBaseUrl: string;
+
+ /**
+ * Base URL of Bucket servers for SSE connections used by AutoFeedback.
+ */
sseBaseUrl: string;
+
+ /**
+ * Whether to enable tracking.
+ */
enableTracking: boolean;
}
+/**
+ * Toolbar options.
+ */
export type ToolbarOptions =
| boolean
| {
@@ -105,6 +177,9 @@ export type ToolbarOptions =
position?: ToolbarPosition;
};
+/**
+ * Feature definitions.
+ */
export type FeatureDefinitions = Readonly>;
/**
@@ -117,12 +192,14 @@ export interface InitOptions {
publishableKey: string;
/**
- * User related context. If you provide `id` Bucket will enrich the evaluation context with user attributes on Bucket servers.
+ * User related context. If you provide `id` Bucket will enrich the evaluation context with
+ * user attributes on Bucket servers.
*/
user?: UserContext;
/**
- * Company related context. If you provide `id` Bucket will enrich the evaluation context with company attributes on Bucket servers.
+ * Company related context. If you provide `id` Bucket will enrich the evaluation context with
+ * company attributes on Bucket servers.
*/
company?: CompanyContext;
@@ -182,6 +259,10 @@ export interface InitOptions {
* Version of the SDK
*/
sdkVersion?: string;
+
+ /**
+ * Whether to enable tracking. Defaults to `true`.
+ */
enableTracking?: boolean;
/**
@@ -189,6 +270,7 @@ export interface InitOptions {
* @ignore
*/
toolbar?: ToolbarOptions;
+
/**
* Local-first definition of features (alpha)
* @ignore
@@ -203,6 +285,9 @@ const defaultConfig: Config = {
enableTracking: true,
};
+/**
+ * Represents a feature.
+ */
export interface Feature {
/**
* Result of feature flag evaluation
@@ -214,6 +299,10 @@ export interface Feature {
*
*/
track: () => Promise;
+
+ /**
+ * Function to request feedback for this feature.
+ */
requestFeedback: (
options: Omit,
) => void;
@@ -231,7 +320,6 @@ function shouldShowToolbar(opts: InitOptions) {
/**
* BucketClient lets you interact with the Bucket API.
- *
*/
export class BucketClient {
private publishableKey: string;
diff --git a/packages/node-sdk/README.md b/packages/node-sdk/README.md
index 765cb46c..33391253 100644
--- a/packages/node-sdk/README.md
+++ b/packages/node-sdk/README.md
@@ -63,10 +63,12 @@ const boundClient = bucketClient.bindClient({
id: "john_doe",
name: "John Doe",
email: "john@acme.com",
+ avatar: "https://example.com/users/jdoe",
},
company: {
id: "acme_inc",
name: "Acme, Inc.",
+ avatar: "https://example.com/companies/acme",
},
});
@@ -242,7 +244,7 @@ See [example/app.ts](https://github.com/bucketco/bucket-javascript-sdk/tree/main
## Remote flag evaluation with stored context
If you don't want to provide context each time when evaluating feature flags but
-rather you would like to utilise the attributes you sent to Bucket previously
+rather you would like to utilize the attributes you sent to Bucket previously
(by calling `updateCompany` and `updateUser`) you can do so by calling `getFeaturesRemote`
(or `getFeatureRemote` for a specific feature) with providing just `userId` and `companyId`.
These methods will call Bucket's servers and feature flags will be evaluated remotely
@@ -368,6 +370,7 @@ to provide for easier navigation:
- `name` -- display name for `user`/`company`,
- `email` -- the email of the user.
+- `avatar` -- the URL for `user`/`company` avatar image.
Attributes cannot be nested (multiple levels) and must be either strings,
integers or booleans.
diff --git a/packages/node-sdk/src/types.ts b/packages/node-sdk/src/types.ts
index 463e33fd..25b8b8b2 100644
--- a/packages/node-sdk/src/types.ts
+++ b/packages/node-sdk/src/types.ts
@@ -397,19 +397,56 @@ export type Context = {
* The user context. If no `id` key is set, the whole object is ignored.
*/
user?: {
+ /**
+ * The identifier of the user.
+ */
id: string | number | undefined;
+
+ /**
+ * The name of the user.
+ */
name?: string | undefined;
+
+ /**
+ * The email of the user.
+ */
email?: string | undefined;
+
+ /**
+ * The avatar URL of the user.
+ */
+ avatar?: string | undefined;
+
+ /**
+ * Custom attributes of the user.
+ */
[k: string]: any;
};
/**
* The company context. If no `id` key is set, the whole object is ignored.
*/
company?: {
+ /**
+ * The identifier of the company.
+ */
id: string | number | undefined;
+
+ /**
+ * The name of the company.
+ */
name?: string | undefined;
+
+ /**
+ * The avatar URL of the company.
+ */
+ avatar?: string | undefined;
+
+ /**
+ * Custom attributes of the company.
+ */
[k: string]: any;
};
+
/**
* The other context. This is used for any additional context that is not related to user or company.
*/
From 58f46d724a16237836be83b3b5d9a41bb3729d7b Mon Sep 17 00:00:00 2001
From: Rasmus Makwarth
Date: Tue, 28 Jan 2025 14:09:57 +0100
Subject: [PATCH 09/24] Bump year (#281)
Co-authored-by: Alexandru Ciobanu
---
packages/browser-sdk/README.md | 2 +-
packages/node-sdk/README.md | 2 +-
packages/openfeature-browser-provider/README.md | 2 +-
packages/openfeature-node-provider/README.md | 2 +-
packages/react-sdk/README.md | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/packages/browser-sdk/README.md b/packages/browser-sdk/README.md
index f76ce5e2..63e88c60 100644
--- a/packages/browser-sdk/README.md
+++ b/packages/browser-sdk/README.md
@@ -275,4 +275,4 @@ If you are including the Bucket tracking SDK with a `
+
+ Click me
+
+ Give feedback!
+
+
+
+
+
+