From 90135e7d09ab9b1ac90cb05d862c32626e2993e5 Mon Sep 17 00:00:00 2001 From: Pawel Date: Mon, 26 Aug 2024 18:20:54 +0200 Subject: [PATCH] feat(TU-13163): Add the duplicate-detected callback (#660) * feat(TU-13163): Add the duplicate-detected callback * feat(TU-13163): Update developer portal onDuplicateDetected docs * feat(TU-13163): Update developer portal onDuplicateDetected docs * feat(TU-13163): Add duplicate prevention callback example --- docs/callbacks.md | 32 ++++++++++++++ docs/configuration.md | 1 + .../callbacks-duplicate-prevention.html | 42 +++++++++++++++++++ packages/embed/README.md | 6 +++ packages/embed/src/base/actionable-options.ts | 7 ++++ .../build-options-from-attributes.spec.ts | 3 ++ .../build-options-from-attributes.ts | 1 + .../src/utils/create-iframe/create-iframe.ts | 3 ++ .../create-iframe/get-form-event-handler.ts | 4 ++ 9 files changed, 99 insertions(+) create mode 100644 packages/demo-html/public/callbacks-duplicate-prevention.html diff --git a/docs/callbacks.md b/docs/callbacks.md index 03d3ac82..64a2a610 100644 --- a/docs/callbacks.md +++ b/docs/callbacks.md @@ -17,6 +17,7 @@ Available callbacks: - **onQuestionChanged** fires when user navigates between form questions - **onHeightChanged** fires when height of currently displayed question changes - **onEndingButtonClick** fires when user clicks on the button on your typeform ending screen (it also disables the redirect functionality) +- **onDuplicateDetected** fires when the respondent reaches the quota of responses defined in [the duplicate prevention setting](https://www.typeform.com/help/a/prevent-duplicate-responses-27917825492244/) Each callback receives a payload object with `formId` to identify the typeform that sent the event. Depending on the callback there might be more data in the payload - see examples below. @@ -246,6 +247,37 @@ Or in HTML: ``` +## onDuplicateDetected + +The `onDuplicateDetected` callback will execute whenever we detect the respondent reached the quota of responses +defined in [the duplicate prevention setting](https://www.typeform.com/help/a/prevent-duplicate-responses-27917825492244/). + +In JavaScript: + +```javascript +import { createSlider } from '@typeform/embed' +import '@typeform/embed/build/css/slider.css' + +createSlider('', { + onDuplicateDetected: ({ formId }) => { + console.log(`Duplicate detected for form ${formId}`) + }, +}) +``` + +Or in HTML: + +```html + + + +``` + ## What's next? Learn more about [contributing](/embed/contribute), or see what other open-source developers have created on the [Community projects](/community/) page. diff --git a/docs/configuration.md b/docs/configuration.md index 7de455b8..2c82a852 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -67,6 +67,7 @@ If you embed via HTML, you need to pass optinos as attributes with `data-tf-` pr | onQuestionChanged | function | fires when user navigates between form questions | `undefined` | | onHeightChanged | function | fires when form question height changes (eg. on navigation between questions or on error message) | `undefined` | | onEndingButtonClick | function | fires when button on ending screen is clicked | `undefined` | +| onDuplicateDetected | function | fires when the respondent reaches the quota of responses defined in [the duplicate prevention setting](https://www.typeform.com/help/a/prevent-duplicate-responses-27917825492244/) | `undefined` | | autoResize | string / boolean | resize form to always fit the displayed question height, avoid scrollbars in the form (inline widget only), set min and max height separated by coma, eg. `"200,600"` | `false` | | shareGaInstance | string / boolean | shares Google Analytics instance of the host page with embedded typeform, you can provide your Google Analytics ID to specify which instance to share (if you have more than one in your page) | `false` | | inlineOnMobile | boolean | removes placeholder welcome screen in mobile and makes form show inline instead of fullscreen | `false` | diff --git a/packages/demo-html/public/callbacks-duplicate-prevention.html b/packages/demo-html/public/callbacks-duplicate-prevention.html new file mode 100644 index 00000000..fe08bc61 --- /dev/null +++ b/packages/demo-html/public/callbacks-duplicate-prevention.html @@ -0,0 +1,42 @@ + + + + + + Static HTML Demo + + + + +
I have z-index 10k
+
+ + + + diff --git a/packages/embed/README.md b/packages/embed/README.md index d3c19848..924e5dd8 100644 --- a/packages/embed/README.md +++ b/packages/embed/README.md @@ -159,6 +159,7 @@ Closing and opening a typeform in modal window will restart the progress from th | [onQuestionChanged](https://codesandbox.io/s/github/Typeform/embed-demo/tree/main/demo-html/callbacks) | function | fires when user navigates between form questions | `undefined` | | [onHeightChanged](https://codesandbox.io/s/github/Typeform/embed-demo/tree/main/demo-html/callbacks) | function | fires when form question height changes (eg. on navigation between questions or on error message) | `undefined` | | [onEndingButtonClick](https://codesandbox.io/s/github/Typeform/embed-demo/tree/main/demo-html/callbacks) | function | fires when button on ending screen is clicked, disables button redirect functionality | `undefined` | +| onDuplicateDetected | function | fires when the respondent reaches the quota of responses defined in [the duplicate prevention setting](https://www.typeform.com/help/a/prevent-duplicate-responses-27917825492244/) | `undefined` | | [autoResize](https://codesandbox.io/s/github/Typeform/embed-demo/tree/main/demo-html/widget-autoresize) | string / boolean | resize form to always fit the displayed question height, avoid scrollbars in the form (inline widget only), set min and max height separated by coma, eg. `"200,600"` | `false` | | [shareGaInstance](https://codesandbox.io/s/github/Typeform/embed-demo/tree/main/demo-html/widget-inline) | string / boolean | shares Google Analytics instance of the host page with embedded typeform, you can provide your Google Analytics ID to specify which instance to share (if you have more than one in your page) | `false` | | [inlineOnMobile](https://codesandbox.io/s/github/Typeform/embed-demo/tree/main/demo-html/widget-inline) | boolean | removes placeholder welcome screen in mobile and makes form show inline instead of fullscreen | `false` | @@ -255,6 +256,9 @@ You can listen to form events by providing callback methods: // for plans with "Redirect from ending screen" feature you also receive `ref`: console.log(`Ending button clicked in end screen ${ref}`) + }, + onDuplicateDetected: ({ formId }) => { + console.log(`User reached the quote of responses for form ${formId}`) } }) document.querySelector('#btn').click = () => { @@ -285,6 +289,8 @@ Callback method receive payload object from the form. Each payload contains form - onEndingButtonClick - `formId` (string) - `ref` (string) identifies the end screen (_Note:_ this is available for plans with "Redirect from ending screen" feature only.) +- onDuplicateDetected + - `formId` (string) See [callbacks example in demo package](../../packages/demo-html/public/callbacks.html). diff --git a/packages/embed/src/base/actionable-options.ts b/packages/embed/src/base/actionable-options.ts index 689cee06..f1c56e67 100644 --- a/packages/embed/src/base/actionable-options.ts +++ b/packages/embed/src/base/actionable-options.ts @@ -56,4 +56,11 @@ export type ActionableOptions = { * @param {string} event.ref - End screen ref string (for plans with "Redirect from ending screen" feature). */ onEndingButtonClick?: (event: WithFormId & Partial) => void + + /** + * Callback function that will be executed once we detect the current user reached the form answer quota. + * @param {Object} event - Event payload. + * @param {string} event.formId - Form ID string. + */ + onDuplicateDetected?: (event: WithFormId) => void } diff --git a/packages/embed/src/initializers/build-options-from-attributes.spec.ts b/packages/embed/src/initializers/build-options-from-attributes.spec.ts index c6de0231..f786084a 100644 --- a/packages/embed/src/initializers/build-options-from-attributes.spec.ts +++ b/packages/embed/src/initializers/build-options-from-attributes.spec.ts @@ -17,6 +17,7 @@ describe('build-options-from-attributes', () => { data-tf-on-submit="onTypeformSubmit" data-tf-on-question-changed="onTypeformQuestionChanged" data-tf-on-height-changed="onTypeformHeightChanged" + data-tf-on-duplicate-detected="onDuplicateDetected" data-tf-auto-resize="100,300" data-tf-open="exit" data-tf-open-value="3000" @@ -45,6 +46,7 @@ describe('build-options-from-attributes', () => { win.onTypeformSubmit = jest.fn() win.onTypeformQuestionChanged = jest.fn() win.onTypeformHeightChanged = jest.fn() + win.onDuplicateDetected = jest.fn() const element = wrapper.querySelector('#element') as HTMLElement const options = buildOptionsFromAttributes(element) @@ -62,6 +64,7 @@ describe('build-options-from-attributes', () => { onSubmit: win.onTypeformSubmit, onQuestionChanged: win.onTypeformQuestionChanged, onHeightChanged: win.onTypeformHeightChanged, + onDuplicateDetected: win.onDuplicateDetected, autoResize: '100,300', open: 'exit', openValue: 3000, diff --git a/packages/embed/src/initializers/build-options-from-attributes.ts b/packages/embed/src/initializers/build-options-from-attributes.ts index 4d474e50..aaf9f097 100644 --- a/packages/embed/src/initializers/build-options-from-attributes.ts +++ b/packages/embed/src/initializers/build-options-from-attributes.ts @@ -18,6 +18,7 @@ export const buildOptionsFromAttributes = (element: HTMLElement) => { onSubmit: 'function', onQuestionChanged: 'function', onHeightChanged: 'function', + onDuplicateDetected: 'function', autoResize: 'stringOrBoolean', onClose: 'function', onEndingButtonClick: 'function', diff --git a/packages/embed/src/utils/create-iframe/create-iframe.ts b/packages/embed/src/utils/create-iframe/create-iframe.ts index dd92eaab..88efe44c 100644 --- a/packages/embed/src/utils/create-iframe/create-iframe.ts +++ b/packages/embed/src/utils/create-iframe/create-iframe.ts @@ -12,6 +12,7 @@ import { getThankYouScreenButtonClickHandler, getFormStartedHandler, getRedirectHandler, + getDuplicateDetectedHandler, } from './get-form-event-handler' import { triggerIframeRedraw } from './trigger-iframe-redraw' import { dispatchCustomKeyEventFromIframe } from './setup-custom-keyboard-close' @@ -35,6 +36,7 @@ export const createIframe = (type: EmbedType, { formId, domain, options }: Creat onHeightChanged, onSubmit, onEndingButtonClick, + onDuplicateDetected, shareGaInstance, } = options @@ -67,6 +69,7 @@ export const createIframe = (type: EmbedType, { formId, domain, options }: Creat window.addEventListener('message', getFormThemeHandler(embedId, onTheme)) window.addEventListener('message', getThankYouScreenButtonClickHandler(embedId, onEndingButtonClick)) window.addEventListener('message', getRedirectHandler(embedId, iframe)) + window.addEventListener('message', getDuplicateDetectedHandler(embedId, onDuplicateDetected)) if (type !== 'widget') { window.addEventListener('message', dispatchCustomKeyEventFromIframe) diff --git a/packages/embed/src/utils/create-iframe/get-form-event-handler.ts b/packages/embed/src/utils/create-iframe/get-form-event-handler.ts index b4c024eb..a4f08806 100644 --- a/packages/embed/src/utils/create-iframe/get-form-event-handler.ts +++ b/packages/embed/src/utils/create-iframe/get-form-event-handler.ts @@ -23,6 +23,10 @@ export const getFormSubmitHandler = (embedId: string, callback?: callbackFn) => return getFormEventHandler('form-submit', embedId, callback) } +export const getDuplicateDetectedHandler = (embedId: string, callback?: callbackFn) => { + return getFormEventHandler('duplicate-detected', embedId, callback) +} + export const getWelcomeScreenHiddenHandler = (embedId: string, callback?: callbackFn) => { return getFormEventHandler('welcome-screen-hidden', embedId, callback) }