Skip to content

Commit

Permalink
Merge pull request #18 from coinbase/support-initial-vals
Browse files Browse the repository at this point in the history
Wired up initial values support
  • Loading branch information
joshuaostrom-cb authored Jun 12, 2024
2 parents 921ed08 + 1a94372 commit c27a9a2
Show file tree
Hide file tree
Showing 13 changed files with 95 additions and 29 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ cd /path/to/coinbase/cb-cookie-manager/example/app
yarn dev
```

## Testing

yarn test

## Packages

- `@coinbase/cookie-manager`: Package that helps with managing first party client side cookies to adhere to CCPA and GDPR Cookie regulations. More information [here](./packages/cookie-manager/README.md)
Expand Down
2 changes: 1 addition & 1 deletion apps/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"dependencies": {
"@coinbase/cookie-banner": "1.0.4",
"@coinbase/cookie-manager": "1.1.4",
"@coinbase/cookie-manager": "1.1.5",
"next": "14.1.1",
"react": "^18",
"react-dom": "^18"
Expand Down
2 changes: 1 addition & 1 deletion packages/cookie-banner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"react-dom": "^18.1.0"
},
"dependencies": {
"@coinbase/cookie-manager": "^1.1.4",
"@coinbase/cookie-manager": "^1.1.5",
"react-intl": "^6.5.1",
"styled-components": "^5.3.6"
}
Expand Down
4 changes: 4 additions & 0 deletions packages/cookie-manager/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 1.1.5 (06/12/2024)

- Added support for initialCookieValues, initialGPCValue

## 1.1.4 (06/11/2024)

- Updated next version from 14.0.0 to 14.1.1
Expand Down
4 changes: 4 additions & 0 deletions packages/cookie-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ The provider must wrap the entire application and only be instantiated once. On

`log: (str: string, options?: Record<string, any>) => void`: Log function

`initialCookieValues?:Record<string, string> `: Useful for server side rendering flows - setting of initial cookie values

`initialGPCValue?:boolean`: Useful for server side rendering flows - honoring of Set-GPC header

Example usage:

```typescript
Expand Down
2 changes: 1 addition & 1 deletion packages/cookie-manager/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coinbase/cookie-manager",
"version": "1.1.4",
"version": "1.1.5",
"description": "Coinbase Cookie Manager",
"main": "dist/index.js",
"license": "Apache-2.0",
Expand Down
34 changes: 23 additions & 11 deletions packages/cookie-manager/src/CookieContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,22 @@ type Props = {
};

