Skip to content

Commit

Permalink
Support Accept/Reject All with minimal TCF (#5298)
Browse files Browse the repository at this point in the history
  • Loading branch information
gilluminate authored Sep 19, 2024
1 parent b1d8892 commit a19d30b
Show file tree
Hide file tree
Showing 16 changed files with 542 additions and 80 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ The types of changes are:
- Add ability to edit dataset YAML from dataset view [#5262](https://github.com/ethyca/fides/pull/5262)
- Added support for "in progress" status in classification [#5248](https://github.com/ethyca/fides/pull/5248)
- Clarify GCP service account permissions when setting up Google Cloud SQL for Postgres in Admin-UI [#5245](https://github.com/ethyca/fides/pull/5266)
- Support minimal GVL in minimal TCF response allowing Accept/Reject from banner before full GVL is loaded [#5298](https://github.com/ethyca/fides/pull/5298)

### Changed
- Validate no path in `server_host` var for CLI config; if there is one then take only up until the first forward slash
Expand Down
2 changes: 1 addition & 1 deletion clients/fides-js/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,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 = 86;
const GZIP_SIZE_TCF_WARN_KB = 75;

const preactAliases = {
Expand Down
7 changes: 6 additions & 1 deletion clients/fides-js/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FunctionComponent, h } from "preact";

import { ButtonType } from "../lib/consent-types";
import { Spinner } from "./Spinner";

interface ButtonProps {
buttonType: ButtonType;
Expand All @@ -9,6 +10,7 @@ interface ButtonProps {
onClick?: () => void;
className?: string;
disabled?: boolean;
loading?: boolean;
}

const Button: FunctionComponent<ButtonProps> = ({
Expand All @@ -18,16 +20,19 @@ const Button: FunctionComponent<ButtonProps> = ({
onClick,
className = "",
disabled,
loading,
}) => (
<button
type="button"
id={id}
className={`fides-banner-button fides-banner-button-${buttonType.valueOf()} ${className}`}
onClick={onClick}
data-testid={`${label}-btn`}
disabled={disabled}
disabled={disabled || loading}
style={{ cursor: disabled || loading ? "not-allowed" : "pointer" }}
>
{label || ""}
{loading && <Spinner />}
</button>
);

Expand Down
31 changes: 25 additions & 6 deletions clients/fides-js/src/components/ConsentButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Fragment, h, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";

import {
ButtonType,
Expand All @@ -25,7 +26,8 @@ interface ConsentButtonProps {
hideOptInOut?: boolean;
isInModal?: boolean;
isTCF?: boolean;
disableAll?: boolean;
isMinimalTCF?: boolean;
isGVLLoading?: boolean;
}
export const ConsentButtons = ({
availableLocales = [DEFAULT_LOCALE],
Expand All @@ -37,14 +39,31 @@ export const ConsentButtons = ({
options,
isInModal,
isTCF,
disableAll,
isMinimalTCF,
isGVLLoading,
}: ConsentButtonProps) => {
const [isLoadingModal, setIsLoadingModal] = useState<boolean>(false);
const { i18n } = useI18n();
const isMobile = useMediaQuery("(max-width: 768px)");
const includeLanguageSelector = i18n.availableLanguages?.length > 1;
const includePrivacyPolicyLink =
messageExists(i18n, "exp.privacy_policy_link_label") &&
messageExists(i18n, "exp.privacy_policy_url");
const handleManagePreferencesClick = () => {
const isReady = !isTCF || !isMinimalTCF;
setIsLoadingModal(!isReady);
if (onManagePreferencesClick && isReady) {
onManagePreferencesClick();
}
};

useEffect(() => {
if (isLoadingModal && !isMinimalTCF) {
handleManagePreferencesClick();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoadingModal, isMinimalTCF]);

return (
<div id="fides-button-group">
<div
Expand All @@ -61,24 +80,24 @@ export const ConsentButtons = ({
<Button
buttonType={ButtonType.SECONDARY}
label={i18n.t("exp.privacy_preferences_link_label")}
onClick={onManagePreferencesClick}
onClick={handleManagePreferencesClick}
className="fides-manage-preferences-button"
disabled={disableAll}
loading={isLoadingModal}
/>
)}
<Button
buttonType={ButtonType.PRIMARY}
label={i18n.t("exp.reject_button_label")}
onClick={onRejectAll}
className="fides-reject-all-button"
disabled={disableAll}
loading={isGVLLoading}
/>
<Button
buttonType={ButtonType.PRIMARY}
label={i18n.t("exp.accept_button_label")}
onClick={onAcceptAll}
className="fides-accept-all-button"
disabled={disableAll}
loading={isGVLLoading}
/>
</Fragment>
)}
Expand Down
53 changes: 30 additions & 23 deletions clients/fides-js/src/components/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,27 +249,10 @@ const Overlay: FunctionComponent<Props> = ({
onManagePreferencesClick: handleManagePreferencesClick,
})
: null}
{!!renderModalContent &&
!!renderModalFooter &&
(options.fidesEmbed ? (
bannerIsOpen ? null : (
<ConsentContent
titleProps={attributes.title}
renderModalFooter={() =>
renderModalFooter({
onClose: handleCloseModalAfterSave,
isMobile: false,
})
}
>
{renderModalContent()}
</ConsentContent>
)
) : (
<ConsentModal
attributes={attributes}
dismissable={experience.experience_config.dismissable}
onVendorPageClick={onVendorPageClick}
{options.fidesEmbed ? (
bannerIsOpen || !renderModalContent || !renderModalFooter ? null : (
<ConsentContent
titleProps={attributes.title}
renderModalFooter={() =>
renderModalFooter({
onClose: handleCloseModalAfterSave,
Expand All @@ -278,8 +261,32 @@ const Overlay: FunctionComponent<Props> = ({
}
>
{renderModalContent()}
</ConsentModal>
))}
</ConsentContent>
)
) : (
/* If the modal is not going to be embedded, we at least need to instantiate the wrapper
* before the footer and content are available so that it can be opened while those render.
* Otherwise a race condition can cause problems with the following scenario:
* button click too early -> button has spinner -> experience loads -> modal opener called.
* This scenario exists today for TCF Minimal experience where banner appears before
* full experience (and therefore the modal) is ready.
*/
<ConsentModal
attributes={attributes}
dismissable={experience.experience_config.dismissable}
onVendorPageClick={onVendorPageClick}
renderModalFooter={() =>
renderModalFooter
? renderModalFooter({
onClose: handleCloseModalAfterSave,
isMobile: false,
})
: null
}
>
{renderModalContent && renderModalContent()}
</ConsentModal>
)}
</div>
);
};
Expand Down
3 changes: 3 additions & 0 deletions clients/fides-js/src/components/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { h } from "preact";

export const Spinner = () => <div className="fides-spinner" />;
28 changes: 26 additions & 2 deletions clients/fides-js/src/components/fides.css
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,10 @@ div#fides-button-group {

button.fides-banner-button {
font-size: var(--fides-overlay-font-size-buttons);
display: inline-block;
display: flex;
cursor: pointer;
text-align: center;
align-items: center;
justify-content: center;
margin: 0;
margin-top: 4px;
padding: var(--fides-overlay-button-padding);
Expand Down Expand Up @@ -307,6 +308,29 @@ button.fides-banner-button.fides-acknowledge-button {
min-width: 160px;
}

.fides-spinner {
border: 2px solid transparent;
border-top: 2px solid;
border-right: 2px solid;
border-top-color: var(--fides-overlay-primary-color);
border-right-color: var(--fides-overlay-primary-color);
border-radius: 50%;
width: 1em;
height: 1em;
animation: spin 1s linear infinite;
margin-left: 8px;
}

.fides-banner-button-primary .fides-spinner {
border-top-color: var(--fides-overlay-primary-button-text-color);
border-right-color: var(--fides-overlay-primary-button-text-color);
}

.fides-banner-button-secondary .fides-spinner {
border-top-color: var(--fides-overlay-secondary-button-border-color);
border-right-color: var(--fides-overlay-secondary-button-border-color);
}

/** Modal */
div.fides-modal-content {
font-family: var(--fides-overlay-font-family);
Expand Down
56 changes: 41 additions & 15 deletions clients/fides-js/src/components/tcf/TcfConsentButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,14 @@ export const TcfConsentButtons = ({
return null;
}

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;
const allIds: EnabledIds = {
allIds = {
purposesConsent: getAllIds(experience.tcf_purpose_consents),
purposesLegint: getAllIds(experience.tcf_purpose_legitimate_interests),
specialPurposes: getAllIds(experience.tcf_special_purposes),
Expand All @@ -56,22 +59,44 @@ export const TcfConsentButtons = ({
...(experience.tcf_system_legitimate_interests || []),
]),
};
onSave(ConsentMethod.ACCEPT, allIds);
} 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 = () => {
if (!experience.minimal_tcf) {
const emptyIds: EnabledIds = {
purposesConsent: [],
purposesLegint: [],
specialPurposes: [],
features: [],
specialFeatures: [],
vendorsConsent: [],
vendorsLegint: [],
};
onSave(ConsentMethod.REJECT, emptyIds);
}
const emptyIds: EnabledIds = {
purposesConsent: [],
purposesLegint: [],
specialPurposes: [],
features: [],
specialFeatures: [],
vendorsConsent: [],
vendorsLegint: [],
};
onSave(ConsentMethod.REJECT, emptyIds);
};

return (
Expand All @@ -84,7 +109,8 @@ export const TcfConsentButtons = ({
isInModal={isInModal}
options={options}
isTCF
disableAll={experience.minimal_tcf}
isMinimalTCF={experience.minimal_tcf}
isGVLLoading={isGVLLoading}
/>
);
};
31 changes: 23 additions & 8 deletions clients/fides-js/src/components/tcf/TcfOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ import {
import { useI18n } from "../../lib/i18n/i18n-context";
import { updateConsentPreferences } from "../../lib/preferences";
import { useGvl } from "../../lib/tcf/gvl-context";
import type { EnabledIds } from "../../lib/tcf/types";
import type { EnabledIds, TcfSavePreferences } from "../../lib/tcf/types";
import {
buildTcfEntitiesFromCookieAndFidesString as buildUserPrefs,
constructTCFNoticesServedProps,
createTcfSavePayload,
createTcfSavePayloadFromMinExp,
getEnabledIds,
getGVLPurposeList,
updateCookie,
Expand Down Expand Up @@ -245,17 +246,25 @@ export const TcfOverlay = ({

const handleUpdateAllPreferences = useCallback(
(consentMethod: ConsentMethod, enabledIds: EnabledIds) => {
if (!experience) {
if (!experience && !experienceMinimal) {
return;
}
const tcf = createTcfSavePayload({
experience,
enabledIds,
});
let tcf: TcfSavePreferences;
if (!experience && experienceMinimal?.minimal_tcf) {
tcf = createTcfSavePayloadFromMinExp({
experience: experienceMinimal,
enabledIds,
});
} else {
tcf = createTcfSavePayload({
experience: experience as PrivacyExperience,
enabledIds,
});
}
updateConsentPreferences({
consentPreferencesToSave: [],
privacyExperienceConfigHistoryId,
experience,
experience: experience || experienceMinimal,
consentMethod,
options,
userLocationString: fidesRegionString,
Expand All @@ -264,13 +273,19 @@ export const TcfOverlay = ({
tcf,
servedNoticeHistoryId,
updateCookie: (oldCookie) =>
updateCookie(oldCookie, tcf, enabledIds, experience),
updateCookie(
oldCookie,
tcf,
enabledIds,
experience || experienceMinimal,
),
});
setDraftIds(enabledIds);
},
[
cookie,
experience,
experienceMinimal,
fidesRegionString,
options,
privacyExperienceConfigHistoryId,
Expand Down
Loading

0 comments on commit a19d30b

Please sign in to comment.