diff --git a/CHANGELOG.md b/CHANGELOG.md index d70985841a..d0075d4a8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ The types of changes are: ## [Unreleased](https://github.com/ethyca/fides/compare/2.15.0...main) +### Added +- Empty state for when there are no relevant privacy notices in the privacy center [#3640](https://github.com/ethyca/fides/pull/3640) ### Fixed - Render linebreaks in the Fides.js overlay descriptions, etc. [#3665](https://github.com/ethyca/fides/pull/3665) diff --git a/clients/privacy-center/components/modals/NoticeEmptyStateModal.tsx b/clients/privacy-center/components/modals/NoticeEmptyStateModal.tsx new file mode 100644 index 0000000000..848f7e6db1 --- /dev/null +++ b/clients/privacy-center/components/modals/NoticeEmptyStateModal.tsx @@ -0,0 +1,39 @@ +import { + Button, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Text, +} from "@fidesui/react"; + +const NoticeEmptyStateModal = ({ + isOpen, + onClose, +}: { + isOpen: boolean; + onClose: () => void; +}) => ( + + + + + Consent management unavailable + + + + Consent management is unavailable in your area. + + + + + + + +); + +export default NoticeEmptyStateModal; diff --git a/clients/privacy-center/cypress/e2e/consent-notices.cy.ts b/clients/privacy-center/cypress/e2e/consent-notices.cy.ts index cc5fac8f6e..ce382b73c6 100644 --- a/clients/privacy-center/cypress/e2e/consent-notices.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent-notices.cy.ts @@ -131,10 +131,11 @@ describe("Privacy notice driven consent", () => { it("uses the device id found in an already existing cookie", () => { const uuid = "4fbb6edf-34f6-4717-a6f1-541fd1e5d585"; - const now = "2023-04-28T12:00:00.000Z"; + const createdAt = "2023-04-28T12:00:00.000Z"; + const updatedAt = "2023-04-29T12:00:00.000Z"; const cookie = { identity: { fides_user_device_id: uuid }, - fides_meta: { version: "0.9.0", createdAt: now }, + fides_meta: { version: "0.9.0", createdAt, updatedAt }, consent: {}, }; cy.setCookie(CONSENT_COOKIE_NAME, JSON.stringify(cookie)); diff --git a/clients/privacy-center/cypress/e2e/consent.cy.ts b/clients/privacy-center/cypress/e2e/consent.cy.ts index a1fc73d595..2709fbc9b4 100644 --- a/clients/privacy-center/cypress/e2e/consent.cy.ts +++ b/clients/privacy-center/cypress/e2e/consent.cy.ts @@ -7,6 +7,7 @@ describe("Consent modal deeplink", () => { beforeEach(() => { cy.visit("/?showConsentModal=true"); cy.loadConfigFixture("config/config_consent.json").as("config"); + cy.overrideSettings({ IS_OVERLAY_ENABLED: false }); cy.intercept("POST", `${API_URL}/consent-request`, { body: { consent_request_id: "consent-request-id", @@ -56,6 +57,7 @@ describe("Consent settings", () => { beforeEach(() => { cy.visit("/"); cy.loadConfigFixture("config/config_consent.json").as("config"); + cy.overrideSettings({ IS_OVERLAY_ENABLED: false }); }); describe("when the user isn't verified", () => { @@ -203,7 +205,7 @@ describe("Consent settings", () => { cy.visit("/consent"); cy.getByTestId("consent"); cy.loadConfigFixture("config/config_consent.json").as("config"); - cy.overrideSettings({ IS_OVERLAY_DISABLED: true }); + cy.overrideSettings({ IS_OVERLAY_ENABLED: false }); }); it("populates its header and description from config", () => { @@ -336,7 +338,7 @@ describe("Consent settings", () => { cy.visit("/consent?globalPrivacyControl=true"); cy.getByTestId("consent"); cy.loadConfigFixture("config/config_consent.json").as("config"); - cy.overrideSettings({ IS_OVERLAY_DISABLED: true }); + cy.overrideSettings({ IS_OVERLAY_ENABLED: false }); }); it("applies the GPC defaults", () => { diff --git a/clients/privacy-center/cypress/e2e/home.cy.ts b/clients/privacy-center/cypress/e2e/home.cy.ts index a9685314ec..3ca35c115c 100644 --- a/clients/privacy-center/cypress/e2e/home.cy.ts +++ b/clients/privacy-center/cypress/e2e/home.cy.ts @@ -1,8 +1,11 @@ +import { Config } from "~/types/config"; +import { API_URL } from "../support/constants"; + describe("Home", () => { it("renders the configured page info", () => { cy.visit("/"); cy.getByTestId("home"); - cy.loadConfigFixture("config/config_all.json").then((config) => { + cy.loadConfigFixture("config/config_all.json").then((config: Config) => { // DEFER: Test *all* the configurable display options // (see https://github.com/ethyca/fides/issues/3216) @@ -32,6 +35,27 @@ describe("Home", () => { cy.get("body").should("have.css", "background-color", "rgb(255, 99, 71)"); }); + it("should show an empty state when notice-driven but there are no notices", () => { + const geolocationApiUrl = "https://www.example.com/location"; + const settings = { + IS_OVERLAY_ENABLED: true, + IS_GEOLOCATION_ENABLED: true, + GEOLOCATION_API_URL: geolocationApiUrl, + }; + cy.intercept("GET", geolocationApiUrl, { + fixture: "consent/geolocation.json", + }).as("getGeolocation"); + // Will return undefined when there are no relevant privacy notices + cy.intercept("GET", `${API_URL}/privacy-experience/*`, { + body: undefined, + }).as("getExperience"); + cy.visit("/"); + cy.overrideSettings(settings); + + cy.getByTestId("card").contains("Manage your consent").click(); + cy.getByTestId("notice-empty-state"); + }); + describe("when handling errors", () => { // Allow uncaught exceptions to occur without failing the test beforeEach(() => { diff --git a/clients/privacy-center/features/consent/consent.slice.ts b/clients/privacy-center/features/consent/consent.slice.ts index a8b045e544..e6ef46807e 100644 --- a/clients/privacy-center/features/consent/consent.slice.ts +++ b/clients/privacy-center/features/consent/consent.slice.ts @@ -149,7 +149,10 @@ export const consentSlice = createSlice({ }); }, - setFidesUserDeviceId(draftState, { payload }: PayloadAction) { + setFidesUserDeviceId( + draftState, + { payload }: PayloadAction + ) { draftState.fidesUserDeviceId = payload; }, }, diff --git a/clients/privacy-center/features/consent/hooks.ts b/clients/privacy-center/features/consent/hooks.ts index 3f7e5f32fd..9ee367292d 100644 --- a/clients/privacy-center/features/consent/hooks.ts +++ b/clients/privacy-center/features/consent/hooks.ts @@ -1,4 +1,4 @@ -import { getOrMakeFidesCookie } from "fides-js"; +import { getOrMakeFidesCookie, isNewFidesCookie } from "fides-js"; import { useEffect } from "react"; import { useAppDispatch, useAppSelector } from "~/app/hooks"; import { PrivacyNoticeRegion } from "~/types/api"; @@ -30,7 +30,13 @@ export const useSubscribeToPrivacyExperienceQuery = () => { const dispatch = useAppDispatch(); const { IS_GEOLOCATION_ENABLED, GEOLOCATION_API_URL } = useSettings(); const cookie = getOrMakeFidesCookie(); - const { fides_user_device_id: fidesUserDeviceId } = cookie.identity; + const hasExistingCookie = !isNewFidesCookie(cookie); + // fidesUserDeviceId is only stable in this function if the cookie already exists + // Using it when the cookie is new results in an unstable device ID and can + // cause infinite fetching of the privacy experience, so make sure we only use a saved one + const fidesUserDeviceId = hasExistingCookie + ? cookie.identity.fides_user_device_id + : undefined; const skipFetchExperience = !useAppSelector(selectIsNoticeDriven); const skipFetchGeolocation = diff --git a/clients/privacy-center/pages/index.tsx b/clients/privacy-center/pages/index.tsx index 0e1e750f66..1416052824 100644 --- a/clients/privacy-center/pages/index.tsx +++ b/clients/privacy-center/pages/index.tsx @@ -1,4 +1,11 @@ -import { Flex, Heading, Text, Stack, useToast } from "@fidesui/react"; +import { + Flex, + Heading, + Text, + Stack, + useToast, + useDisclosure, +} from "@fidesui/react"; import React, { useEffect, useState } from "react"; import type { NextPage } from "next"; import { useRouter } from "next/router"; @@ -16,6 +23,11 @@ import { useGetIdVerificationConfigQuery } from "~/features/id-verification"; import PrivacyCard from "~/components/PrivacyCard"; import ConsentCard from "~/components/consent/ConsentCard"; import { useConfig } from "~/features/common/config.slice"; +import { useSubscribeToPrivacyExperienceQuery } from "~/features/consent/hooks"; +import { useAppSelector } from "~/app/hooks"; +import { selectPrivacyExperience } from "~/features/consent/consent.slice"; +import NoticeEmptyStateModal from "~/components/modals/NoticeEmptyStateModal"; +import { selectIsNoticeDriven } from "~/features/common/settings.slice"; const Home: NextPage = () => { const router = useRouter(); @@ -48,6 +60,24 @@ const Home: NextPage = () => { let isConsentModalOpen = isConsentModalOpenConst; const getIdVerificationConfigQuery = useGetIdVerificationConfigQuery(); + // Subscribe to experiences just to see if there are any notices. + // The subscription automatically handles skipping if overlay is not enabled + useSubscribeToPrivacyExperienceQuery(); + const noticeEmptyStateModal = useDisclosure(); + const experience = useAppSelector(selectPrivacyExperience); + const isNoticeDriven = useAppSelector(selectIsNoticeDriven); + const emptyNotices = + experience?.privacy_notices == null || + experience.privacy_notices.length === 0; + + const handleConsentCardOpen = () => { + if (isNoticeDriven && emptyNotices) { + noticeEmptyStateModal.onOpen(); + } else { + onConsentModalOpen(); + } + }; + useEffect(() => { if (getIdVerificationConfigQuery.isError) { // TODO(#2299): Use error utils from shared package. @@ -88,7 +118,7 @@ const Home: NextPage = () => { title={config.consent.button.title} iconPath={config.consent.button.icon_path} description={config.consent.button.description} - onOpen={onConsentModalOpen} + onOpen={handleConsentCardOpen} /> ); if (router.query?.showConsentModal === "true") { @@ -178,6 +208,11 @@ const Home: NextPage = () => { isVerificationRequired={isVerificationRequired} successHandler={consentModalSuccessHandler} /> + + ); };