diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cf466a581..c7e4cc073c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ The types of changes are: - Support temporary credentials in AWS generate + scan features [#4607](https://github.com/ethyca/fides/pull/4603), [#4608](https://github.com/ethyca/fides/pull/4608) - Add ability to store and read Fides cookie in Base64 format [#4556](https://github.com/ethyca/fides/pull/4556) - Structured logging for SaaS connector requests [#4594](https://github.com/ethyca/fides/pull/4594) +- Added Fides.showModal() to fides.js to allow programmatic opening of consent modals [#4617](https://github.com/ethyca/fides/pull/4617) ### Fixed diff --git a/clients/fides-js/src/components/Overlay.tsx b/clients/fides-js/src/components/Overlay.tsx index b651c13a7f..f06999a409 100644 --- a/clients/fides-js/src/components/Overlay.tsx +++ b/clients/fides-js/src/components/Overlay.tsx @@ -1,5 +1,11 @@ import { h, FunctionComponent, VNode } from "preact"; -import { useEffect, useState, useCallback, useMemo } from "preact/hooks"; +import { + useEffect, + useState, + useCallback, + useMemo, + useRef, +} from "preact/hooks"; import { FidesCookie, FidesOptions, @@ -14,6 +20,7 @@ import ConsentModal from "./ConsentModal"; import { useHasMounted } from "../lib/hooks"; import { dispatchFidesEvent } from "../lib/events"; import ConsentContent from "./ConsentContent"; +import { defaultShowModal } from "../fides"; interface RenderBannerProps { isOpen: boolean; @@ -53,6 +60,7 @@ const Overlay: FunctionComponent = ({ const delayModalLinkMilliseconds = 200; const hasMounted = useHasMounted(); const [bannerIsOpen, setBannerIsOpen] = useState(false); + const modalLinkRef = useRef(null); const dispatchCloseEvent = useCallback( ({ saved = false }: { saved?: boolean }) => { @@ -75,6 +83,7 @@ const Overlay: FunctionComponent = ({ const handleOpenModal = useCallback(() => { if (instance) { + setBannerIsOpen(false); instance.show(); onOpen(); } @@ -101,6 +110,8 @@ const Overlay: FunctionComponent = ({ }, [setBannerIsOpen]); useEffect(() => { + window.Fides.showModal = handleOpenModal; + document.body.classList.add("fides-overlay-modal-link-shown"); // use a delay to ensure that link exists in the DOM const delayModalLinkBinding = setTimeout(() => { const modalLinkId = options.modalLinkId || "fides-modal-link"; @@ -110,19 +121,24 @@ const Overlay: FunctionComponent = ({ options.debug, "Modal link element found, updating it to show and trigger modal on click." ); - // Update modal link to trigger modal on click - const modalLink = modalLinkEl; - modalLink.onclick = () => { - setBannerIsOpen(false); - handleOpenModal(); - }; + modalLinkRef.current = modalLinkEl; + modalLinkRef.current.addEventListener("click", window.Fides.showModal); // Update to show the pre-existing modal link in the DOM - modalLink.classList.add("fides-modal-link-shown"); + modalLinkRef.current.classList.add("fides-modal-link-shown"); } else { debugLog(options.debug, "Modal link element not found."); } }, delayModalLinkMilliseconds); - return () => clearTimeout(delayModalLinkBinding); + return () => { + clearTimeout(delayModalLinkBinding); + if (modalLinkRef.current) { + modalLinkRef.current.removeEventListener( + "click", + window.Fides.showModal + ); + } + window.Fides.showModal = defaultShowModal; + }; }, [options.modalLinkId, options.debug, handleOpenModal]); const showBanner = useMemo( @@ -136,7 +152,6 @@ const Overlay: FunctionComponent = ({ const handleManagePreferencesClick = (): void => { handleOpenModal(); - setBannerIsOpen(false); }; if (!hasMounted) { diff --git a/clients/fides-js/src/fides-tcf.ts b/clients/fides-js/src/fides-tcf.ts index 7e84bfdaa9..a043ebebab 100644 --- a/clients/fides-js/src/fides-tcf.ts +++ b/clients/fides-js/src/fides-tcf.ts @@ -69,7 +69,7 @@ import { } from "./lib/initialize"; import type { Fides } from "./lib/initialize"; import { dispatchFidesEvent } from "./lib/events"; -import { debugLog, FidesCookie } from "./fides"; +import { debugLog, FidesCookie, defaultShowModal } from "./fides"; import { renderOverlay } from "./lib/tcf/renderOverlay"; import type { GppFunction } from "./lib/gpp/types"; import { makeStub } from "./lib/tcf/stub"; @@ -243,6 +243,7 @@ _Fides = { initialized: false, meta, shopify, + showModal: defaultShowModal, }; if (typeof window !== "undefined") { diff --git a/clients/fides-js/src/fides.ts b/clients/fides-js/src/fides.ts index f350bbca34..94d6acb7ab 100644 --- a/clients/fides-js/src/fides.ts +++ b/clients/fides-js/src/fides.ts @@ -74,6 +74,7 @@ import type { Fides } from "./lib/initialize"; import { renderOverlay } from "./lib/renderOverlay"; import { customGetConsentPreferences } from "./services/external/preferences"; +import { debugLog } from "./lib/consent-utils"; declare global { interface Window { @@ -161,6 +162,13 @@ const init = async (config: FidesConfig) => { dispatchFidesEvent("FidesInitialized", cookie, config.options.debug); }; +export const defaultShowModal = () => { + debugLog( + window.Fides.options.debug, + "The current experience does not support displaying a modal." + ); +}; + // The global Fides object; this is bound to window.Fides if available _Fides = { consent: {}, @@ -198,6 +206,7 @@ _Fides = { initialized: false, meta, shopify, + showModal: defaultShowModal, }; if (typeof window !== "undefined") { diff --git a/clients/fides-js/src/lib/initialize.ts b/clients/fides-js/src/lib/initialize.ts index 76ba18267f..cdebf62a05 100644 --- a/clients/fides-js/src/lib/initialize.ts +++ b/clients/fides-js/src/lib/initialize.ts @@ -62,6 +62,7 @@ export type Fides = { initialized: boolean; meta: typeof meta; shopify: typeof shopify; + showModal: () => void; }; const retrieveEffectiveRegionString = async ( diff --git a/clients/privacy-center/cypress/e2e/show-modal.cy.ts b/clients/privacy-center/cypress/e2e/show-modal.cy.ts new file mode 100644 index 0000000000..452d54d512 --- /dev/null +++ b/clients/privacy-center/cypress/e2e/show-modal.cy.ts @@ -0,0 +1,53 @@ +import { stubConfig } from "../support/stubs"; + +describe("Fides.showModal", () => { + describe("Overlay enabled", () => { + beforeEach(() => { + stubConfig({ + options: { + isOverlayEnabled: true, + }, + experience: { + show_banner: false, + }, + }); + }); + + it("Should add 'fides-overlay-modal-link-shown' class to body", () => { + cy.get("body").should("have.class", "fides-overlay-modal-link-shown"); + }); + + it("Should allow showModal", () => { + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(100); + cy.window().its("Fides").invoke("showModal"); + cy.get("@FidesUIShown").should("have.been.calledOnce"); + cy.get(".fides-modal-content").should("be.visible"); + }); + }); + + describe("Overlay disabled", () => { + beforeEach(() => { + stubConfig({ + options: { + isOverlayEnabled: false, + }, + experience: { + show_banner: false, + }, + }); + }); + + it("Should not add 'fides-overlay-modal-link-shown' class to body", () => { + cy.get("body").should("not.have.class", "fides-overlay-modal-link-shown"); + }); + + it("Should not allow showModal", () => { + // eslint-disable-next-line cypress/no-unnecessary-waiting + cy.wait(100); + cy.window().its("Fides").invoke("showModal"); + cy.get("@FidesUIShown").should("not.have.been.called"); + cy.get(".fides-modal-content").should("not.exist"); + }); + }); +});