Skip to content

Commit

Permalink
feat: Add final parameter to onConsentChange (#664)
Browse files Browse the repository at this point in the history
* Add final parameter to onConsent Callback.

* Update README

* Tests for final parameter in onConsentChange

* Add changeset

---------

Co-authored-by: Sue <sookburt@gmail.com>
Co-authored-by: Sue <sookburt@users.noreply.github.com>
  • Loading branch information
3 people committed Feb 13, 2024
1 parent 8b6e705 commit 8b3c987
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/breezy-oranges-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@guardian/consent-management-platform': minor
---

Adding extra final parameter onConsentChange
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ and TCFv2 to everyone else.
* [`cmp.willShowPrivacyMessageSync()`](#cmpwillshowprivacymessagesync)
* [`cmp.showPrivacyManager()`](#cmpshowprivacymanager)
- [Using Consent](#using-consent)
* [`onConsentChange(callback)`](#onconsentchangecallback)
* [`onConsentChange(callback, final?)`](#onconsentchangecallback-final)
* [`onConsent()`](#onconsent)
* [`getConsentFor(vendor, consentState)`](#getconsentforvendor-consentstate)
- [Disabling Consent](#disabling-consent)
Expand Down Expand Up @@ -172,7 +172,7 @@ import {
} from '@guardian/consent-management-platform';
```

### `onConsentChange(callback)`
### `onConsentChange(callback, final?)`

returns: `void`

Expand All @@ -184,6 +184,11 @@ An event listener that invokes callbacks whenever the consent state:
If the consent state has already been acquired when `onConsentChange` is called,
the callback will be invoked immediately.

Passing `true` for the optional `final` parameter guarantees that the callback
will be executed after all other callbacks that haven't been registered with the flag when consent state changes.
If more than one callback registered with `final = true`, they will be executed in the order in which they were registered
when consent changes.

#### `callback(consentState)`

type: `function`
Expand Down
169 changes: 169 additions & 0 deletions src/onConsentChange.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,66 @@ describe('under CCPA', () => {
expect(callback).toHaveBeenCalledTimes(2);
});
});


it('callbacks executed in correct order', async () => {
let callbackLastExecuted = {};
const setCallbackLastExecuted = (callback) => {
const now = window.performance.now();
callbackLastExecuted[callback] = now;
};
const callback1 = jest.fn(() => setCallbackLastExecuted(1));
const callback2 = jest.fn(() => setCallbackLastExecuted(2));
const callback3 = jest.fn(() => setCallbackLastExecuted(3));
const callback4 = jest.fn(() => setCallbackLastExecuted(4));

uspData.uspString = '1YYN';

// callback 3 and 4 registered first with final flag
onConsentChange(callback3, true);
onConsentChange(callback4, true);
onConsentChange(callback1);
onConsentChange(callback2);

await waitForExpect(() => {
expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledTimes(1);
expect(callback3).toHaveBeenCalledTimes(1);
expect(callback4).toHaveBeenCalledTimes(1);

// callbacks initially executed in order they were registered in
expect(callbackLastExecuted[3]).toBeLessThan(
callbackLastExecuted[4],
);
expect(callbackLastExecuted[4]).toBeLessThan(
callbackLastExecuted[1],
);
expect(callbackLastExecuted[1]).toBeLessThan(
callbackLastExecuted[2],
);
});

uspData.uspString = '1YNN';
invokeCallbacks();

await waitForExpect(() => {
expect(callback1).toHaveBeenCalledTimes(2);
expect(callback2).toHaveBeenCalledTimes(2);
expect(callback3).toHaveBeenCalledTimes(2);
expect(callback4).toHaveBeenCalledTimes(2);

// after consent state change, callbacks were executed in order 1, 2, 3, 4
expect(callbackLastExecuted[1]).toBeLessThan(
callbackLastExecuted[2],
);
expect(callbackLastExecuted[2]).toBeLessThan(
callbackLastExecuted[3],
);
expect(callbackLastExecuted[3]).toBeLessThan(
callbackLastExecuted[4],
);
});
});
});

describe('under AUS', () => {
Expand Down Expand Up @@ -129,6 +189,66 @@ describe('under AUS', () => {
expect(callback).toHaveBeenCalledTimes(2);
});
});


it('callbacks executed in correct order', async () => {
let callbackLastExecuted = {};
const setCallbackLastExecuted = (callback) => {
const now = window.performance.now();
callbackLastExecuted[callback] = now;
};
const callback1 = jest.fn(() => setCallbackLastExecuted(1));
const callback2 = jest.fn(() => setCallbackLastExecuted(2));
const callback3 = jest.fn(() => setCallbackLastExecuted(3));
const callback4 = jest.fn(() => setCallbackLastExecuted(4));

ausData.uspString = '1YYN';

// callback 3 and 4 registered first with final flag
onConsentChange(callback3, true);
onConsentChange(callback4, true);
onConsentChange(callback1);
onConsentChange(callback2);

await waitForExpect(() => {
expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledTimes(1);
expect(callback3).toHaveBeenCalledTimes(1);
expect(callback4).toHaveBeenCalledTimes(1);

// callbacks initially executed in order they were registered in
expect(callbackLastExecuted[3]).toBeLessThan(
callbackLastExecuted[4],
);
expect(callbackLastExecuted[4]).toBeLessThan(
callbackLastExecuted[1],
);
expect(callbackLastExecuted[1]).toBeLessThan(
callbackLastExecuted[2],
);
});

ausData.uspString = '1YNN';
invokeCallbacks();

await waitForExpect(() => {
expect(callback1).toHaveBeenCalledTimes(2);
expect(callback2).toHaveBeenCalledTimes(2);
expect(callback3).toHaveBeenCalledTimes(2);
expect(callback4).toHaveBeenCalledTimes(2);

// after consent state change, callbacks were executed in order 1, 2, 3, 4
expect(callbackLastExecuted[1]).toBeLessThan(
callbackLastExecuted[2],
);
expect(callbackLastExecuted[2]).toBeLessThan(
callbackLastExecuted[3],
);
expect(callbackLastExecuted[3]).toBeLessThan(
callbackLastExecuted[4],
);
});
});
});

describe('under TCFv2', () => {
Expand Down Expand Up @@ -216,4 +336,53 @@ describe('under TCFv2', () => {
expect(callback).toHaveBeenCalledTimes(2);
});
});

it('callbacks executed in correct order', async () => {
let callbackLastExecuted = {};
const setCallbackLastExecuted = (callback) => {
const now = window.performance.now();
callbackLastExecuted[callback] = now;
};
const callback1 = jest.fn(() => setCallbackLastExecuted(1));
const callback2 = jest.fn(() => setCallbackLastExecuted(2));
const callback3 = jest.fn(() => setCallbackLastExecuted(3));
const callback4 = jest.fn(() => setCallbackLastExecuted(4));

tcData.eventStatus = 'cmpuishown';

// callback 3 and 4 registered first with final flag
onConsentChange(callback3, true);
onConsentChange(callback4, true);
onConsentChange(callback1);
onConsentChange(callback2);

await waitForExpect(() => {
expect(callback1).toHaveBeenCalledTimes(0);
expect(callback2).toHaveBeenCalledTimes(0);
expect(callback3).toHaveBeenCalledTimes(0);
expect(callback4).toHaveBeenCalledTimes(0);
});

tcData.eventStatus = 'useractioncomplete';

invokeCallbacks();

await waitForExpect(() => {
expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledTimes(1);
expect(callback3).toHaveBeenCalledTimes(1);
expect(callback4).toHaveBeenCalledTimes(1);

// callbacks were executed in order 1, 2, 3, 4
expect(callbackLastExecuted[1]).toBeLessThan(
callbackLastExecuted[2],
);
expect(callbackLastExecuted[2]).toBeLessThan(
callbackLastExecuted[3],
);
expect(callbackLastExecuted[3]).toBeLessThan(
callbackLastExecuted[4],
);
});
});
});
14 changes: 10 additions & 4 deletions src/onConsentChange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface ConsentStateBasic {

// callbacks cache
const callBackQueue: CallbackQueueItem[] = [];
const finalCallbackQueue: CallbackQueueItem[] = [];

/**
* In TCFv2, check whether the event status anything but `cmpuishown`, i.e.:
Expand Down Expand Up @@ -98,18 +99,23 @@ const getConsentState: () => Promise<ConsentState> = async () => {

// invokes all stored callbacks with the current consent state
export const invokeCallbacks = (): void => {
if (callBackQueue.length === 0) return;
const callbacksToInvoke = callBackQueue.concat(finalCallbackQueue)
if (callbacksToInvoke.length === 0) return;
void getConsentState().then((state) => {
if (awaitingUserInteractionInTCFv2(state)) return;

callBackQueue.forEach((callback) => invokeCallback(callback, state));
callbacksToInvoke.forEach((callback) => invokeCallback(callback, state));
});
};

export const onConsentChange: OnConsentChange = (callBack) => {
export const onConsentChange: OnConsentChange = (callBack, final = false) => {
const newCallback: CallbackQueueItem = { fn: callBack };

callBackQueue.push(newCallback);
if (final) {
finalCallbackQueue.push(newCallback);
} else {
callBackQueue.push(newCallback)
}

// if consentState is already available, invoke callback immediately
void getConsentState()
Expand Down
2 changes: 1 addition & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type CMP = {

export type InitCMP = (arg0: { pubData?: PubData; country?: Country }) => void;

export type OnConsentChange = (fn: Callback) => void;
export type OnConsentChange = (fn: Callback, final?: boolean) => void;
export type GetConsentFor = (
vendor: VendorName,
consent: ConsentState,
Expand Down

1 comment on commit 8b3c987

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report

St.
Category Percentage Covered / Total
🟢 Statements 92.81% 258/278
🟢 Branches 83.9% 99/118
🟢 Functions 90% 63/70
🟢 Lines 92.59% 250/270

Test suite run success

331 tests passing in 16 suites.

Report generated by 🧪jest coverage report action from 8b3c987

Please sign in to comment.