From 26962c6cfdbe742069c0e0377efacd5dea7074d0 Mon Sep 17 00:00:00 2001 From: Erik Hughes Date: Wed, 26 Feb 2025 12:14:20 +0100 Subject: [PATCH 1/3] refactor(tracking-sdk): official deprecate --- .github/workflows/package-ci.yml | 3 - packages/browser-sdk/FEEDBACK.md | 8 +- packages/flag-evaluation/package.json | 2 +- packages/tracking-sdk/.prettierignore | 2 - packages/tracking-sdk/FEEDBACK.md | 396 -------- packages/tracking-sdk/README.md | 206 ---- packages/tracking-sdk/dev/app.tsx | 186 ---- packages/tracking-sdk/dev/index.css | 74 -- packages/tracking-sdk/dev/index.tsx | 10 - packages/tracking-sdk/eslint.config.js | 3 - packages/tracking-sdk/index.html | 14 - packages/tracking-sdk/package.json | 75 -- packages/tracking-sdk/playwright.config.ts | 36 - packages/tracking-sdk/postcss.config.js | 3 - packages/tracking-sdk/src/config.ts | 8 - .../src/default-feedback-prompt-handler.ts | 13 - packages/tracking-sdk/src/feedback/Button.css | 54 -- packages/tracking-sdk/src/feedback/Button.tsx | 19 - .../src/feedback/FeedbackDialog.css | 195 ---- .../src/feedback/FeedbackDialog.tsx | 268 ------ .../src/feedback/FeedbackForm.css | 165 ---- .../src/feedback/FeedbackForm.tsx | 282 ------ packages/tracking-sdk/src/feedback/Plug.tsx | 13 - .../src/feedback/RadialProgress.css | 6 - .../src/feedback/RadialProgress.tsx | 26 - .../tracking-sdk/src/feedback/StarRating.css | 69 -- .../tracking-sdk/src/feedback/StarRating.tsx | 180 ---- .../feedback/config/defaultTranslations.tsx | 16 - .../tracking-sdk/src/feedback/constants.ts | 65 -- .../src/feedback/hooks/useTimer.ts | 66 -- .../tracking-sdk/src/feedback/icons/Check.tsx | 18 - .../src/feedback/icons/CheckCircle.tsx | 18 - .../tracking-sdk/src/feedback/icons/Close.tsx | 19 - .../src/feedback/icons/Dissatisfied.tsx | 23 - .../tracking-sdk/src/feedback/icons/Logo.tsx | 27 - .../src/feedback/icons/Neutral.tsx | 19 - .../src/feedback/icons/Satisfied.tsx | 23 - .../src/feedback/icons/VeryDissatisfied.tsx | 19 - .../src/feedback/icons/VerySatisfied.tsx | 19 - packages/tracking-sdk/src/feedback/index.css | 5 - packages/tracking-sdk/src/feedback/index.ts | 63 -- .../packages/floating-ui-preact-dom/README.md | 3 - .../packages/floating-ui-preact-dom/arrow.ts | 52 - .../packages/floating-ui-preact-dom/index.ts | 17 - .../packages/floating-ui-preact-dom/types.ts | 146 --- .../floating-ui-preact-dom/useFloating.ts | 197 ---- .../floating-ui-preact-dom/utils/deepEqual.ts | 58 -- .../floating-ui-preact-dom/utils/getDPR.ts | 7 - .../utils/roundByDPR.ts | 6 - .../utils/useLatestRef.ts | 9 - packages/tracking-sdk/src/feedback/types.ts | 84 -- packages/tracking-sdk/src/flags-cache.ts | 153 --- packages/tracking-sdk/src/flags-fetch.ts | 199 ---- packages/tracking-sdk/src/flags.ts | 13 - packages/tracking-sdk/src/index.ts | 16 - packages/tracking-sdk/src/main.ts | 644 ------------- packages/tracking-sdk/src/prompt-storage.ts | 61 -- packages/tracking-sdk/src/prompts.ts | 64 -- packages/tracking-sdk/src/sse.ts | 318 ------- packages/tracking-sdk/src/types.ts | 198 ---- packages/tracking-sdk/src/vite-env.d.ts | 1 - .../test/e2e/acceptance.browser.spec.ts | 102 -- .../test/e2e/acceptance.node.test.ts | 64 -- .../test/e2e/feedback-widget.browser.spec.ts | 387 -------- .../tracking-sdk/test/flags-cache.test.ts | 98 -- packages/tracking-sdk/test/flags.test.ts | 302 ------ packages/tracking-sdk/test/init.test.ts | 79 -- .../tracking-sdk/test/prompt-storage.test.ts | 236 ----- packages/tracking-sdk/test/prompts.test.ts | 203 ---- packages/tracking-sdk/test/singleton.test.ts | 9 - packages/tracking-sdk/test/sse.test.ts | 779 --------------- packages/tracking-sdk/test/usage.test.ts | 898 ------------------ packages/tracking-sdk/tsconfig.build.json | 4 - packages/tracking-sdk/tsconfig.eslint.json | 4 - packages/tracking-sdk/tsconfig.json | 13 - packages/tracking-sdk/vite.config.js | 15 - packages/tracking-sdk/vite.e2e.config.js | 7 - packages/tracking-sdk/webpack.config.ts | 92 -- 78 files changed, 2 insertions(+), 8252 deletions(-) delete mode 100644 packages/tracking-sdk/.prettierignore delete mode 100644 packages/tracking-sdk/FEEDBACK.md delete mode 100644 packages/tracking-sdk/README.md delete mode 100644 packages/tracking-sdk/dev/app.tsx delete mode 100644 packages/tracking-sdk/dev/index.css delete mode 100644 packages/tracking-sdk/dev/index.tsx delete mode 100644 packages/tracking-sdk/eslint.config.js delete mode 100644 packages/tracking-sdk/index.html delete mode 100644 packages/tracking-sdk/package.json delete mode 100644 packages/tracking-sdk/playwright.config.ts delete mode 100644 packages/tracking-sdk/postcss.config.js delete mode 100644 packages/tracking-sdk/src/config.ts delete mode 100644 packages/tracking-sdk/src/default-feedback-prompt-handler.ts delete mode 100644 packages/tracking-sdk/src/feedback/Button.css delete mode 100644 packages/tracking-sdk/src/feedback/Button.tsx delete mode 100644 packages/tracking-sdk/src/feedback/FeedbackDialog.css delete mode 100644 packages/tracking-sdk/src/feedback/FeedbackDialog.tsx delete mode 100644 packages/tracking-sdk/src/feedback/FeedbackForm.css delete mode 100644 packages/tracking-sdk/src/feedback/FeedbackForm.tsx delete mode 100644 packages/tracking-sdk/src/feedback/Plug.tsx delete mode 100644 packages/tracking-sdk/src/feedback/RadialProgress.css delete mode 100644 packages/tracking-sdk/src/feedback/RadialProgress.tsx delete mode 100644 packages/tracking-sdk/src/feedback/StarRating.css delete mode 100644 packages/tracking-sdk/src/feedback/StarRating.tsx delete mode 100644 packages/tracking-sdk/src/feedback/config/defaultTranslations.tsx delete mode 100644 packages/tracking-sdk/src/feedback/constants.ts delete mode 100644 packages/tracking-sdk/src/feedback/hooks/useTimer.ts delete mode 100644 packages/tracking-sdk/src/feedback/icons/Check.tsx delete mode 100644 packages/tracking-sdk/src/feedback/icons/CheckCircle.tsx delete mode 100644 packages/tracking-sdk/src/feedback/icons/Close.tsx delete mode 100644 packages/tracking-sdk/src/feedback/icons/Dissatisfied.tsx delete mode 100644 packages/tracking-sdk/src/feedback/icons/Logo.tsx delete mode 100644 packages/tracking-sdk/src/feedback/icons/Neutral.tsx delete mode 100644 packages/tracking-sdk/src/feedback/icons/Satisfied.tsx delete mode 100644 packages/tracking-sdk/src/feedback/icons/VeryDissatisfied.tsx delete mode 100644 packages/tracking-sdk/src/feedback/icons/VerySatisfied.tsx delete mode 100644 packages/tracking-sdk/src/feedback/index.css delete mode 100644 packages/tracking-sdk/src/feedback/index.ts delete mode 100644 packages/tracking-sdk/src/feedback/packages/floating-ui-preact-dom/README.md delete mode 100644 packages/tracking-sdk/src/feedback/packages/floating-ui-preact-dom/arrow.ts delete mode 100644 packages/tracking-sdk/src/feedback/packages/floating-ui-preact-dom/index.ts delete mode 100644 packages/tracking-sdk/src/feedback/packages/floating-ui-preact-dom/types.ts delete mode 100644 packages/tracking-sdk/src/feedback/packages/floating-ui-preact-dom/useFloating.ts delete mode 100644 packages/tracking-sdk/src/feedback/packages/floating-ui-preact-dom/utils/deepEqual.ts delete mode 100644 packages/tracking-sdk/src/feedback/packages/floating-ui-preact-dom/utils/getDPR.ts delete mode 100644 packages/tracking-sdk/src/feedback/packages/floating-ui-preact-dom/utils/roundByDPR.ts delete mode 100644 packages/tracking-sdk/src/feedback/packages/floating-ui-preact-dom/utils/useLatestRef.ts delete mode 100644 packages/tracking-sdk/src/feedback/types.ts delete mode 100644 packages/tracking-sdk/src/flags-cache.ts delete mode 100644 packages/tracking-sdk/src/flags-fetch.ts delete mode 100644 packages/tracking-sdk/src/flags.ts delete mode 100644 packages/tracking-sdk/src/index.ts delete mode 100644 packages/tracking-sdk/src/main.ts delete mode 100644 packages/tracking-sdk/src/prompt-storage.ts delete mode 100644 packages/tracking-sdk/src/prompts.ts delete mode 100644 packages/tracking-sdk/src/sse.ts delete mode 100644 packages/tracking-sdk/src/types.ts delete mode 100644 packages/tracking-sdk/src/vite-env.d.ts delete mode 100644 packages/tracking-sdk/test/e2e/acceptance.browser.spec.ts delete mode 100644 packages/tracking-sdk/test/e2e/acceptance.node.test.ts delete mode 100644 packages/tracking-sdk/test/e2e/feedback-widget.browser.spec.ts delete mode 100644 packages/tracking-sdk/test/flags-cache.test.ts delete mode 100644 packages/tracking-sdk/test/flags.test.ts delete mode 100644 packages/tracking-sdk/test/init.test.ts delete mode 100644 packages/tracking-sdk/test/prompt-storage.test.ts delete mode 100644 packages/tracking-sdk/test/prompts.test.ts delete mode 100644 packages/tracking-sdk/test/singleton.test.ts delete mode 100644 packages/tracking-sdk/test/sse.test.ts delete mode 100644 packages/tracking-sdk/test/usage.test.ts delete mode 100644 packages/tracking-sdk/tsconfig.build.json delete mode 100644 packages/tracking-sdk/tsconfig.eslint.json delete mode 100644 packages/tracking-sdk/tsconfig.json delete mode 100644 packages/tracking-sdk/vite.config.js delete mode 100644 packages/tracking-sdk/vite.e2e.config.js delete mode 100644 packages/tracking-sdk/webpack.config.ts diff --git a/.github/workflows/package-ci.yml b/.github/workflows/package-ci.yml index f753fcac..e8a49da4 100644 --- a/.github/workflows/package-ci.yml +++ b/.github/workflows/package-ci.yml @@ -25,9 +25,6 @@ jobs: - name: Restore package.json # This step is necessary because the previous step may have updated package.json run: git checkout -- package.json packages/*/package.json - - name: Install Playwright Browsers - run: yarn playwright install --with-deps - working-directory: ./packages/tracking-sdk - name: Install Playwright Browsers run: yarn playwright install --with-deps working-directory: ./packages/browser-sdk diff --git a/packages/browser-sdk/FEEDBACK.md b/packages/browser-sdk/FEEDBACK.md index f93d2626..6dcdc80f 100644 --- a/packages/browser-sdk/FEEDBACK.md +++ b/packages/browser-sdk/FEEDBACK.md @@ -351,13 +351,7 @@ properties to your page in your CSS `:root`-scope. For example, a dark mode theme might look like this: -```html -image -``` +![image](https://github.com/bucketco/bucket-tracking-sdk/assets/34348/5d579b7b-a830-4530-8b40-864488a8597e) ```css :root { diff --git a/packages/flag-evaluation/package.json b/packages/flag-evaluation/package.json index 7f95cc80..c6881a92 100644 --- a/packages/flag-evaluation/package.json +++ b/packages/flag-evaluation/package.json @@ -4,7 +4,7 @@ "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/bucketco/bucket-tracking-sdk.git" + "url": "https://github.com/bucketco/bucket-javascript-sdk.git" }, "publishConfig": { "access": "public" diff --git a/packages/tracking-sdk/.prettierignore b/packages/tracking-sdk/.prettierignore deleted file mode 100644 index adc0c14a..00000000 --- a/packages/tracking-sdk/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -eslint-report.json diff --git a/packages/tracking-sdk/FEEDBACK.md b/packages/tracking-sdk/FEEDBACK.md deleted file mode 100644 index fbd81245..00000000 --- a/packages/tracking-sdk/FEEDBACK.md +++ /dev/null @@ -1,396 +0,0 @@ -# Bucket Feedback UI - -The Bucket SDK includes a UI you can use to collect feedback from user about particular features. - -![image](https://github.com/bucketco/bucket-tracking-sdk/assets/34348/c387bac1-f2e2-4efd-9dda-5030d76f9532) - -## Global feedback configuration - -The Bucket SDK feedback UI is configured with reasonable defaults, positioning itself as a [dialog](#dialog) in the lower right-hand corner of the viewport, displayed in english, and with a [light-mode theme](#custom-styling). - -These settings can be overwritten when initializing the Bucket SDK: - -```javascript -bucket.init("bucket-publishable-key", { - feedback: { - ui: { - position: POSITION_CONFIG, // See positioning section - translations: TRANSLATION_KEYS, // See internationalization section - - // Enable Live Satisfaction. Default: `true` - enableLiveSatisfaction: boolean, - - /** - * Do your own feedback prompt handling or override - * default settings at runtime. - */ - liveSatisfactionHandler: (promptMessage, handlers) => { - // See Live Satisfaction section - }, - }, - }, -}); -``` - -See also: - -- [Positioning and behavior](#positioning-and-behavior) for the position option. -- [Static language configuration](#static-language-configuration) if you want to translate the feedback UI. -- [Live Satisfaction](#live-satisfaction) to override default configuration. - -## Live Satisfaction - -Live Satisfaction is enabled by default. - -When Live Satisfaction is enabled, the Bucket SDK will open and maintain a connection to the Bucket service. When a user triggers an event tracked by a feature and is eligible to be prompted for feedback, the Bucket service will send a request to the SDK instance. By default, this request will open up the Bucket feedback UI in the user's browser, but you can intercept the request and override this behaviour. - -The live connection for automated feedback is established once you have identified a user with `bucket.user()`. - -### Disabling Live Satisfaction - -You can disable automated collection in the `bucket.init()`-call: - -```javascript -bucket.init("bucket-publishable-key", { - feedback: { - enableLiveSatisfaction: false, - }, -}); -``` - -### Overriding prompt event defaults - -If you are not satisfied with the default UI behavior when an automated prompt event arrives, you can can [override the global defaults](#global-feedback-configuration) or intercept and override settings at runtime like this: - -```javascript -bucket.init("bucket-publishable-key", { - feedback: { - liveSatisfactionHandler: (promptMessage, handlers) => { - // Pass your overrides here. Everything is optional - handlers.openFeedbackForm({ - title: promptMessage.question, - - position: POSITION_CONFIG, // See positioning section - translations: TRANSLATION_KEYS, // See internationalization section - - // Trigger side effects with the collected data, - // for example posting it back into your own CRM - onAfterSubmit: (feedback) => { - storeFeedbackInCRM({ - score: feedback.score, - comment: feedback.comment, - }); - }, - }); - }, - }, -}); -``` - -See also: - -- [Positioning and behavior](#positioning-and-behavior) for the position option. -- [Runtime language configuration](#runtime-language-configuration) if you want to translate the feedback UI. -- [Use your own UI to collect feedback](#using-your-own-ui-to-collect-feedback) if the feedback UI doesn't match your design. - -## Manual feedback collection - -To open up the feedback collection UI, call `bucket.requestFeedback(options)` with the appropriate options. This approach is particularly beneficial if you wish to retain manual control over feedback collection from your users while leveraging the convenience of the Bucket feedback UI to reduce the amount of code you need to maintain. - -Examples of this could be if you want the click of a `give us feedback`-button or the end of a specific user flow, to trigger a pop-up displaying the feedback user interface. - -### bucket.requestFeedback() options - -Minimal usage with defaults: - -```javascript -bucket.requestFeedback({ - featureId: "bucket-feature-id", - title: "How satisfied are you with file uploads?", -}); -``` - -All options: - -```javascript -bucket.requestFeedback({ - featureId: "bucket-feature-id", // [Required] - userId: "your-user-id", // [Optional] if user persistence is enabled (default in browsers), - companyId: "users-company-or-account-id", // [Optional] - title: "How satisfied are you with file uploads?" // [Optional] - - position: POSITION_CONFIG, // [Optional] see the positioning section - translations: TRANSLATION_KEYS // [Optional] see the internationalization section - - // [Optional] trigger side effects with the collected data, - // for example sending the feedback to your own CRM - onAfterSubmit: (feedback) => { - storeFeedbackInCRM({ - score: feedback.score, - comment: feedback.comment - }) - } -}) -``` - -See also: - -- [Positioning and behavior](#positioning-and-behavior) for the position option. -- [Runtime language configuration](#runtime-language-configuration) if you want to translate the feedback UI. - -## Positioning and behavior - -The feedback UI can be configured to be placed and behave in 3 different ways: - -### Positioning configuration - -#### Modal - -A modal overlay with a backdrop that blocks interaction with the underlying page. It can be dismissed with the keyboard shortcut `` or the dedicated close button in the top right corner. It is always centered on the page, capturing focus, and making it the primary interface the user needs to interact with. - -![image](https://github.com/bucketco/bucket-tracking-sdk/assets/331790/6c6efbd3-cf7d-4d5b-b126-7ac978b2e512) - -Using a modal is the strongest possible push for feedback. You are interrupting the user's normal flow, which can cause annoyance. A good use-case for the modal is when the user finishes a linear flow that they don't perform often, for example setting up a new account. - -```javascript -position: { - type: "MODAL"; -} -``` - -#### Dialog - -A dialog that appears in a specified corner of the viewport, without limiting the user's interaction with the rest of the page. It can be dismissed with the dedicated close button, but will automatically disappear after a short time period if the user does not interact with it. - -![image](https://github.com/bucketco/bucket-tracking-sdk/assets/331790/30413513-fd5f-4a2c-852a-9b074fa4666c) - -Using a dialog is a soft push for feedback. It lets the user continue their work with a minimal amount of intrusion. The user can opt-in to respond but is not required to. A good use case for this behaviour is when a user uses a feature where the expected outcome is predictable, possibly because they have used it multiple times before. For example: Uploading a file, switching to a different view of a visualisation, visiting a specific page, or manipulating some data. - -The default feedback UI behaviour is a dialog placed in the bottom right corner of the viewport. - -```typescript -position: { - type: "DIALOG", - placement: "top-left" | "top-right" | "bottom-left" | "bottom-right" - offset?: { - x?: string | number; // e.g. "-5rem", "10px" or 10 (pixels) - y?: string | number; - } -} -``` - -#### Popover - -A popover that is anchored relative to a DOM-element (typically a button). It can be dismissed by clicking outside the popover or by pressing the dedicated close button. - -![image](https://github.com/bucketco/bucket-tracking-sdk/assets/331790/4c5c5597-9ed3-4d4d-90c0-950926d0d967) - -You can use the popover mode to implement your own button to collect feedback manually. - -```javascript -position: { - type: "POPOVER", - anchor: DOMElement -} -``` - -Popover feedback button example: - -```html - - -``` - -## Internationalization (i18n) - -By default, the feedback UI is written in English. However, you can supply your own translations by passing an object to the options to either or both of the `bucket.init(options)` or `bucket.requestFeedback(options)` calls. These translations will replace the English ones used by the feedback interface. See examples below. - -![image](https://github.com/bucketco/bucket-tracking-sdk/assets/331790/68805b38-e9f6-4de5-9f55-188216983e3c) - -See [default english localization keys](./src/feedback/config/defaultTranslations.tsx) for a reference of what translation keys can be supplied. - -### Static language configuration - -If you know the language at page load, you can configure your translation keys while initializing the Bucket SDK: - -```javascript -bucket.init("my-publishable-key", { - feedback: { - ui: { - translations: { - DefaultQuestionLabel: - "Dans quelle mesure êtes-vous satisfait de cette fonctionnalité ?", - QuestionPlaceholder: - "Comment pouvons-nous améliorer cette fonctionnalité ?", - ScoreStatusDescription: "Choisissez une note et laissez un commentaire", - ScoreStatusLoading: "Chargement...", - ScoreStatusReceived: "La note a été reçue !", - ScoreVeryDissatisfiedLabel: "Très insatisfait", - ScoreDissatisfiedLabel: "Insatisfait", - ScoreNeutralLabel: "Neutre", - ScoreSatisfiedLabel: "Satisfait", - ScoreVerySatisfiedLabel: "Très satisfait", - SuccessMessage: "Merci d'avoir envoyé vos commentaires!", - SendButton: "Envoyer", - }, - }, - }, -}); -``` - -### Runtime language configuration - -If you only know the user's language after the page has loaded, you can provide translations to either the `bucket.requestFeedback(options)` call or the `liveSatisfactionHandler` option before the feedback interface opens. See examples below. - -### Manual feedback collection - -```javascript -bucket.requestFeedback({ - ... // Other options - translations: { - // your translation keys - } -}) -``` - -### Live Satisfaction - -When you are collecting feedback through the Bucket automation, you can intercept the default prompt handling and override the defaults. - -If you set the prompt question in the Bucket app to be one of your own translation keys, you can even get a translated version of the question you want to ask your customer in the feedback UI. - -```javascript -bucket.init("bucket-publishable-key", { - feedback: { - liveSatisfactionHandler: (message, handlers) => { - const translatedQuestion = - i18nLookup[message.question] ?? message.question; - handlers.openFeedbackForm({ - title: translatedQuestion, - translations: { - // your static translation keys - }, - }); - }, - }, -}); -``` - -## Custom styling - -You can adapt parts of the look of the Bucket feedback UI by applying CSS custom properties to your page in your CSS `:root`-scope. - -For example, a dark mode theme might look like this: - -image - -```css -:root { - --bucket-feedback-dialog-background-color: #1e1f24; - --bucket-feedback-dialog-color: rgba(255, 255, 255, 0.92); - --bucket-feedback-dialog-secondary-color: rgba(255, 255, 255, 0.3); - --bucket-feedback-dialog-border: rgba(255, 255, 255, 0.16); - --bucket-feedback-dialog-primary-button-background-color: #655bfa; - --bucket-feedback-dialog-primary-button-color: white; - --bucket-feedback-dialog-input-border-color: rgba(255, 255, 255, 0.16); - --bucket-feedback-dialog-input-focus-border-color: rgba(255, 255, 255, 0.3); - --bucket-feedback-dialog-error-color: #f56565; - - --bucket-feedback-dialog-rating-1-color: #ed8936; - --bucket-feedback-dialog-rating-1-background-color: #7b341e; - --bucket-feedback-dialog-rating-2-color: #dd6b20; - --bucket-feedback-dialog-rating-2-background-color: #652b19; - --bucket-feedback-dialog-rating-3-color: #787c91; - --bucket-feedback-dialog-rating-3-background-color: #3e404c; - --bucket-feedback-dialog-rating-4-color: #38a169; - --bucket-feedback-dialog-rating-4-background-color: #1c4532; - --bucket-feedback-dialog-rating-5-color: #48bb78; - --bucket-feedback-dialog-rating-5-background-color: #22543d; - - --bucket-feedback-dialog-submitted-check-background-color: #38a169; - --bucket-feedback-dialog-submitted-check-color: #ffffff; -} -``` - -Other examples of custom styling can be found in our [development example stylesheet](./dev/index.css). - -## Using your own UI to collect feedback - -You may have very strict design guidelines for your app and maybe the Bucket feedback UI doesn't quite work for you. - -In this case, you can implement your own feedback collection mechanism, which follows your own design guidelines. - -This is the data type you need to collect: - -```typescript -{ - /** Customer satisfaction score */ - score?: 1 | 2 | 3 | 4 | 5, - comment?: string -} -``` - -Either `score` or `comment` must be defined in order to pass validation in the Bucket API. - -### Manual feedback collection - -Examples of a HTML-form that collects the relevant data can be found in [feedback.html](./example/feedback/feedback.html) and [feedback.jsx](./example/feedback/feedback.jsx). - -Once you have collected the feedback data, pass it along to `bucket.feedback()`: - -```javascript -bucket.feedback({ - featureId: "bucket-feature-id", - userId: "your-user-id", - score: 5, - comment: "Best thing I"ve ever tried!", -}); -``` - -### Intercepting Live Satisfaction events - -When using Live Satisfaction, the Bucket service will, when specified, send a feedback prompt message to your user's instance of the Bucket SDK. This will result in the feedback UI being opened. - -You can intercept this behavior and open your own custom feedback collection form: - -```javascript -bucket.init("bucket-publishable-key", { - feedback: { - liveSatisfactionHandler: async (promptMessage, handlers) => { - // This opens your custom UI - customFeedbackCollection({ - // The question configured in the Bucket UI for the feature - question: promptMessage.question, - // When the user successfully submits feedback data. - // Use this instead of `bucket.feedback()`, otherwise - // the feedback prompt handler will keep being called - // with the same prompt message - onFeedbackSubmitted: (feedback) => { - handlers.reply(feedback); - }, - // When the user closes the custom feedback form - // without leaving any response. - // It is important to feed this back, otherwise - // the feedback prompt handler will keep being called - // with the same prompt message - onFeedbackDismissed: () => { - handlers.reply(null); - }, - }); - }, - }, -}); -``` diff --git a/packages/tracking-sdk/README.md b/packages/tracking-sdk/README.md deleted file mode 100644 index 4038485e..00000000 --- a/packages/tracking-sdk/README.md +++ /dev/null @@ -1,206 +0,0 @@ -# Bucket Tracking SDK - -Isomorphic JS/TS tracking agent for [Bucket.co](https://bucket.co) - -## Install - -The library can be included directly as an external script or you can import it. - -A. Script tag (client-side directly in html) - -```html - -``` - -B. Import module (in either node or browser bundling) - -```js -import bucket from "@bucketco/tracking-sdk"; -// or -var bucket = require("@bucketco/tracking-sdk"); -``` - -Other languages than Javascript/Typescript are currently not supported by an SDK. You can [use the HTTP API directly](https://docs.bucket.co/reference/http-tracking-api) - -## Basic usage - -```js -// init the script with your publishable key -bucket.init("tk123", {}); - -// set current user -bucket.user("john_doe", { name: "John Doe" }); - -// set current company -bucket.company("acme_inc", { name: "Acme Inc", plan: "pro" }, "john_doe"); - -// track events -bucket.track("sent_message", { foo: "bar" }, "john_doe", "company_id"); -``` - -**NOTE**: When used in the browser, you can omit the 3rd argument (userId) to the `company` and `track` methods. See [persisting users](#persisting-users) for more details. - -### Init options - -Supply these to the `init` call (2nd argument) - -```ts -{ - debug?: false, // enable debug mode to log all info and errors - persistUser?: true | false // default value depends on environment, see below under "Persisting users" - host?: "https://tracking.bucket.co", - sseHost?: "https://livemessaging.bucket.co" -} -``` - -### Qualitative feedback - -Bucket can collect qualitative feedback from your users in the form of a [Customer Satisfaction Score](https://en.wikipedia.org/wiki/Customer_satisfaction) and a comment. - -#### Live Satisfaction collection - -The Bucket SDK comes with a Live Satisfaction collection mode enabled by default, which lets the Bucket service ask your users for feedback for relevant features just after they've used them. - -Note: To get started with automatic feedback collection, make sure you call `bucket.user()`. - -Live Satisfaction works even if you're not using the SDK to send events to Bucket. -It works because the Bucket SDK maintains a live connection to Bucket's servers and can show a Live Satisfaction prompt whenever the Bucket servers determines that an event should trigger a prompt - regardless of how this event is sent to Bucket. - -You can find all the options to make changes to the default behaviour in the [Bucket feedback documentation](./FEEDBACK.md). - -#### Bucket feedback UI - -Bucket can assist you with collecting your user's feedback by offering a pre-built UI, allowing you to get started with minimal code and effort. - -![image](https://github.com/bucketco/bucket-tracking-sdk/assets/34348/c387bac1-f2e2-4efd-9dda-5030d76f9532) - -[Read the Bucket feedback UI documentation](./FEEDBACK.md) - -#### Bucket feedback SDK - -Feedback can be submitted to Bucket using the SDK: - -```js -bucket.feedback({ - featureId: "my_feature_id", // String (required), copy from Feature feedback tab - userId: "john_doe", // String, optional if using user persistence - companyId: "acme_inc", // String (optional) - score: 5, // Number: 1-5 (optional) - comment: "Absolutely stellar work!", // String (optional) -}); -``` - -#### Bucket feedback API - -If you are not using the Bucket SDK, you can still submit feedback using the HTTP API. - -See details in [Feedback HTTP API](https://docs.bucket.co/reference/http-tracking-api#feedback) - -### Feature Flags - -Bucket can determine which feature flags are active for a given context. - -The context should take the form of `{ user: { id }, company: { id } }` plus anything additional you want to be able to evaluate flags against. In the browser, if a `user` call has been made the `user.id` will be used automatically and merged with anything provided in the `context` argument. - -```ts -const flags = await bucket.getFeatureFlags({ - context: { - user: { id: "user_123" }, - company: { id: "company_123" }, - }, -}); -// { -// "join-huddle": { -// "key": "join-huddle", -// "value": true -// }, -// "post-message": { -// "key": "post-message", -// "value": true -// } -// } -``` - -### Zero PII - -The Bucket SDK doesn't collect any metadata and HTTP IP addresses are _not_ being stored. - -For tracking individual users, we recommend using something like database ID as userId, as it's unique and doesn't include any PII (personal identifiable information). If, however, you're using e.g. email address as userId, but prefer not to send any PII to Bucket, you can hash the sensitive data before sending it to Bucket: - -``` -import bucket from "@bucketco/tracking-sdk"; -import { sha256 } from 'crypto-hash'; - -bucket.user(await sha256("john_doe")); -``` - -### Use of cookies - -The Bucket SDK uses a couple of cookies to support Live Satisfaction. These cookies are not used for tracking purposes and thus should not need to appear in cookie consent forms. - -The two cookies are: - -- `bucket-prompt-${userId}`: store the last Live Satisfaction prompt message ID received to avoid repeating prompts -- `bucket-token-${userId}`: caching a token used to connect to Bucket's live messaging infrastructure that is used to deliver Live Satisfaction prompts in real time. - -### Custom attributes - -You can pass attributes as a object literal to the `user`, `company` and `track` methods (2nd argument). -Attributes cannot be nested (multiple levels) and must be either strings, integers or booleans. - -Built-in attributes: - -- `name` (display name for user/company) - -### Context - -You can supply additional `context` to `group`, `user` and `event` calls. - -#### context.active - -By default, sending `group`, `user` and `event` calls automatically update the given user/company "Last seen" property. -You can control if "Last seen" should be updated when the events are sent by setting `context.active=false` to avoid updating last seen. -This is often useful if you have a background job that goes through a set of companies just to update their attributes or similar - -```typescript -// set current company without updating last seen. -bucket.company("acme_inc", { name: "Acme Inc", plan: "pro" }, "john_doe", { - active: false, -}); -``` - -### Persisting users - -**Usage in the browser** (imported or script tag): -Once you call `user`, the userId will be persisted so you don't have to supply userId to each subsequent `company` and `track` calls. -This is practical for client-side usage where a session always is a single user. - -**Usage in node.js** -User persistence is disabled by default when imported in node.js to avoid that companies or events are tied to the wrong user by mistake. This is because your server is (usually) not in a single user context. -Instead, you should provide the userId to each call, as the 3rd argument to `company` and `track`. - -### Typescript - -Types are bundled together with the library and exposed automatically when importing through a package manager. - -## Content Security Policy (CSP) - -If you are running with strict Content Security Policies active on your website, you will need to enable these directives in order to use the SDK: - -| Directive | Values | Module | Reason | -| ----------- | ------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | -| connect-src | https://tracking.bucket.co | tracking | Used for all tracking methods: `bucket.user()`, `bucket.company()`, `bucket.track()` and `bucket.feedback()` | -| connect-src | https://livemessaging.bucket.co | live satisfaction | Server sent events from the Bucket Live Satisfaction service, which allows for automatically collecting feedback when a user used a feature. | -| style-src | 'unsafe-inline' | feedback UI | The feedback UI is styled with inline styles. Not having this directive results unstyled HTML elements. | - -If you are including the Bucket tracking SDK with a ` - - diff --git a/packages/tracking-sdk/package.json b/packages/tracking-sdk/package.json deleted file mode 100644 index dd5c6ff0..00000000 --- a/packages/tracking-sdk/package.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "name": "@bucketco/tracking-sdk", - "version": "2.4.0", - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/bucketco/bucket-javascript-sdk.git" - }, - "scripts": { - "dev": "vite", - "start": "vite", - "build": "tsc --project tsconfig.build.json && webpack", - "test": "vitest -c vite.config.js", - "test:e2e": "yarn build && vitest run -c vite.e2e.config.js && playwright test", - "test:ci": "vitest run -c vite.config.js --reporter default --reporter=junit --outputFile=junit.xml && vitest run -c vite.e2e.config.js && CI=true playwright test", - "coverage": "vitest run --coverage", - "lint": "eslint .", - "lint:ci": "eslint --output-file eslint-report.json --format json .", - "prettier": "prettier --check .", - "format": "yarn lint --fix && yarn prettier --write", - "preversion": "yarn lint && yarn prettier && yarn vitest run -c vite.config.js && yarn build" - }, - "files": [ - "dist" - ], - "publishConfig": { - "access": "public" - }, - "main": "./dist/bucket-tracking-sdk.node.js", - "browser": "./dist/bucket-tracking-sdk.browser.js", - "types": "./dist/types/src/index.d.ts", - "devDependencies": { - "@babel/core": "^7.23.7", - "@bucketco/eslint-config": "workspace:^", - "@bucketco/tsconfig": "workspace:^", - "@playwright/test": "^1.40.1", - "@preact/preset-vite": "^2.8.1", - "@types/js-cookie": "^3.0.6", - "@types/jsdom": "^21.1.6", - "@types/node": "^22.12.0", - "@types/webpack": "^5.28.5", - "@types/webpack-node-externals": "^3.0.4", - "@vitest/coverage-v8": "^1.1.3", - "c8": "^9.1.0", - "css-loader": "^6.9.0", - "eslint": "^8.57.0", - "flush-promises": "^1.0.2", - "globals": "^13.24.0", - "http-server": "^14.1.1", - "jsdom": "^23.2.0", - "nock": "^13.4.0", - "postcss": "^8.4.33", - "postcss-loader": "^7.3.4", - "postcss-nesting": "^12.0.2", - "postcss-preset-env": "^9.3.0", - "prettier": "^3.3.3", - "style-loader": "^3.3.4", - "ts-loader": "^9.5.1", - "ts-node": "^10.9.2", - "typescript": "^5.7.3", - "vite": "^5.0.13", - "vite-plugin-dts": "^3.7.0", - "vitest": "^1.1.3", - "webpack": "^5.89.0", - "webpack-cli": "^5.1.4", - "webpack-node-externals": "^3.0.0" - }, - "dependencies": { - "@floating-ui/dom": "^1.4.5", - "cross-fetch": "^4.0.0", - "is-bundling-for-browser-or-node": "^1.1.1", - "js-cookie": "^3.0.5", - "preact": "^10.16.0" - } -} diff --git a/packages/tracking-sdk/playwright.config.ts b/packages/tracking-sdk/playwright.config.ts deleted file mode 100644 index dc5049ac..00000000 --- a/packages/tracking-sdk/playwright.config.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { defineConfig, devices } from "@playwright/test"; - -/** - * See https://playwright.dev/docs/test-configuration. - */ -export default defineConfig({ - testDir: "./test/e2e", - testMatch: "**/*.spec.?(c|m)[jt]s?(x)", - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - reporter: "list", - - use: { - trace: "on-first-retry", - }, - - projects: [ - { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, - }, - { - name: "firefox", - use: { ...devices["Desktop Firefox"] }, - }, - { - name: "webkit", - use: { ...devices["Desktop Safari"] }, - }, - ], - - webServer: { - command: "npx http-server . -p 8000", - }, -}); diff --git a/packages/tracking-sdk/postcss.config.js b/packages/tracking-sdk/postcss.config.js deleted file mode 100644 index da558986..00000000 --- a/packages/tracking-sdk/postcss.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - plugins: [require("postcss-nesting"), require("postcss-preset-env")], -}; diff --git a/packages/tracking-sdk/src/config.ts b/packages/tracking-sdk/src/config.ts deleted file mode 100644 index 67a7ff02..00000000 --- a/packages/tracking-sdk/src/config.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { version } from "../package.json"; - -export const API_HOST = "https://tracking.bucket.co"; -export const SSE_REALTIME_HOST = "https://livemessaging.bucket.co"; - -export const SDK_VERSION_HEADER_NAME = "bucket-sdk-version"; - -export const SDK_VERSION = version; diff --git a/packages/tracking-sdk/src/default-feedback-prompt-handler.ts b/packages/tracking-sdk/src/default-feedback-prompt-handler.ts deleted file mode 100644 index 8bde3152..00000000 --- a/packages/tracking-sdk/src/default-feedback-prompt-handler.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - FeedbackPrompt, - FeedbackPromptHandler, - FeedbackPromptHandlerOpenFeedbackFormOptions, -} from "./types"; - -export const createDefaultFeedbackPromptHandler = ( - options: FeedbackPromptHandlerOpenFeedbackFormOptions = {}, -): FeedbackPromptHandler => { - return (_prompt: FeedbackPrompt, handlers) => { - handlers.openFeedbackForm(options); - }; -}; diff --git a/packages/tracking-sdk/src/feedback/Button.css b/packages/tracking-sdk/src/feedback/Button.css deleted file mode 100644 index acf690f3..00000000 --- a/packages/tracking-sdk/src/feedback/Button.css +++ /dev/null @@ -1,54 +0,0 @@ -.button { - appearance: none; - display: inline-flex; - justify-content: center; - align-items: center; - user-select: none; - position: relative; - white-space: nowrap; - height: 2rem; - padding-inline-start: 0.75rem; - padding-inline-end: 0.75rem; - gap: 0.5em; - justify-content: center; - border: none; - cursor: pointer; - font-family: var(--bucket-feedback-dialog-font-family); - font-size: 12px; - font-weight: 500; - box-shadow: - 0 1px 2px 0 rgba(0, 0, 0, 0.06), - 0 1px 1px 0 rgba(0, 0, 0, 0.01); - border-radius: var(--bucket-feedback-dialog-border-radius, 6px); - transition-duration: 200ms; - transition-property: background-color, border-color, color, opacity, - box-shadow, transform; - - &.primary { - background-color: var( - --bucket-feedback-dialog-primary-button-background-color, - white - ); - color: var(--bucket-feedback-dialog-primary-button-color, #1e1f24); - border: 1px solid - var(--bucket-feedback-dialog-primary-border-color, #d8d9df); - } - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - border-color: var(--bucket-feedback-dialog-primary-border-color, #d8d9df); - - transition-duration: 200ms; - transition-property: background-color, border-color, color, opacity, - box-shadow, transform; - } - - &:focus { - outline: none; - border-color: var( - --bucket-feedback-dialog-input-focus-border-color, - #787c91 - ); - } -} diff --git a/packages/tracking-sdk/src/feedback/Button.tsx b/packages/tracking-sdk/src/feedback/Button.tsx deleted file mode 100644 index 2aee413d..00000000 --- a/packages/tracking-sdk/src/feedback/Button.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { FunctionComponent, h } from "preact"; - -export type ButtonProps = h.JSX.HTMLAttributes & { - variant?: "primary" | "secondary"; -}; - -export const Button: FunctionComponent = ({ - variant = "primary", - children, - ...rest -}) => { - const classes = ["button", variant].join(" "); - - return ( - - ); -}; diff --git a/packages/tracking-sdk/src/feedback/FeedbackDialog.css b/packages/tracking-sdk/src/feedback/FeedbackDialog.css deleted file mode 100644 index 91a99ec7..00000000 --- a/packages/tracking-sdk/src/feedback/FeedbackDialog.css +++ /dev/null @@ -1,195 +0,0 @@ -@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; - padding: 16px 22px 10px; - font-size: var(--bucket-feedback-dialog-font-size, 1rem); - font-family: var( - --bucket-feedback-dialog-font-family, - InterVariable, - Inter, - system-ui, - Open Sans, - sans-serif - ); - color: var(--bucket-feedback-dialog-color, #1e1f24); - border: 1px solid; - border-color: var(--bucket-feedback-dialog-border, #d8d9df); - border-radius: var(--bucket-feedback-dialog-border-radius, 6px); - box-shadow: var( - --bucket-feedback-dialog-box-shadow, - 0 10px 15px -3px rgba(0, 0, 0, 0.1), - 0 4px 6px -2px rgba(0, 0, 0, 0.05) - ); - background-color: var(--bucket-feedback-dialog-background-color, #fff); - z-index: 2147410000; - - &:not(.modal) { - margin: unset; - top: unset; - right: unset; - left: unset; - bottom: unset; - } -} - -.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; - } - &.top { - box-shadow: var(--bucket-feedback-dialog-border, #d8d9df) 1px 1px 1px 0px; - } - &.left { - box-shadow: var(--bucket-feedback-dialog-border, #d8d9df) 1px -1px 1px 0px; - } - &.right { - box-shadow: var(--bucket-feedback-dialog-border, #d8d9df) -1px 1px 1px 0px; - } -} - -.close { - position: absolute; - top: 6px; - right: 6px; - - width: 28px; - height: 28px; - - padding: 0; - margin: 0; - background: none; - border: none; - - cursor: pointer; - - display: flex; - justify-content: center; - align-items: center; - - color: var(--bucket-feedback-dialog-color, #1e1f24); - - svg { - position: absolute; - } -} - -.plug { - font-size: 0.75em; - text-align: center; - margin-top: 7px; - width: 100%; -} - -.plug a { - opacity: 0.5; - color: var(--bucket-feedback-dialog-color, #1e1f24); - text-decoration: none; - - transition: opacity 200ms; -} - -.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/tracking-sdk/src/feedback/FeedbackDialog.tsx b/packages/tracking-sdk/src/feedback/FeedbackDialog.tsx deleted file mode 100644 index ef820b58..00000000 --- a/packages/tracking-sdk/src/feedback/FeedbackDialog.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import { Fragment, FunctionComponent, h } from "preact"; -import { useCallback, useEffect, useRef, useState } from "preact/hooks"; - -import { DEFAULT_TRANSLATIONS } from "./config/defaultTranslations"; -import { useTimer } from "./hooks/useTimer"; -import { Close } from "./icons/Close"; -import { - arrow, - autoUpdate, - 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" ->; - -const INACTIVE_DURATION_MS = 20 * 1000; -const SUCCESS_DURATION_MS = 3 * 1000; - -export const FeedbackDialog: FunctionComponent = ({ - key, - title = DEFAULT_TRANSLATIONS.DefaultQuestionLabel, - position, - translations = DEFAULT_TRANSLATIONS, - openWithCommentVisible = false, - onClose, - onDismiss, - 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: [ - 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" - >("idle"); - - const submit = useCallback( - async (data: Omit) => { - await onSubmit({ ...data, feedbackId }); - autoClose.startWithDuration(SUCCESS_DURATION_MS); - }, - [feedbackId, onSubmit], - ); - - const submitScore = useCallback( - async (data: Omit) => { - if (onScoreSubmit !== undefined) { - setScoreState("submitting"); - - const res = await onScoreSubmit({ ...data, feedbackId }); - setFeedbackId(res.feedbackId); - setScoreState("submitted"); - } - }, - [feedbackId, onSubmit], - ); - - 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]); - - return ( - <> - - - - - - - {anchor && ( -
- )} -
- - ); -}; - -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/tracking-sdk/src/feedback/FeedbackForm.css b/packages/tracking-sdk/src/feedback/FeedbackForm.css deleted file mode 100644 index 364e7635..00000000 --- a/packages/tracking-sdk/src/feedback/FeedbackForm.css +++ /dev/null @@ -1,165 +0,0 @@ -.container { - overflow-y: hidden; - transition: max-height 400ms cubic-bezier(0.65, 0, 0.35, 1); -} - -.form { - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 10px; - overflow-y: hidden; - max-height: 400px; - transition: opacity 400ms cubic-bezier(0.65, 0, 0.35, 1); -} - -.form-control { - display: flex; - flex-direction: column; - width: 100%; - gap: 8px; - border: none; - padding: 0; - margin: 0; - - font-size: 12px; - color: var(--bucket-feedback-dialog-secondary-color, #787c91); -} - -.form-expanded-content { - width: 100%; - display: flex; - flex-direction: column; - align-items: flex-start; - gap: 10px; - transition: opacity 400ms cubic-bezier(0.65, 0, 0.35, 1); - - opacity: 0; - position: absolute; - top: 0; - left: 0; -} - -.title { - color: var(--bucket-feedback-dialog-color, #1e1f24); - font-size: 15px; - font-weight: 400; - line-height: 115%; - text-wrap: balance; - max-width: calc(100% - 20px); - margin-bottom: 6px; - line-height: 1.3; -} - -.dimmed { - opacity: 0.5; -} - -.textarea { - background-color: transparent; - border: 1px solid; - border-color: var(--bucket-feedback-dialog-input-border-color, #d8d9df); - padding: 0.5rem 0.75rem; - border-radius: var(--bucket-feedback-dialog-border-radius, 6px); - transition: border-color 0.2s ease-in-out; - font-family: var( - --bucket-feedback-dialog-font-family, - InterVariable, - Inter, - system-ui, - Open Sans, - sans-serif - ); - line-height: 1.3; - resize: none; - - color: var(--bucket-feedback-dialog-color, #1e1f24); - font-size: 13px; - - &::placeholder { - color: var(--bucket-feedback-dialog-color, #1e1f24); - opacity: 0.36; - } - - &:focus { - outline: none; - border-color: var( - --bucket-feedback-dialog-input-focus-border-color, - #787c91 - ); - } -} - -.score-status-container { - position: relative; - padding-bottom: 6px; - height: 14px; - - > .score-status { - display: flex; - align-items: center; - - position: absolute; - top: 0; - left: 0; - - opacity: 0; - transition: opacity 200ms ease-in-out; - } -} - -.error { - margin: 0; - color: var(--bucket-feedback-dialog-error-color, #e53e3e); - font-size: 0.8125em; - font-weight: 500; -} - -.submitted { - display: flex; - flex-direction: column; - transition: opacity 400ms cubic-bezier(0.65, 0, 0.35, 1); - - position: absolute; - top: 0; - left: 0; - opacity: 0; - pointer-events: none; - width: calc(100% - 56px); - - padding: 0px 28px; - - .submitted-check { - background: var(--bucket-feedback-dialog-submitted-check-color, #fff); - color: var( - --bucket-feedback-dialog-submitted-check-background-color, - #38a169 - ); - height: 24px; - width: 24px; - display: block; - border-radius: 50%; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - - margin: 16px auto 8px; - } - - .text { - margin: auto auto 16px; - text-align: center; - color: var(--bucket-feedback-dialog-color, #1e1f24); - font-size: var(--bucket-feedback-dialog-font-size, 1rem); - font-weight: 400; - line-height: 130%; - - flex-grow: 1; - max-width: 160px; - } - - > .plug { - flex-grow: 0; - } -} diff --git a/packages/tracking-sdk/src/feedback/FeedbackForm.tsx b/packages/tracking-sdk/src/feedback/FeedbackForm.tsx deleted file mode 100644 index 949ce658..00000000 --- a/packages/tracking-sdk/src/feedback/FeedbackForm.tsx +++ /dev/null @@ -1,282 +0,0 @@ -import { FunctionComponent, h } from "preact"; -import { useCallback, useEffect, useRef, useState } from "preact/hooks"; - -import { Check } from "./icons/Check"; -import { CheckCircle } from "./icons/CheckCircle"; -import { Button } from "./Button"; -import { Plug } from "./Plug"; -import { StarRating } from "./StarRating"; -import { - FeedbackScoreSubmission, - FeedbackSubmission, - FeedbackTranslations, -} from "./types"; - -const ANIMATION_SPEED = 400; - -function getFeedbackDataFromForm(el: HTMLFormElement) { - const formData = new FormData(el); - return { - score: Number(formData.get("score")?.toString()), - comment: (formData.get("comment")?.toString() || "").trim(), - }; -} - -type FeedbackFormProps = { - t: FeedbackTranslations; - question: string; - scoreState: "idle" | "submitting" | "submitted"; - openWithCommentVisible: boolean; - onInteraction: () => void; - onSubmit: ( - data: Omit, - ) => Promise | void; - onScoreSubmit: ( - score: Omit, - ) => Promise | void; -}; - -export const FeedbackForm: FunctionComponent = ({ - question, - scoreState, - openWithCommentVisible, - onInteraction, - onSubmit, - onScoreSubmit, - t, -}) => { - const [hasRating, setHasRating] = useState(false); - const [status, setStatus] = useState<"idle" | "submitting" | "submitted">( - "idle", - ); - const [error, setError] = useState(); - const [showForm, setShowForm] = useState(true); - - const handleSubmit: h.JSX.GenericEventHandler = async ( - e, - ) => { - e.preventDefault(); - const data: FeedbackSubmission = { - ...getFeedbackDataFromForm(e.target as HTMLFormElement), - question, - }; - if (!data.score) return; - setError(""); - try { - setStatus("submitting"); - await onSubmit(data); - setStatus("submitted"); - } catch (err) { - setStatus("idle"); - if (err instanceof Error) { - setError(err.message); - } else if (typeof err === "string") { - setError(err); - } else { - setError("Couldn't submit feedback. Please try again."); - } - } - }; - - const containerRef = useRef(null); - const formRef = useRef(null); - const headerRef = useRef(null); - const expandedContentRef = useRef(null); - const submittedRef = useRef(null); - - const transitionToDefault = useCallback(() => { - if (containerRef.current === null) return; - if (headerRef.current === null) return; - if (expandedContentRef.current === null) return; - - containerRef.current.style.maxHeight = - headerRef.current.clientHeight + "px"; - - expandedContentRef.current.style.position = "absolute"; - expandedContentRef.current.style.opacity = "0"; - expandedContentRef.current.style.pointerEvents = "none"; - }, [containerRef, headerRef, expandedContentRef]); - - const transitionToExpanded = useCallback(() => { - if (containerRef.current === null) return; - if (headerRef.current === null) return; - if (expandedContentRef.current === null) return; - - containerRef.current.style.maxHeight = - headerRef.current.clientHeight + // Header height - expandedContentRef.current.clientHeight + // Comment + Button Height - 10 + // Gap height - "px"; - - expandedContentRef.current.style.position = "relative"; - expandedContentRef.current.style.opacity = "1"; - expandedContentRef.current.style.pointerEvents = "all"; - }, [containerRef, headerRef, expandedContentRef]); - - const transitionToSuccess = useCallback(() => { - if (containerRef.current === null) return; - if (formRef.current === null) return; - if (submittedRef.current === null) return; - - formRef.current.style.opacity = "0"; - formRef.current.style.pointerEvents = "none"; - containerRef.current.style.maxHeight = - submittedRef.current.clientHeight + "px"; - - // Fade in "submitted" step once container has resized - setTimeout(() => { - submittedRef.current!.style.position = "relative"; - submittedRef.current!.style.opacity = "1"; - submittedRef.current!.style.pointerEvents = "all"; - setShowForm(false); - }, ANIMATION_SPEED + 10); - }, [formRef, containerRef, submittedRef]); - - useEffect(() => { - if (status === "submitted") { - transitionToSuccess(); - } else if (openWithCommentVisible || hasRating) { - transitionToExpanded(); - } else { - transitionToDefault(); - } - }, [ - transitionToDefault, - transitionToExpanded, - transitionToSuccess, - openWithCommentVisible, - hasRating, - status, - ]); - - return ( -
- - {showForm && ( -
-
-
- {question} -
- { - setHasRating(true); - await onScoreSubmit({ - question, - score: Number(e.currentTarget.value), - }); - }} - /> - - -
- -
-
-