From 8723d8468f9916b26a5434c4b02ba54681eb23c7 Mon Sep 17 00:00:00 2001 From: Jason Gill Date: Tue, 3 Dec 2024 14:35:53 -0700 Subject: [PATCH] Mobile Native POC support work (#5541) --- CHANGELOG.md | 3 + clients/README.md | 2 - .../src/types/api/models/ConsentMethod.ts | 1 + .../fides-js/docs/interfaces/FidesOptions.md | 16 +- clients/fides-js/rollup.config.mjs | 2 +- .../src/components/ConsentButtons.tsx | 25 +- clients/fides-js/src/components/Overlay.tsx | 9 +- .../src/components/notices/NoticeOverlay.tsx | 61 +- .../src/components/tcf/TcfConsentButtons.tsx | 79 +- .../src/components/tcf/TcfOverlay.tsx | 127 +- clients/fides-js/src/docs/fides-options.ts | 13 +- clients/fides-js/src/fides-ext-gpp.ts | 25 +- clients/fides-js/src/fides-tcf.ts | 7 +- clients/fides-js/src/fides.ts | 7 +- clients/fides-js/src/lib/common-utils.ts | 9 + clients/fides-js/src/lib/consent-constants.ts | 6 + clients/fides-js/src/lib/consent-types.ts | 7 +- clients/fides-js/src/lib/tcf/constants.ts | 10 + .../privacy-center/app/server-environment.ts | 2 + .../app/server-utils/PrivacyCenterSettings.ts | 3 + .../server-utils/loadEnvironmentVariables.ts | 5 + .../cypress/e2e/consent-banner-tcf.cy.ts | 170 ++ .../cypress/e2e/consent-banner.cy.ts | 270 +++ .../consent/experience_tcf_minimal.json | 215 +++ clients/privacy-center/pages/api/fides-js.ts | 1 + .../public/fides-js-components-demo.html | 2 +- .../privacy-center/public/fides-js-demo.html | 2 +- .../types/api/models/ConsentMethod.ts | 1 + clients/sample-app/package-lock.json | 1643 +++++++++++------ clients/sample-app/package.json | 10 +- .../src/components/Home/style.module.scss | 31 + clients/sample-app/src/pages/_app.tsx | 7 +- src/fides/api/models/privacy_preference.py | 1 + 33 files changed, 2036 insertions(+), 736 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fadcb62430..17bf562308 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,9 +19,12 @@ The types of changes are: ### Added - Added new column for Action Type in privacy request event logs [#5546](https://github.com/ethyca/fides/pull/5546) +- Added `fides_consent_override` option in FidesJS SDK [#5541](https://github.com/ethyca/fides/pull/5541) +- Added new `script` ConsentMethod in FidesJS SDK for tracking automated consent [#5541](https://github.com/ethyca/fides/pull/5541) ### Changed - Adding hashes to system tab URLs [#5535](https://github.com/ethyca/fides/pull/5535) +- Updated Cookie House to be responsive [#5541](https://github.com/ethyca/fides/pull/5541) ### Developer Experience - Migrated remaining instances of Chakra's Select component to use Ant's Select component [#5502](https://github.com/ethyca/fides/pull/5502) diff --git a/clients/README.md b/clients/README.md index 0349eededa..d3ebd3577d 100644 --- a/clients/README.md +++ b/clients/README.md @@ -1,5 +1,3 @@ # Clients The clients directory houses all front-end packages and shared code amongst clients, and also includes e2e tests. - -See the [UI Contribution Guide](http://localhost:8000/fides/development/ui/overview/) for more information diff --git a/clients/admin-ui/src/types/api/models/ConsentMethod.ts b/clients/admin-ui/src/types/api/models/ConsentMethod.ts index ed8c78cbba..6ae5a06a35 100644 --- a/clients/admin-ui/src/types/api/models/ConsentMethod.ts +++ b/clients/admin-ui/src/types/api/models/ConsentMethod.ts @@ -6,6 +6,7 @@ export enum ConsentMethod { BUTTON = "button", REJECT = "reject", ACCEPT = "accept", + SCRIPT = "script", SAVE = "save", DISMISS = "dismiss", GPC = "gpc", diff --git a/clients/fides-js/docs/interfaces/FidesOptions.md b/clients/fides-js/docs/interfaces/FidesOptions.md index a510a57997..fd280e87c2 100644 --- a/clients/fides-js/docs/interfaces/FidesOptions.md +++ b/clients/fides-js/docs/interfaces/FidesOptions.md @@ -1,7 +1,7 @@ # Interface: FidesOptions FidesJS supports a variety of custom options to modify it's behavior or -enabled more advanced usage. For example, the `fides_locale` option can be +enable more advanced usage. For example, the `fides_locale` option can be provided to override the browser locale. See the properties list below for the supported options and example usage for each. @@ -160,3 +160,17 @@ overriden at the page-level as needed. Only applicable to a TCF experience. For more details, see the [TCF CMP API technical specification](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#what-does-the-gdprapplies-value-mean) * Defaults to `true`. + +*** + +### fides\_consent\_override + +> **fides\_consent\_override**: `"accept"` \| `"reject"` + +FidesJS will automatically opt in or out of all notices with this option and +only show the consent modal upon user request. This is useful for any +scenario where the user has previously provided consent in a different +context (e.g. a native app, another website, etc.) and you want to ensure +that those preferences are respected. + +Defaults to `undefined`. diff --git a/clients/fides-js/rollup.config.mjs b/clients/fides-js/rollup.config.mjs index eb8de09257..5e52b4d1d4 100644 --- a/clients/fides-js/rollup.config.mjs +++ b/clients/fides-js/rollup.config.mjs @@ -16,7 +16,7 @@ const GZIP_SIZE_ERROR_KB = 45; // fail build if bundle size exceeds this const GZIP_SIZE_WARN_KB = 35; // log a warning if bundle size exceeds this // TCF -const GZIP_SIZE_TCF_ERROR_KB = 85; +const GZIP_SIZE_TCF_ERROR_KB = 85.5; const GZIP_SIZE_TCF_WARN_KB = 75; const preactAliases = { diff --git a/clients/fides-js/src/components/ConsentButtons.tsx b/clients/fides-js/src/components/ConsentButtons.tsx index ea19b10cee..7bc6b00f2f 100644 --- a/clients/fides-js/src/components/ConsentButtons.tsx +++ b/clients/fides-js/src/components/ConsentButtons.tsx @@ -3,7 +3,6 @@ import { useEffect, useState } from "preact/hooks"; import { ButtonType, - ConsentMechanism, ConsentMethod, FidesInitOptions, PrivacyExperience, @@ -139,6 +138,8 @@ type NoticeKeys = Array; interface NoticeConsentButtonProps { experience: PrivacyExperience; + onAcceptAll: () => void; + onRejectAll: () => void; onSave: (consentMethod: ConsentMethod, noticeKeys: NoticeKeys) => void; onManagePreferencesClick?: () => void; enabledKeys: NoticeKeys; @@ -150,6 +151,8 @@ interface NoticeConsentButtonProps { export const NoticeConsentButtons = ({ experience, + onAcceptAll, + onRejectAll, onSave, onManagePreferencesClick, enabledKeys, @@ -164,13 +167,6 @@ export const NoticeConsentButtons = ({ } const { privacy_notices: notices } = experience; - const handleAcceptAll = () => { - onSave( - ConsentMethod.ACCEPT, - notices.map((n) => n.notice_key), - ); - }; - const handleAcknowledgeNotices = () => { onSave( ConsentMethod.ACKNOWLEDGE, @@ -178,15 +174,6 @@ export const NoticeConsentButtons = ({ ); }; - const handleRejectAll = () => { - onSave( - ConsentMethod.REJECT, - notices - .filter((n) => n.consent_mechanism === ConsentMechanism.NOTICE_ONLY) - .map((n) => n.notice_key), - ); - }; - const handleSave = () => { onSave(ConsentMethod.SAVE, enabledKeys); }; @@ -219,8 +206,8 @@ export const NoticeConsentButtons = ({ void; - onSave: () => void; onManagePreferencesClick: () => void; } @@ -72,13 +72,15 @@ const Overlay: FunctionComponent = ({ const delayBannerMilliseconds = 100; const delayModalLinkMilliseconds = 200; const hasMounted = useHasMounted(); + const isAutomatedConsent = isConsentOverride(options); const showBanner = useMemo( () => + !isAutomatedConsent && !options.fidesDisableBanner && experience.experience_config?.component !== ComponentType.MODAL && shouldResurfaceConsent(experience, cookie, savedConsent), - [cookie, savedConsent, experience, options], + [cookie, savedConsent, experience, options, isAutomatedConsent], ); const [bannerIsOpen, setBannerIsOpen] = useState( @@ -237,9 +239,6 @@ const Overlay: FunctionComponent = ({ onClose: () => { setBannerIsOpen(false); }, - onSave: () => { - setBannerIsOpen(false); - }, onManagePreferencesClick: handleManagePreferencesClick, }) : null} diff --git a/clients/fides-js/src/components/notices/NoticeOverlay.tsx b/clients/fides-js/src/components/notices/NoticeOverlay.tsx index 4f764f4a05..b24ca220b3 100644 --- a/clients/fides-js/src/components/notices/NoticeOverlay.tsx +++ b/clients/fides-js/src/components/notices/NoticeOverlay.tsx @@ -3,6 +3,7 @@ import "../fides.css"; import { FunctionComponent, h } from "preact"; import { useCallback, useEffect, useMemo, useState } from "preact/hooks"; +import { isConsentOverride } from "../../lib/common-utils"; import { getConsentContext } from "../../lib/consent-context"; import { ConsentMechanism, @@ -229,6 +230,47 @@ const NoticeOverlay: FunctionComponent = ({ ], ); + const handleAcceptAll = useCallback( + (wasAutomated?: boolean) => { + handleUpdatePreferences( + wasAutomated ? ConsentMethod.SCRIPT : ConsentMethod.ACCEPT, + privacyNoticeItems.map((n) => n.notice.notice_key), + ); + }, + [handleUpdatePreferences, privacyNoticeItems], + ); + + const handleRejectAll = useCallback( + (wasAutomated?: boolean) => { + handleUpdatePreferences( + wasAutomated ? ConsentMethod.SCRIPT : ConsentMethod.REJECT, + privacyNoticeItems + .filter( + (n) => n.notice.consent_mechanism === ConsentMechanism.NOTICE_ONLY, + ) + .map((n) => n.notice.notice_key), + ); + }, + [handleUpdatePreferences, privacyNoticeItems], + ); + + useEffect(() => { + if (isConsentOverride(options) && experience.privacy_notices) { + if (options.fidesConsentOverride === ConsentMethod.ACCEPT) { + fidesDebugger( + "Consent automatically accepted by fides_accept_all override!", + ); + handleAcceptAll(true); + } else if (options.fidesConsentOverride === ConsentMethod.REJECT) { + fidesDebugger( + "Consent automatically rejected by fides_reject_all override!", + ); + handleRejectAll(true); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [experience.privacy_notices, options.fidesConsentOverride]); + const dispatchOpenBannerEvent = useCallback(() => { dispatchFidesEvent("FidesUIShown", cookie, options.debug, { servingComponent: ServingComponent.BANNER, @@ -268,7 +310,6 @@ const NoticeOverlay: FunctionComponent = ({ isEmbedded, isOpen, onClose, - onSave, onManagePreferencesClick, }) => { const isAcknowledge = @@ -290,12 +331,20 @@ const NoticeOverlay: FunctionComponent = ({ experience={experience} onManagePreferencesClick={onManagePreferencesClick} enabledKeys={draftEnabledNoticeKeys} + onAcceptAll={() => { + handleAcceptAll(); + onClose(); + }} + onRejectAll={() => { + handleRejectAll(); + onClose(); + }} onSave={( consentMethod: ConsentMethod, keys: Array, ) => { handleUpdatePreferences(consentMethod, keys); - onSave(); + onClose(); }} isAcknowledge={isAcknowledge} hideOptInOut={isAcknowledge} @@ -323,6 +372,14 @@ const NoticeOverlay: FunctionComponent = ({ { + handleAcceptAll(); + onClose(); + }} + onRejectAll={() => { + handleRejectAll(); + onClose(); + }} onSave={( consentMethod: ConsentMethod, keys: Array, diff --git a/clients/fides-js/src/components/tcf/TcfConsentButtons.tsx b/clients/fides-js/src/components/tcf/TcfConsentButtons.tsx index eeaf2e83dc..5329d2c0ac 100644 --- a/clients/fides-js/src/components/tcf/TcfConsentButtons.tsx +++ b/clients/fides-js/src/components/tcf/TcfConsentButtons.tsx @@ -1,34 +1,27 @@ import { h, VNode } from "preact"; import { - ConsentMethod, FidesInitOptions, PrivacyExperience, PrivacyExperienceMinimal, } from "../../lib/consent-types"; -import type { EnabledIds, TcfModels } from "../../lib/tcf/types"; import { ConsentButtons } from "../ConsentButtons"; interface TcfConsentButtonProps { experience: PrivacyExperience | PrivacyExperienceMinimal; options: FidesInitOptions; onManagePreferencesClick?: () => void; - onSave: (consentMethod: ConsentMethod, keys: EnabledIds) => void; + onRejectAll: () => void; + onAcceptAll: () => void; renderFirstButton?: () => VNode; isInModal?: boolean; } -const getAllIds = (modelList: TcfModels) => { - if (!modelList) { - return []; - } - return modelList.map((m) => `${m.id}`); -}; - export const TcfConsentButtons = ({ experience, onManagePreferencesClick, - onSave, + onRejectAll, + onAcceptAll, renderFirstButton, isInModal, options, @@ -39,72 +32,12 @@ export const TcfConsentButtons = ({ const isGVLLoading = Object.keys(experience.gvl || {}).length === 0; - const handleAcceptAll = () => { - let allIds: EnabledIds; - if (!experience.minimal_tcf) { - // eslint-disable-next-line no-param-reassign - experience = experience as PrivacyExperience; - allIds = { - purposesConsent: getAllIds(experience.tcf_purpose_consents), - purposesLegint: getAllIds(experience.tcf_purpose_legitimate_interests), - specialPurposes: getAllIds(experience.tcf_special_purposes), - features: getAllIds(experience.tcf_features), - specialFeatures: getAllIds(experience.tcf_special_features), - vendorsConsent: getAllIds([ - ...(experience.tcf_vendor_consents || []), - ...(experience.tcf_system_consents || []), - ]), - vendorsLegint: getAllIds([ - ...(experience.tcf_vendor_legitimate_interests || []), - ...(experience.tcf_system_legitimate_interests || []), - ]), - }; - } else { - // eslint-disable-next-line no-param-reassign - experience = experience as PrivacyExperienceMinimal; - allIds = { - purposesConsent: - experience.tcf_purpose_consent_ids?.map((id) => `${id}`) || [], - purposesLegint: - experience.tcf_purpose_legitimate_interest_ids?.map( - (id) => `${id}`, - ) || [], - specialPurposes: - experience.tcf_special_purpose_ids?.map((id) => `${id}`) || [], - features: experience.tcf_feature_ids?.map((id) => `${id}`) || [], - specialFeatures: - experience.tcf_special_feature_ids?.map((id) => `${id}`) || [], - vendorsConsent: [ - ...(experience.tcf_vendor_consent_ids || []), - ...(experience.tcf_system_consent_ids || []), - ], - vendorsLegint: [ - ...(experience.tcf_vendor_legitimate_interest_ids || []), - ...(experience.tcf_system_legitimate_interest_ids || []), - ], - }; - } - onSave(ConsentMethod.ACCEPT, allIds); - }; - const handleRejectAll = () => { - const emptyIds: EnabledIds = { - purposesConsent: [], - purposesLegint: [], - specialPurposes: [], - features: [], - specialFeatures: [], - vendorsConsent: [], - vendorsLegint: [], - }; - onSave(ConsentMethod.REJECT, emptyIds); - }; - return ( { + if (!modelList) { + return []; + } + return modelList.map((m) => `${m.id}`); +}; + interface TcfOverlayProps extends Omit { experienceMinimal: PrivacyExperienceMinimal; } @@ -159,19 +171,11 @@ export const TcfOverlay = ({ const { setVendorCount } = useVendorButton(); - const [draftIds, setDraftIds] = useState(); + const [draftIds, setDraftIds] = useState(EMPTY_ENABLED_IDS); useEffect(() => { if (!experience) { - setDraftIds({ - purposesConsent: [], - purposesLegint: [], - specialPurposes: [], - features: [], - specialFeatures: [], - vendorsConsent: [], - vendorsLegint: [], - }); + setDraftIds(EMPTY_ENABLED_IDS); } else { const { tcf_purpose_consents: consentPurposes = [], @@ -293,6 +297,83 @@ export const TcfOverlay = ({ ], ); + const handleAcceptAll = useCallback( + (wasAutomated?: boolean) => { + let allIds: EnabledIds; + let exp = experience || experienceMinimal; + if (!exp.minimal_tcf) { + exp = experience as PrivacyExperience; + allIds = { + purposesConsent: getAllIds(exp.tcf_purpose_consents), + purposesLegint: getAllIds(exp.tcf_purpose_legitimate_interests), + specialPurposes: getAllIds(exp.tcf_special_purposes), + features: getAllIds(exp.tcf_features), + specialFeatures: getAllIds(exp.tcf_special_features), + vendorsConsent: getAllIds([ + ...(exp.tcf_vendor_consents || []), + ...(exp.tcf_system_consents || []), + ]), + vendorsLegint: getAllIds([ + ...(exp.tcf_vendor_legitimate_interests || []), + ...(exp.tcf_system_legitimate_interests || []), + ]), + }; + } else { + // eslint-disable-next-line no-param-reassign + exp = experienceMinimal as PrivacyExperienceMinimal; + allIds = { + purposesConsent: + exp.tcf_purpose_consent_ids?.map((id) => `${id}`) || [], + purposesLegint: + exp.tcf_purpose_legitimate_interest_ids?.map((id) => `${id}`) || [], + specialPurposes: + exp.tcf_special_purpose_ids?.map((id) => `${id}`) || [], + features: exp.tcf_feature_ids?.map((id) => `${id}`) || [], + specialFeatures: + exp.tcf_special_feature_ids?.map((id) => `${id}`) || [], + vendorsConsent: [ + ...(exp.tcf_vendor_consent_ids || []), + ...(exp.tcf_system_consent_ids || []), + ], + vendorsLegint: [ + ...(exp.tcf_vendor_legitimate_interest_ids || []), + ...(exp.tcf_system_legitimate_interest_ids || []), + ], + }; + } + handleUpdateAllPreferences( + wasAutomated ? ConsentMethod.SCRIPT : ConsentMethod.ACCEPT, + allIds, + ); + }, + [experience, experienceMinimal, handleUpdateAllPreferences], + ); + + const handleRejectAll = useCallback( + (wasAutomated?: boolean) => { + handleUpdateAllPreferences( + wasAutomated ? ConsentMethod.SCRIPT : ConsentMethod.REJECT, + EMPTY_ENABLED_IDS, + ); + }, + [handleUpdateAllPreferences], + ); + + useEffect(() => { + if (options.fidesConsentOverride === ConsentMethod.ACCEPT) { + fidesDebugger( + "Consent automatically accepted by fides_accept_all override!", + ); + handleAcceptAll(true); + } else if (options.fidesConsentOverride === ConsentMethod.REJECT) { + fidesDebugger( + "Consent automatically rejected by fides_reject_all override!", + ); + handleRejectAll(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [options.fidesConsentOverride]); + const [activeTabIndex, setActiveTabIndex] = useState(0); const dispatchOpenBannerEvent = useCallback(() => { @@ -336,7 +417,6 @@ export const TcfOverlay = ({ isEmbedded, isOpen, onClose, - onSave, onManagePreferencesClick, }) => { const goToVendorTab = () => { @@ -350,17 +430,21 @@ export const TcfOverlay = ({ isEmbedded={isEmbedded} onOpen={dispatchOpenBannerEvent} onClose={() => { - onClose(); handleDismiss(); + onClose(); }} onVendorPageClick={goToVendorTab} renderButtonGroup={() => ( { - handleUpdateAllPreferences(consentMethod, keys); - onSave(); + onAcceptAll={() => { + handleAcceptAll(); + onClose(); + }} + onRejectAll={() => { + handleRejectAll(); + onClose(); }} options={options} /> @@ -401,12 +485,19 @@ export const TcfOverlay = ({ return ( { + handleAcceptAll(); + onClose(); + }} + onRejectAll={() => { + handleRejectAll(); + onClose(); + }} renderFirstButton={() => (