Skip to content

Commit

Permalink
Initialize Fides.consent with default values from experience when sav…
Browse files Browse the repository at this point in the history
…ed consent cookie (fides_consent) does not exist (#4665)
  • Loading branch information
NevilleS committed Mar 7, 2024
1 parent a2cd94a commit b9d987c
Show file tree
Hide file tree
Showing 18 changed files with 376 additions and 123 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ The types of changes are:

### Fixed
- Fix issue where "x" button on Fides.js components overwrites saved preferences [#4649](https://github.com/ethyca/fides/pull/4649)
- Initialize Fides.consent with default values from experience when saved consent cookie (fides_consent) does not exist [#4665](https://github.com/ethyca/fides/pull/4665)


## [2.30.1](https://github.com/ethyca/fides/compare/2.30.0...2.30.1)
Expand Down
7 changes: 5 additions & 2 deletions clients/fides-js/src/components/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
useRef,
} from "preact/hooks";
import {
CookieKeyConsent,
FidesCookie,
FidesOptions,
PrivacyExperience,
Expand Down Expand Up @@ -37,6 +38,7 @@ interface Props {
options: FidesOptions;
experience: PrivacyExperience;
cookie: FidesCookie;
savedConsent: CookieKeyConsent;
onOpen: () => void;
onDismiss: () => void;
renderBanner: (props: RenderBannerProps) => VNode | null;
Expand All @@ -49,6 +51,7 @@ const Overlay: FunctionComponent<Props> = ({
experience,
options,
cookie,
savedConsent,
onOpen,
onDismiss,
renderBanner,
Expand Down Expand Up @@ -145,9 +148,9 @@ const Overlay: FunctionComponent<Props> = ({
() =>
!options.fidesDisableBanner &&
experience.show_banner &&
shouldResurfaceConsent(experience, cookie) &&
shouldResurfaceConsent(experience, cookie, savedConsent) &&
!options.fidesEmbed,
[cookie, experience, options]
[cookie, savedConsent, experience, options]
);

const handleManagePreferencesClick = (): void => {
Expand Down
2 changes: 2 additions & 0 deletions clients/fides-js/src/components/notices/NoticeOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const NoticeOverlay: FunctionComponent<OverlayProps> = ({
options,
fidesRegionString,
cookie,
savedConsent,
}) => {
const initialEnabledNoticeKeys = () => {
if (experience.privacy_notices) {
Expand Down Expand Up @@ -148,6 +149,7 @@ const NoticeOverlay: FunctionComponent<OverlayProps> = ({
options={options}
experience={experience}
cookie={cookie}
savedConsent={savedConsent}
onOpen={dispatchOpenOverlayEvent}
onDismiss={handleDismiss}
renderBanner={({ isOpen, onClose, onSave, onManagePreferencesClick }) => (
Expand Down
2 changes: 2 additions & 0 deletions clients/fides-js/src/components/tcf/TcfOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ const TcfOverlay: FunctionComponent<OverlayProps> = ({
experience,
options,
cookie,
savedConsent,
}) => {
const initialEnabledIds: EnabledIds = useMemo(() => {
const {
Expand Down Expand Up @@ -292,6 +293,7 @@ const TcfOverlay: FunctionComponent<OverlayProps> = ({
options={options}
experience={experience}
cookie={cookie}
savedConsent={savedConsent}
onVendorPageClick={() => {
setActiveTabIndex(2);
}}
Expand Down
2 changes: 2 additions & 0 deletions clients/fides-js/src/components/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
CookieKeyConsent,
FidesCookie,
FidesOptions,
PrivacyExperience,
Expand All @@ -16,4 +17,5 @@ export interface OverlayProps {
experience: PrivacyExperience;
cookie: FidesCookie;
fidesRegionString: string;
savedConsent: CookieKeyConsent;
}
19 changes: 10 additions & 9 deletions clients/fides-js/src/fides-ext-gpp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,25 +57,25 @@ declare global {
/**
* Special GPP util method to determine if user has existing prefs, including those on the cookie or fides string.
* Specifically, this method does not consider legacy consent has an existing pref, since they aren't relevant for GPP.
* @param consent: CookieKeyConsent | undefined
* @param savedConsent: CookieKeyConsent | undefined
* @param fides_string: string | undefined
* @param notices: Array<PrivacyNoticeWithPreference> | undefined
* @return boolean
*/
const userHasExistingPrefs = (
consent: CookieKeyConsent | undefined,
savedConsent: CookieKeyConsent | undefined,
fides_string: string | undefined,
notices: Array<PrivacyNoticeWithPreference> | undefined
): boolean => {
if (!consent) {
if (!savedConsent) {
return false;
}
if (fides_string) {
return true;
}
return Boolean(
notices &&
Object.entries(consent).some(
Object.entries(savedConsent).some(
([key, val]) =>
key in notices.map((i) => i.notice_key) && val !== undefined
)
Expand Down Expand Up @@ -136,17 +136,18 @@ export const initializeGppCmpApi = () => {
cmpApi.setCmpStatus(CmpStatus.LOADED);
// If consent does not need to be resurfaced, then we can set the signal to Ready here
window.addEventListener("FidesInitialized", (event) => {
const { experience } = window.Fides;
// TODO (PROD-1439): re-evaluate if GPP is "cheating" accessing window.Fides instead of using the event details only
const { experience, saved_consent: savedConsent } = window.Fides;
cmpApi.setSupportedAPIs(getSupportedApis());
// Set status to ready immediately upon initialization, if either:
// A. Consent should not be resurfaced
// B. User has no prefs and has all opt-in notices
if (
isPrivacyExperience(experience) &&
(!shouldResurfaceConsent(experience, event.detail) ||
(!shouldResurfaceConsent(experience, event.detail, savedConsent) ||
(allNoticesAreDefaultOptIn(experience.privacy_notices) &&
!userHasExistingPrefs(
event.detail.consent,
savedConsent,
event.detail.fides_string,
experience.privacy_notices
)))
Expand All @@ -170,13 +171,13 @@ export const initializeGppCmpApi = () => {

window.addEventListener("FidesUIShown", (event) => {
// Set US GPP notice fields
const { experience } = window.Fides;
const { experience, saved_consent: savedConsent } = window.Fides;
if (isPrivacyExperience(experience)) {
// set signal status to ready only for users with no existing prefs and if notices are all opt-in by default
if (
allNoticesAreDefaultOptIn(experience.privacy_notices) &&
!userHasExistingPrefs(
event.detail.consent,
savedConsent,
event.detail.fides_string,
experience.privacy_notices
)
Expand Down
15 changes: 13 additions & 2 deletions clients/fides-js/src/fides-tcf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { meta } from "./integrations/meta";
import { shopify } from "./integrations/shopify";

import {
CookieKeyConsent,
FidesConfig,
FidesOptionsOverrides,
FidesOverrides,
Expand Down Expand Up @@ -100,7 +101,7 @@ declare global {
// eslint-disable-next-line no-underscore-dangle,@typescript-eslint/naming-convention
let _Fides: Fides;

const updateExperience = async ({
const updateExperience = ({
cookie,
experience,
debug = false,
Expand All @@ -110,7 +111,7 @@ const updateExperience = async ({
experience: PrivacyExperience;
debug?: boolean;
isExperienceClientSideFetched: boolean;
}): Promise<Partial<PrivacyExperience>> => {
}): Partial<PrivacyExperience> => {
if (!isExperienceClientSideFetched) {
// If it's not client side fetched, we don't update anything since the cookie has already
// been updated earlier.
Expand Down Expand Up @@ -160,6 +161,13 @@ const init = async (config: FidesConfig) => {
...getInitialCookie(config),
...overrides.consentPrefsOverrides?.consent,
};

// Keep a copy of saved consent from the cookie, since we update the "cookie"
// value during initialization based on overrides, experience, etc.
const savedConsent: CookieKeyConsent = {
...cookie.consent,
};

// Update the fidesString if we have an override and the TC portion is valid
const { fidesString } = config.options;
if (fidesString) {
Expand All @@ -184,6 +192,7 @@ const init = async (config: FidesConfig) => {
const initialFides = getInitialFides({
...config,
cookie,
savedConsent,
updateExperienceFromCookieConsent: updateExperienceFromCookieConsentTcf,
});
// Initialize the CMP API early so that listeners are established
Expand All @@ -196,6 +205,7 @@ const init = async (config: FidesConfig) => {
const updatedFides = await initialize({
...config,
cookie,
savedConsent,
experience,
renderOverlay,
updateExperience,
Expand Down Expand Up @@ -238,6 +248,7 @@ _Fides = {
fides_meta: {},
identity: {},
tcf_consent: {},
saved_consent: {},
gtm,
init,
initialized: false,
Expand Down
15 changes: 13 additions & 2 deletions clients/fides-js/src/fides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
consentCookieObjHasSomeConsentSet,
} from "./lib/cookie";
import {
CookieKeyConsent,
FidesConfig,
FidesCookie,
FidesOptionsOverrides,
Expand Down Expand Up @@ -87,12 +88,12 @@ declare global {
// eslint-disable-next-line no-underscore-dangle,@typescript-eslint/naming-convention
let _Fides: Fides;

const updateExperience = async (
const updateExperience = (
cookie: FidesCookie,
experience: PrivacyExperience,
debug?: boolean,
isExperienceClientSideFetched?: boolean
): Promise<PrivacyExperience> => {
): Partial<PrivacyExperience> => {
let updatedExperience: PrivacyExperience = experience;
const preferencesExistOnCookie = consentCookieObjHasSomeConsentSet(
cookie.consent
Expand Down Expand Up @@ -128,9 +129,17 @@ const init = async (config: FidesConfig) => {
...getInitialCookie(config),
...overrides.consentPrefsOverrides?.consent,
};

// Keep a copy of saved consent from the cookie, since we update the "cookie"
// value during initialization based on overrides, experience, etc.
const savedConsent: CookieKeyConsent = {
...cookie.consent,
};

const initialFides = getInitialFides({
...config,
cookie,
savedConsent,
updateExperienceFromCookieConsent: updateExperienceFromCookieConsentNotices,
});
if (initialFides) {
Expand All @@ -141,6 +150,7 @@ const init = async (config: FidesConfig) => {
const updatedFides = await initialize({
...config,
cookie,
savedConsent,
experience,
renderOverlay,
updateExperience: ({
Expand Down Expand Up @@ -201,6 +211,7 @@ _Fides = {
fides_meta: {},
identity: {},
tcf_consent: {},
saved_consent: {},
gtm,
init,
initialized: false,
Expand Down
13 changes: 10 additions & 3 deletions clients/fides-js/src/lib/consent-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ConsentContext } from "./consent-context";
import {
ComponentType,
ConsentMechanism,
CookieKeyConsent,
EmptyExperience,
FidesCookie,
FidesOptions,
Expand Down Expand Up @@ -206,7 +207,8 @@ export const getTcfDefaultPreference = (tcfObject: TcfModelsRecord) =>
*/
export const shouldResurfaceConsent = (
experience: PrivacyExperience,
cookie: FidesCookie
cookie: FidesCookie,
savedConsent: CookieKeyConsent
): boolean => {
if (experience.component === ComponentType.TCF_OVERLAY) {
if (experience.meta?.version_hash) {
Expand All @@ -222,10 +224,15 @@ export const shouldResurfaceConsent = (
) {
return false;
}
// If not every notice has previous user consent, we need to resurface consent
// Always resurface if there is no prior consent
if (!savedConsent) {
return true;
}
// Lastly, if we do have a prior consent state, resurface if we find *any*
// notices that don't have prior consent in that state
return Boolean(
!experience.privacy_notices?.every((notice) =>
noticeHasConsentInCookie(notice, cookie.consent)
noticeHasConsentInCookie(notice, savedConsent)
)
);
};
Expand Down
1 change: 1 addition & 0 deletions clients/fides-js/src/lib/consent-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const resolveConsentValue = (
return true;
}
// Note about GPC - consent has already applied to the cookie at this point, so we can trust preference there
// DEFER (PROD-1780): delete context arg for safety
if (consent && noticeHasConsentInCookie(notice, consent)) {
return !!consent[notice.notice_key];
}
Expand Down
2 changes: 2 additions & 0 deletions clients/fides-js/src/lib/consent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const initOverlay = async ({
experience,
fidesRegionString,
cookie,
savedConsent,
options,
renderOverlay,
}: OverlayProps & {
Expand Down Expand Up @@ -68,6 +69,7 @@ export const initOverlay = async ({
fidesRegionString,
cookie,
options,
savedConsent,
},
parentElem
);
Expand Down
46 changes: 45 additions & 1 deletion clients/fides-js/src/lib/cookie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,6 @@ export const saveFidesCookie = (
* Updates prefetched experience, based on:
* 1) experience: pre-fetched or client-side experience-based consent configuration
* 2) cookie: cookie containing user preference.
*
* Returns updated experience with user preferences.
*/
Expand Down Expand Up @@ -354,3 +353,48 @@ export const updateCookieFromNoticePreferences = async (
consent: consentCookieKey,
};
};

/**
* Extract the current consent state from the given PrivacyExperience by
* iterating through the notices and pulling out the current preferences (or
* default values). This is used during initialization to override saved cookie
* values with newer values from the experience.
*/
export const getConsentStateFromExperience = (
experience: PrivacyExperience
): CookieKeyConsent => {
const consent: CookieKeyConsent = {};
if (!experience.privacy_notices) {
return consent;
}
experience.privacy_notices.forEach((notice) => {
if (notice.current_preference) {
consent[notice.notice_key] = transformUserPreferenceToBoolean(
notice.current_preference
);
} else if (notice.default_preference) {
consent[notice.notice_key] = transformUserPreferenceToBoolean(
notice.default_preference
);
}
});
return consent;
};

/**
* Update the "cookie" state with any preferences from the given
* PrivacyExperience. See getConsentStateFromExperience for details.
*/
export const updateCookieFromExperience = ({
cookie,
experience,
}: {
cookie: FidesCookie;
experience: PrivacyExperience;
}): FidesCookie => {
const consent = getConsentStateFromExperience(experience);
return {
...cookie,
consent,
};
};
Loading

0 comments on commit b9d987c

Please sign in to comment.