export const CookieProvider = ({ children }: Props) => {
const { config, region, shadowMode, log, onPreferenceChange } = useTrackingManager();
const {
config,
region,
shadowMode,
log,
onPreferenceChange,
initialCookieValues,
initialGPCValue,
} = useTrackingManager();

const POLL_INTERVAL = 500;
const [cookieValues, setCookieValues] = useState(() => getAllCookies(region));
let priorCookieValue: Record<string, any>;
let trackingPreference: TrackingPreference;
let adTrackingPreference: AdTrackingPreference;
const gpc = initialGPCValue || false;

const removeCookies = useCallback(
(cookies: string[]) => {
Expand All @@ -62,17 +71,18 @@ export const CookieProvider = ({ children }: Props) => {
);

useEffect(() => {
// TODO clean up hydration
if (typeof window !== 'undefined') {
const checkCookies = () => {
const currentCookie = getAllCookies(region);
const currentCookie = getAllCookies(region, initialCookieValues);

if (priorCookieValue == undefined || !areRecordsEqual(priorCookieValue, currentCookie)) {
priorCookieValue = currentCookie;
setCookieValues(currentCookie);

// Grab out prefences (they wil have GPC applied if present)
trackingPreference = getTrackingPreference(currentCookie, region, config);
adTrackingPreference = getAdTrackingPreference(currentCookie, region);
// Grab out prefences (they will have GPC applied if present)
trackingPreference = getTrackingPreference(currentCookie, region, config, gpc);
adTrackingPreference = getAdTrackingPreference(currentCookie, region, gpc);

setGTMVariables(trackingPreference, adTrackingPreference);
const cookiesToRemove: Array<string> = [];
Expand Down Expand Up @@ -197,7 +207,8 @@ const setCookieFunction = ({
const getTrackingPreference = (
cookieCache: Record<string, any>,
region: Region,
config: Config
config: Config,
gpcDefault?: boolean
): TrackingPreference => {
const trackingPreference =
region === Region.EU
Expand All @@ -208,27 +219,28 @@ const getTrackingPreference = (
// { region: Region.EU, consent: ['necessary'] }
const preference = trackingPreference || getDefaultTrackingPreference(region, config);
// Apply GPC when present
return applyGpcToCookiePref(preference);
return applyGpcToCookiePref(preference, gpcDefault || false);
};

// Do we want to change the ADVERTISING_SHARING_ALLOWED value to clear prior values?
const getAdTrackingPreference = (
cookieCache: Record<string, any>,
region: Region
region: Region,
gpcHeader?: boolean
): AdTrackingPreference => {
const adTrackingPreference = cookieCache[ADVERTISING_SHARING_ALLOWED];

const adTrackingDefault = region === Region.EU ? { value: 'false' } : { value: 'true' };

// Example: adPreference { value: 'false' }
const adPreference = adTrackingPreference || adTrackingDefault;
return applyGpcToAdPref(region, adPreference);
return applyGpcToAdPref(region, adPreference, gpcHeader || false);
};

export const useCookie = (cookieName: string): [any | undefined, SetCookieFunction] => {
const cookieCache = useContext(CookieContext);
const { config, region, log, shadowMode, onError } = useTrackingManager();
const trackingPreference = getTrackingPreference(cookieCache, region, config);
const { config, region, log, shadowMode, onError, initialGPCValue } = useTrackingManager();
const trackingPreference = getTrackingPreference(cookieCache, region, config, initialGPCValue);
const setCookie = setCookieFunction({
cookieName,
trackingPreference,
Expand Down
2 changes: 2 additions & 0 deletions packages/cookie-manager/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export type TrackingManagerDependencies = {
config: Config;
shadowMode?: boolean;
log: LogFunction;
initialCookieValues?: Record<string, string>;
initialGPCValue?: boolean;
};

export type AdTrackingPreference = {
Expand Down
15 changes: 7 additions & 8 deletions packages/cookie-manager/src/utils/applyGpcToAdPref.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { AdTrackingPreference, Region } from '../types';
import getGpc from './getGpc';

const applyGpcToAdPref = (
region: Region,
preference: AdTrackingPreference
preference: AdTrackingPreference,
gpcHeader?: boolean
): AdTrackingPreference => {
// We are only applying GPC in non-EU countries at this point
if (region == Region.EU) {
return preference;
}

if (typeof window === 'undefined' || typeof window.navigator === 'undefined') {
return preference;
}

// If we lack GPC or it's set ot false we are done
if (!(window.navigator as any).globalPrivacyControl) {
// If the browser is has global privacy control enabled
// we will honor it
const gpc = getGpc(gpcHeader);
if (!gpc) {
return preference;
}

Expand Down
16 changes: 9 additions & 7 deletions packages/cookie-manager/src/utils/applyGpcToCookiePref.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { Region, TrackingCategory, TrackingPreference } from '../types';
import getGpc from './getGpc';
// { region: Region.DEFAULT, consent: ['necessary', 'performance', 'functional', 'targeting'] }
const applyGpcToCookiePref = (preference: TrackingPreference): TrackingPreference => {
const applyGpcToCookiePref = (
preference: TrackingPreference,
gpcHeader?: boolean
): TrackingPreference => {
// We are only applying GPC in non-EU countries at this point
if (preference.region == Region.EU) {
return preference;
}

// TODO: We want to support server side render flows
// where the user can set an initial value and indicate that gpc has been enabled
if (typeof window === 'undefined' || typeof window.navigator === 'undefined') {
// If the browser is has global privacy control enabled
// we will honor it
const gpc = getGpc(gpcHeader);
if (!gpc) {
return preference;
}

if (!(window.navigator as any).globalPrivacyControl) {
return preference;
}
// If the user had opted in to GPC we want to honor it
const categories = preference.consent.filter((cat) => cat !== TrackingCategory.TARGETING);

Expand Down
1 change: 1 addition & 0 deletions packages/cookie-manager/src/utils/getAllCookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const deserializeCookies = (region: Region, cookies: Record<string, strin
return parsedCookies;
};

// TODO clean up hydration
export default function getAllCookies(region: Region, initialCookies?: Record<string, string>) {
if (typeof window === 'undefined' && initialCookies) {
return deserializeCookies(region, initialCookies);
Expand Down
17 changes: 17 additions & 0 deletions packages/cookie-manager/src/utils/getGpc.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import getGpc from './getGpc';

describe('getGpc', () => {
it('honors navigator.globalPrivacyControl', () => {
(navigator as any).globalPrivacyControl = true;
expect(getGpc()).toEqual(true);
});

it('honors the passed header value when passed', () => {
(navigator as any).globalPrivacyControl = false;
expect(getGpc(true)).toEqual(true);
});

it('returns false by default', () => {
expect(getGpc()).toEqual(false);
});
});
21 changes: 21 additions & 0 deletions packages/cookie-manager/src/utils/getGpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export function getGpc(gpcHeaderValue?: boolean): boolean {
const header = gpcHeaderValue == null ? false : gpcHeaderValue;

if (header) {
// honor the Set-GPC header if it's set to true
return true;
}

if (typeof window === 'undefined' || typeof window.navigator === 'undefined') {
// if we don't have access to the window.navigator return the header value
// if present, false otherwise
return header;
}

if (!(window.navigator as any).globalPrivacyControl) {
return false;
}
return true;
}

export default getGpc;

0 comments on commit c27a9a2

Please sign in to comment.