Skip to content
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

✨ [amp-consent] Sync and store purposeConsents in localStorage #32721

Merged
merged 4 commits into from
Feb 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
83 changes: 63 additions & 20 deletions extensions/amp-consent/0.1/amp-consent.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
resolveRelativeUrl,
} from '../../../src/url';
import {dev, devAssert, user, userAssert} from '../../../src/log';
import {dict} from '../../../src/utils/object';
import {dict, hasOwn} from '../../../src/utils/object';
import {getData} from '../../../src/event-helper';
import {getServicePromiseForDoc} from '../../../src/service';
import {isArray, isEnumValue, isObject} from '../../../src/types';
Expand Down Expand Up @@ -381,8 +381,12 @@ export class AmpConsent extends AMP.BaseElement {
) {
continue;
}
if (purposeConsents && action !== ACTION_TYPE.DISMISS) {
this.validateSetPurposeArgs_(purposeConsents);
if (
purposeConsents &&
Object.keys(purposeConsents).length &&
action !== ACTION_TYPE.DISMISS
) {
this.validatePurposeConsents_(purposeConsents);
this.consentStateManager_.updateConsentInstancePurposes(
purposeConsents
);
Expand Down Expand Up @@ -565,13 +569,17 @@ export class AmpConsent extends AMP.BaseElement {
* @param {!../../../src/service/action-impl.ActionInvocation} invocation
*/
handleSetPurpose_(invocation) {
if (!invocation || !invocation['args']) {
if (
!invocation ||
!invocation['args'] ||
!Object.keys(invocation['args']).length
) {
dev().error(TAG, 'Must have arugments for `setPurpose`.');
return;
}
const {args} = invocation;
if (this.isReadyToHandleAction_()) {
this.validateSetPurposeArgs_(args);
this.validatePurposeConsents_(args);
this.consentStateManager_.updateConsentInstancePurposes(args);
}
}
Expand Down Expand Up @@ -667,7 +675,8 @@ export class AmpConsent extends AMP.BaseElement {
this.updateCacheIfNotNull_(
response['consentStateValue'],
response['consentString'] || undefined,
response['consentMetadata'] || undefined
response['consentMetadata'],
response['purposeConsents']
);
}
});
Expand All @@ -676,22 +685,35 @@ export class AmpConsent extends AMP.BaseElement {
/**
* Sync with local storage if consentRequired is true.
*
* @param {string=} responseStateValue
* @param {string=} responseConsentString
* @param {JsonObject=} opt_responseMetadata
* @param {?string=} responseStateValue
* @param {?string=} responseConsentString
* @param {?JsonObject=} responseMetadata
* @param {?JsonObject=} responsePurposeConsents
*/
updateCacheIfNotNull_(
responseStateValue,
responseConsentString,
opt_responseMetadata
responseMetadata,
responsePurposeConsents
) {
const consentStateValue = convertEnumValueToState(responseStateValue);
// consentStateValue and consentString are treated as a pair that will update together
if (consentStateValue !== null) {
if (
this.isGranularConsentExperimentOn_ &&
responsePurposeConsents &&
isObject(responsePurposeConsents) &&
Object.keys(responsePurposeConsents).length
) {
this.validatePurposeConsents_(responsePurposeConsents);
this.consentStateManager_.updateConsentInstancePurposes(
responsePurposeConsents
);
}
this.consentStateManager_.updateConsentInstanceState(
consentStateValue,
responseConsentString,
this.validateMetadata_(opt_responseMetadata)
this.validateMetadata_(responseMetadata)
);
}
}
Expand Down Expand Up @@ -719,6 +741,9 @@ export class AmpConsent extends AMP.BaseElement {
'isDirty': !!storedInfo['isDirty'],
'matchedGeoGroup': this.matchedGeoGroup_,
});
if (this.isGranularConsentExperimentOn_) {
request['purposeConsents'] = storedInfo['purposeConsents'];
}
if (this.consentConfig_['clientConfig']) {
request['clientConfig'] = this.consentConfig_['clientConfig'];
}
Expand Down Expand Up @@ -755,20 +780,36 @@ export class AmpConsent extends AMP.BaseElement {
}

/**
* TODO (micajuineho): Use our stored info to check if we have the
* necessary granular consents.
* @param {ConsentInfoDef} unusedConsentInfo
* Returns true if we have stored the granular consent values
* for the required purposes.
* @param {ConsentInfoDef} consentInfo
* @return {!Promise<boolean>}
*/
checkGranularConsentRequired_(unusedConsentInfo) {
checkGranularConsentRequired_(consentInfo) {
if (!this.isGranularConsentExperimentOn_) {
return Promise.resolve(true);
}
return this.getPurposeConsentRequired_().then((purposeConsentRequired) => {
if (!purposeConsentRequired) {
// True if there are no required purposes
if (!purposeConsentRequired || !purposeConsentRequired.length) {
return true;
}
// TODO: add check here.
const storedPurposeConsents = consentInfo['purposeConsents'];
// False if there are no stored purposes
if (
!storedPurposeConsents ||
Object.keys(storedPurposeConsents).length <
purposeConsentRequired.length
) {
return false;
}
// Check if we have a stored consent for each purpose required
for (let i = 0; i < purposeConsentRequired.length; i++) {
const purpose = purposeConsentRequired[i];
if (!hasOwn(storedPurposeConsents, purpose)) {
return false;
}
}
return true;
});
}
Expand Down Expand Up @@ -804,8 +845,9 @@ export class AmpConsent extends AMP.BaseElement {
*/
hasRequiredConsents_() {
return this.consentStateManager_.getConsentInstanceInfo().then((info) => {
// Global consent
// Check that we have global consent
if (hasStoredValue(info)) {
// Then check granular consent
return this.checkGranularConsentRequired_(info);
}
return Promise.resolve(false);
Expand Down Expand Up @@ -897,10 +939,11 @@ export class AmpConsent extends AMP.BaseElement {
}

/**
* Ensure setPurpose argument is valid.
* Ensure purpose consents to be set are valid.
*
* @param {!Object} purposeObj
*/
validateSetPurposeArgs_(purposeObj) {
validatePurposeConsents_(purposeObj) {
const purposeKeys = Object.keys(purposeObj);
purposeKeys.forEach((purposeKey) => {
dev().assertBoolean(
Expand Down
36 changes: 27 additions & 9 deletions extensions/amp-consent/0.1/consent-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,19 @@ const TAG = 'amp-consent';
* STATE: Set when user accept or reject consent.
* STRING: Set when a consent string is used to store more granular consent info
* on vendors.
* METADATA: set when consent metadata is passed in to store more granular consent info
* METADATA: Set when consent metadata is passed in to store more granular consent info
* on vendors.
* DITRYBIT: Set when the stored consent info need to be revoked next time.
* PURPOSE_CONSENTS: Set when consents for purposes are passed in for client side
* granular consent. Only values ACCEPT and REJECT signals are stored.
* @enum {string}
*/
export const STORAGE_KEY = {
STATE: 's',
STRING: 'r',
IS_DIRTY: 'd',
METADATA: 'm',
PURPOSE_CONSENTS: 'pc',
};

/**
Expand All @@ -52,7 +55,8 @@ export const METADATA_STORAGE_KEY = {

/**
* Unlike the global consent state, only accepted and
* rejected values are respected.
* rejected values are respected and stored.
* In the future, we might consider more nuanced states.
* @enum {number}
*/
export const PURPOSE_CONSENT_STATE = {
Expand Down Expand Up @@ -90,6 +94,7 @@ export const TCF_POST_MESSAGE_API_COMMANDS = {
* consentState: CONSENT_ITEM_STATE,
* consentString: (string|undefined),
* consentMetadata: (ConsentMetadataDef|undefined),
* purposeConsents: (Object<string, PURPOSE_CONSENT_STATE>|undefined),
* isDirty: (boolean|undefined),
* }}
*/
Expand All @@ -113,12 +118,7 @@ export let ConsentMetadataDef;
*/
export function getStoredConsentInfo(value) {
if (value === undefined) {
return constructConsentInfo(
CONSENT_ITEM_STATE.UNKNOWN,
undefined,
undefined,
undefined
);
return constructConsentInfo(CONSENT_ITEM_STATE.UNKNOWN);
}
if (typeof value === 'boolean') {
// legacy format
Expand All @@ -133,6 +133,7 @@ export function getStoredConsentInfo(value) {
consentState,
value[STORAGE_KEY.STRING],
convertStorageMetadata(value[STORAGE_KEY.METADATA]),
value[STORAGE_KEY.PURPOSE_CONSENTS],
value[STORAGE_KEY.IS_DIRTY] && value[STORAGE_KEY.IS_DIRTY] === 1
);
}
Expand Down Expand Up @@ -204,6 +205,10 @@ export function composeStoreValue(consentInfo) {
);
}

if (consentInfo['purposeConsents']) {
obj[STORAGE_KEY.PURPOSE_CONSENTS] = consentInfo['purposeConsents'];
}

if (Object.keys(obj) == 0) {
return null;
}
Expand Down Expand Up @@ -254,7 +259,17 @@ export function isConsentInfoStoredValueSame(infoA, infoB, opt_isDirty) {
infoA['consentMetadata'],
infoB['consentMetadata']
);
return stateEqual && stringEqual && metadataEqual && isDirtyEqual;
const purposeConsentsEqual = deepEquals(
infoA['purposeConsents'],
infoB['purposeConsents']
);
return (
stateEqual &&
stringEqual &&
metadataEqual &&
purposeConsentsEqual &&
isDirtyEqual
);
}
return false;
}
Expand All @@ -275,19 +290,22 @@ function getLegacyStoredConsentInfo(value) {
* @param {CONSENT_ITEM_STATE} consentState
* @param {string=} opt_consentString
* @param {ConsentMetadataDef=} opt_consentMetadata
* @param {Object<string, PURPOSE_CONSENT_STATE>=} opt_purposeConsents
* @param {boolean=} opt_isDirty
* @return {!ConsentInfoDef}
*/
export function constructConsentInfo(
consentState,
opt_consentString,
opt_consentMetadata,
opt_purposeConsents,
opt_isDirty
) {
return {
'consentState': consentState,
'consentString': opt_consentString,
'consentMetadata': opt_consentMetadata,
'purposeConsents': opt_purposeConsents,
'isDirty': opt_isDirty,
};
}
Expand Down