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}
/>
+
+
);
};