diff --git a/README.md b/README.md index ed68d1d..296bb6f 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/apps/example/package.json b/apps/example/package.json index d4216dc..2bfbbdd 100644 --- a/apps/example/package.json +++ b/apps/example/package.json @@ -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" diff --git a/packages/cookie-banner/package.json b/packages/cookie-banner/package.json index 907b138..cd19f79 100644 --- a/packages/cookie-banner/package.json +++ b/packages/cookie-banner/package.json @@ -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" } diff --git a/packages/cookie-manager/CHANGELOG.md b/packages/cookie-manager/CHANGELOG.md index c0556f8..e247f21 100644 --- a/packages/cookie-manager/CHANGELOG.md +++ b/packages/cookie-manager/CHANGELOG.md @@ -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 diff --git a/packages/cookie-manager/README.md b/packages/cookie-manager/README.md index 04db9eb..12ea5cc 100644 --- a/packages/cookie-manager/README.md +++ b/packages/cookie-manager/README.md @@ -214,6 +214,10 @@ The provider must wrap the entire application and only be instantiated once. On `log: (str: string, options?: Record) => void`: Log function +`initialCookieValues?:Record `: 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 diff --git a/packages/cookie-manager/package.json b/packages/cookie-manager/package.json index a74f878..a133271 100644 --- a/packages/cookie-manager/package.json +++ b/packages/cookie-manager/package.json @@ -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", diff --git a/packages/cookie-manager/src/CookieContext.tsx b/packages/cookie-manager/src/CookieContext.tsx index 5ac61c3..466a8dc 100644 --- a/packages/cookie-manager/src/CookieContext.tsx +++ b/packages/cookie-manager/src/CookieContext.tsx @@ -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; let trackingPreference: TrackingPreference; let adTrackingPreference: AdTrackingPreference; + const gpc = initialGPCValue || false; const removeCookies = useCallback( (cookies: string[]) => { @@ -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 = []; @@ -197,7 +207,8 @@ const setCookieFunction = ({ const getTrackingPreference = ( cookieCache: Record, region: Region, - config: Config + config: Config, + gpcDefault?: boolean ): TrackingPreference => { const trackingPreference = region === Region.EU @@ -208,13 +219,14 @@ 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, - region: Region + region: Region, + gpcHeader?: boolean ): AdTrackingPreference => { const adTrackingPreference = cookieCache[ADVERTISING_SHARING_ALLOWED]; @@ -222,13 +234,13 @@ const getAdTrackingPreference = ( // 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, diff --git a/packages/cookie-manager/src/types.ts b/packages/cookie-manager/src/types.ts index 17c1416..a245b1a 100644 --- a/packages/cookie-manager/src/types.ts +++ b/packages/cookie-manager/src/types.ts @@ -66,6 +66,8 @@ export type TrackingManagerDependencies = { config: Config; shadowMode?: boolean; log: LogFunction; + initialCookieValues?: Record; + initialGPCValue?: boolean; }; export type AdTrackingPreference = { diff --git a/packages/cookie-manager/src/utils/applyGpcToAdPref.ts b/packages/cookie-manager/src/utils/applyGpcToAdPref.ts index fd07934..23994a4 100644 --- a/packages/cookie-manager/src/utils/applyGpcToAdPref.ts +++ b/packages/cookie-manager/src/utils/applyGpcToAdPref.ts @@ -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; } diff --git a/packages/cookie-manager/src/utils/applyGpcToCookiePref.ts b/packages/cookie-manager/src/utils/applyGpcToCookiePref.ts index e0b02e2..4ee0e06 100644 --- a/packages/cookie-manager/src/utils/applyGpcToCookiePref.ts +++ b/packages/cookie-manager/src/utils/applyGpcToCookiePref.ts @@ -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); diff --git a/packages/cookie-manager/src/utils/getAllCookies.ts b/packages/cookie-manager/src/utils/getAllCookies.ts index 412a6cc..73b4e8b 100644 --- a/packages/cookie-manager/src/utils/getAllCookies.ts +++ b/packages/cookie-manager/src/utils/getAllCookies.ts @@ -25,6 +25,7 @@ export const deserializeCookies = (region: Region, cookies: Record) { if (typeof window === 'undefined' && initialCookies) { return deserializeCookies(region, initialCookies); diff --git a/packages/cookie-manager/src/utils/getGpc.test.ts b/packages/cookie-manager/src/utils/getGpc.test.ts new file mode 100644 index 0000000..aca20c9 --- /dev/null +++ b/packages/cookie-manager/src/utils/getGpc.test.ts @@ -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); + }); +}); diff --git a/packages/cookie-manager/src/utils/getGpc.ts b/packages/cookie-manager/src/utils/getGpc.ts new file mode 100644 index 0000000..eedcae3 --- /dev/null +++ b/packages/cookie-manager/src/utils/getGpc.ts @@ -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;