-
Notifications
You must be signed in to change notification settings - Fork 359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ability to detect when a user has changed their consent string via manually triggering CMP #348
Comments
I've been struggling for quite few hours and I did a React hook to handle this situation. IMHO this whole GDPR standard is very complicated with lot's of things to understand and to realize if the user has consented or not through specific topics. Here's what I've done. I am using
export type TCData = {
tcString: string;
tcfPolicyVersion: number;
cmpId: number;
cmpVersion: number;
gdprApplies: boolean | undefined;
eventStatus: 'tcloaded' | 'cmpuishown' | 'useractioncomplete';
cmpStatus: string;
listenerId: number | undefined;
isServiceSpecific: boolean;
useNonStandardTexts: boolean;
publisherCC: string;
purposeOneTreatment: boolean;
purpose: {
consents: {
[key: string]: boolean;
};
legitimateInterests: {
[key: string]: boolean;
};
};
vendor: {
consents: {
[key: string]: boolean;
};
legitimateInterests: {
[key: string]: boolean;
};
};
specialFeatureOptins: {
[key: string]: boolean;
};
publisher: {
consents: {
[key: string]: boolean;
};
legitimateInterests: {
[key: string]: boolean;
};
customPurpose: {
consents: {
[key: string]: boolean;
};
legitimateInterests: {
[key: string]: boolean;
};
};
restrictions: {
[key: string]: {
[key: string]: 0 | 1 | 2;
};
};
};
};
export type NonIABVendorsConsents = {
gdprApplies: boolean;
metadata: string;
nonIabVendorConsents: Record<number, boolean>;
};
import { NonIABVendorsConsents, TCData } from '@/types/tcfapi';
// https://vendor-list.consensu.org/v2/vendor-list.json
export class ConsentParser {
constructor(
private consentData: TCData,
private nonIABVendors: NonIABVendorsConsents['nonIabVendorConsents'],
) {}
private getConsent(purposeId: string): boolean {
return this.consentData.purpose.consents[purposeId] ?? false;
}
private getLegitimateInterest(purposeId: string): boolean {
return this.consentData.purpose.legitimateInterests[purposeId] ?? false;
}
canSaveCookies(): boolean {
return this.getConsent('1');
}
canSelectBasicAds(): boolean {
return this.getConsent('2');
}
canCreatePersonalisedAdsProfile(): boolean {
return this.getConsent('3');
}
canSelectPersonalisedAds(): boolean {
return this.getConsent('4');
}
canCreatePersonalisedContentProfile(): boolean {
return this.getConsent('5');
}
canSelectPersonalisedContent(): boolean {
return this.getConsent('6');
}
canMeasureAdPerformance(): boolean {
return this.getConsent('7');
}
canMeasureContentPerformance(): boolean {
return this.getConsent('8');
}
canApplyMarketResearch(): boolean {
return this.getConsent('9');
}
canDevelopAndImproveProducts(): boolean {
return this.getConsent('10');
}
hasLegitimateInterestForStorageAccess(): boolean {
return this.getLegitimateInterest('1');
}
hasLegitimateInterestForBasicAdSelection(): boolean {
return this.getLegitimateInterest('2');
}
hasLegitimateInterestForCreatingPersonalisedAdsProfile(): boolean {
return this.getLegitimateInterest('3');
}
hasLegitimateInterestForSelectingPersonalisedAds(): boolean {
return this.getLegitimateInterest('4');
}
hasLegitimateInterestForCreatingPersonalisedContentProfile(): boolean {
return this.getLegitimateInterest('5');
}
hasLegitimateInterestForSelectingPersonalisedContent(): boolean {
return this.getLegitimateInterest('6');
}
hasLegitimateInterestForAdMeasurement(): boolean {
return this.getLegitimateInterest('7');
}
hasLegitimateInterestForContentMeasurement(): boolean {
return this.getLegitimateInterest('8');
}
hasLegitimateInterestForMarketResearch(): boolean {
return this.getLegitimateInterest('9');
}
hasLegitimateInterestForProductDevelopment(): boolean {
return this.getLegitimateInterest('10');
}
hasOptedInForPreciseGeolocationData(): boolean {
return this.consentData.specialFeatureOptins['1'] ?? false;
}
hasOptedInForActiveDeviceScanning(): boolean {
return this.consentData.specialFeatureOptins['2'] ?? false;
}
}
'use client';
import { useCallback, useEffect, useState } from 'react';
import { useInterval } from '@mantine/hooks';
import { NonIABVendorsConsents, TCData } from './tcfapi';
import { ConsentParser } from './consent-parser';
/**
* It will get the defaults tcData properties
* @see https://vendor-list.consensu.org/v2/vendor-list.json
*/
const useTCFAPI = () => {
const [isLoading, setIsLoading] = useState(true);
const [data, setData] = useState<TCData>();
const [consentManager, setConsentManager] = useState<ConsentParser>();
const [tcStatus, setTcStatus] = useState<TCData['eventStatus'] | undefined>();
const [nonIABVendors, setNonIABVendors] =
useState<NonIABVendorsConsents['nonIabVendorConsents']>();
const [tcfAPI, setTcfAPI] = useState<any>();
const { start, active, stop } = useInterval(() => {
if (
typeof window !== 'undefined' ||
typeof (window as any).__tcfapi === 'function'
) {
setTcfAPI(() => (window as any).__tcfapi);
}
}, 100);
// Check if the window object is present in document
useEffect(() => {
start();
return () => {
stop();
};
}, []);
const _handleGetNonIABVendorConsents = useCallback(
(nonIabConsent: NonIABVendorsConsents, nonIabSuccess: boolean) => {
nonIabSuccess && setNonIABVendors(nonIabConsent.nonIabVendorConsents);
},
[],
);
/**
* @see https://help.quantcast.com/hc/en-us/articles/13422592233371-Choice-CMP2-CCPA-API-Index-
*/
useEffect(() => {
if (!tcfAPI) return;
if (active) {
stop();
}
tcfAPI('addEventListener', 2, (tcData: TCData, success: boolean) => {
success && setData(tcData);
tcfAPI('getNonIABVendorConsents', 2, _handleGetNonIABVendorConsents);
setTcStatus(tcData.eventStatus);
setIsLoading(false);
});
}, [tcfAPI, active]);
/**
* @see https://help.quantcast.com/hc/en-us/articles/13422592233371-Choice-CMP2-CCPA-API-Index-
*/
useEffect(() => {
if (!tcStatus) return;
tcfAPI(
'addEventListener',
2,
({ eventStatus }: TCData, success: boolean) => {
if (
success &&
(eventStatus === 'useractioncomplete' || eventStatus === 'tcloaded')
) {
tcfAPI('getNonIABVendorConsents', 2, _handleGetNonIABVendorConsents);
}
},
);
}, [tcStatus]);
useEffect(() => {
if (!data || !nonIABVendors) return;
setConsentManager(new ConsentParser(data, nonIABVendors));
}, [data, nonIABVendors]);
return { isLoading, consentManager, nonIABVendors };
};
export default useTCFAPI; It took me the whole night to understand and to adapt this convention but I am very satisfied with the approach. I will not follow any support by my script because it has some changes to attend my needs, but that's what I've done. |
@rforster-dev Is what you are requesting something like an event called tcStringHasChanged? Currently the way to do this is how you described it. We can consider adding a new event into the eventhander if this helps further. |
@HeinzBaumann - Yes sort of - we've got a couple of scenarios that are similar to this. 1: Has the user actually consented yet?
Which makes it difficult to understand when a user has actually consented or not. SourcePoint, a CMP handles this by using localStorage and in their kvp, they have a value of We are using this as as stopgap until we can do this natively via the 2: Whether or not a consent string has changed Hope this helps! |
@rforster-dev We discussed this at the TCF Framework Signal Working Group meeting (the tech side of the TCF body). It wasn't clear from reviewing this what use case this addresses. The vendor always has to check the content of the TCString after a notification has been fired e.g. do I as vendor with id xy have consent, what are the values of the purposes flags that I need to be aware of, can I operate? The groups understanding is that this can all be done today with the existing event listener and the different APIs. |
Thanks for responding - apologies I haven't said anything back since. Appreciate the groups time in reviewing this. OK, A question that maybe i'm lacking understanding in or guidance; in the scenario of:
What is the best way to determine this, factoring in:
Does that make sense? Appreciate I might not be wording it particularly great! |
If the event listener eventStatus is equal to "useractioncomplete", and the purpose object is empty the user rejected all. |
One thing that would be useful that I haven't seen (maybe i'm not looking hard enough!) is:
A combination of
cmpuishown
anduseractioncomplete
alone doesn't seem to be enough.I've done a work around which:
cmpuishown
useractioncomplete
if the tcString is now different, then do something (in our case, reload the page).Is this something that could be worked in as a useful event listener?
The text was updated successfully, but these errors were encountered: