Skip to content

Commit

Permalink
✨ [amp-consent] Sync and store purposeConsents in localStorage (#32721
Browse files Browse the repository at this point in the history
)

* init store

* nits
  • Loading branch information
Micajuine Ho committed Feb 26, 2021
1 parent 3044bc0 commit 1ee05d6
Show file tree
Hide file tree
Showing 8 changed files with 729 additions and 251 deletions.
83 changes: 63 additions & 20 deletions extensions/amp-consent/0.1/amp-consent.js
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
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

0 comments on commit 1ee05d6

Please sign in to comment.