diff --git a/apps/example/package.json b/apps/example/package.json index f6277f8..5dae562 100644 --- a/apps/example/package.json +++ b/apps/example/package.json @@ -11,8 +11,8 @@ "test": "jest --passWithNoTests" }, "dependencies": { - "@coinbase/cookie-banner": "1.0.1", - "@coinbase/cookie-manager": "^1.0.0", + "@coinbase/cookie-banner": "1.0.2", + "@coinbase/cookie-manager": "1.1.0", "next": "14.0.0", "react": "^18", "react-dom": "^18" diff --git a/apps/example/pages/_app.tsx b/apps/example/pages/_app.tsx index 77c3edc..d194262 100644 --- a/apps/example/pages/_app.tsx +++ b/apps/example/pages/_app.tsx @@ -9,11 +9,12 @@ import { cookieManagerConfig } from '../utils/cookieManagerConfig'; export default function App({ Component, pageProps }: AppProps) { const [isMounted, setIsMounted] = useState(false); + const trackingPreference = useRef(); + useEffect(() => { setIsMounted(true); }, []); - const trackingPreference = useRef(); const setTrackingPreference = useCallback((newPreference: TrackingPreference) => { const priorConsent = trackingPreference.current?.consent; trackingPreference.current = newPreference; @@ -38,6 +39,7 @@ export default function App({ Component, pageProps }: AppProps) { window.location.reload(); } }, []); + if (!isMounted) return null; return ( @@ -45,7 +47,7 @@ export default function App({ Component, pageProps }: AppProps) { onError={console.error} projectName="test" locale="en" - region={Region.EU} + region={Region.DEFAULT} config={cookieManagerConfig} log={console.log} onPreferenceChange={setTrackingPreference} diff --git a/apps/example/pages/index.tsx b/apps/example/pages/index.tsx index 91fa96d..caa6e4e 100644 --- a/apps/example/pages/index.tsx +++ b/apps/example/pages/index.tsx @@ -1,11 +1,24 @@ import { CookieBanner } from '@coinbase/cookie-banner'; +import { useCookie } from '@coinbase/cookie-manager'; import { Inter } from 'next/font/google'; +import { useEffect } from 'react'; import useTranslations from '@/hooks/useTranslations'; const inter = Inter({ subsets: ['latin'] }); export default function Home() { + const [, setIpCountryCookie] = useCookie('ip_country'); + const [, setLocaleCookie] = useCookie('locale'); + const [, setRFMCookie] = useCookie('rfm'); + const [trackingPreference] = useCookie('cm_eu_preference'); + + useEffect(() => { + setIpCountryCookie('US'); + setRFMCookie('locale'); + setLocaleCookie('en'); + }, [setIpCountryCookie, setLocaleCookie, setRFMCookie, trackingPreference]); + return (
theme.breakpoints.tablet}px) { - display: flex; + display: none; } `; diff --git a/packages/cookie-banner/src/examples/config.ts b/packages/cookie-banner/src/examples/config.ts index 364ea29..0676da3 100644 --- a/packages/cookie-banner/src/examples/config.ts +++ b/packages/cookie-banner/src/examples/config.ts @@ -4,6 +4,7 @@ export default { categories: [ { id: TrackingCategory.NECESSARY, + expiry: 365, required: true, trackers: [ { @@ -14,6 +15,7 @@ export default { }, { id: TrackingCategory.PERFORMANCE, + expiry: 365, trackers: [ { id: 'some_cookie', @@ -31,6 +33,7 @@ export default { }, { id: TrackingCategory.FUNCTIONAL, + expiry: 365, trackers: [ { id: 'mode', @@ -40,6 +43,7 @@ export default { }, { id: TrackingCategory.TARGETING, + expiry: 365, trackers: [ // First party { @@ -55,6 +59,7 @@ export default { }, { id: TrackingCategory.DELETE_IF_SEEN, + expiry: 0, trackers: [ { id: 'cgl_prog', diff --git a/packages/cookie-manager/CHANGELOG.md b/packages/cookie-manager/CHANGELOG.md index 1047720..64de425 100644 --- a/packages/cookie-manager/CHANGELOG.md +++ b/packages/cookie-manager/CHANGELOG.md @@ -1,6 +1,14 @@ # Changelog -## 1.0.0 (12/7/2023 PST) +## 1.1.0 (12/14/2023) + +- Updated cookie config to include retention durations at both category and cookie levels. +- Introduced an optional boolean field `sessionCookie` in cookie config to accommodate session cookies. +- Implemented a setExpiryForCookie hook that adjusts the cookie's expiration in the following manner: + - If the sessionCookie parameter is enabled, the expiry is set to session + - If an expiry is specified at the cookie level, it takes precedence; otherwise, the expiry is derived from the category's expiration setting. + +## 1.0.0 (12/7/2023) #### 🚀 Updates diff --git a/packages/cookie-manager/README.md b/packages/cookie-manager/README.md index 06bf0d6..04db9eb 100644 --- a/packages/cookie-manager/README.md +++ b/packages/cookie-manager/README.md @@ -4,8 +4,8 @@ # Contents -- [Introduction](#Introduction) - [Installation](#installation) +- [Introduction](#Introduction) - [Methods](#methods) - [Provider](#provider) - [useCookie](#usecookie) @@ -20,6 +20,18 @@ - [isOptOut](#isoptout) - [License](#license) +## Installation + +Install the package as follows: + +```shell +yarn add @coinbase/cookie-manager + +npm install @coinbase/cookie-manager + +pnpm install @coinbase/cookie-manager +``` + ## Introduction `@coinbase/cookie-manager` helps manage the following first party client side cookie categories: @@ -57,24 +69,29 @@ export default { { id: TrackingCategory.NECESSARY, required: true, + expiry: 365, trackers: [ { id: 'locale', type: TrackerType.COOKIE, + expiry: 10, }, ], }, { id: TrackingCategory.PERFORMANCE, + expiry: 365, trackers: [ { id: 'some_cookie', type: TrackerType.COOKIE, + sessionCookie: true, }, ], }, { id: TrackingCategory.FUNCTIONAL, + expiry: 365, trackers: [ { id: 'mode', @@ -84,6 +101,7 @@ export default { }, { id: TrackingCategory.TARGETING, + expiry: 365, trackers: [ { id: 'id-regex', @@ -94,6 +112,7 @@ export default { }, { id: TrackingCategory.DELETE_IF_SEEN, + expiry: 0, trackers: [ { id: 'cgl_prog', @@ -115,7 +134,7 @@ export default { }; ``` -In this config, under each category you can specify what all cookies should be allowed. Everything else, if detected will be deleted at an interval of 500 ms. +In this config, under each category you can specify what all cookies that should be allowed. Everything else, if detected will be deleted at an interval of 500 ms. `DELETE_IF_SEEN` can be used to specify the cookies which should be deleted if seen on the browser @@ -131,19 +150,47 @@ You can also specify regex for a given cookie as follows: Any id with `-regex` at the end should contain a `regex` which will be used to match different cookies. -In this example: `id_ac7a5c3da45e3612b44543a702e42b01` will also be allowed +In this example: `id_ac7a5c3da45e3612b44543a702e42b01` will also be allowed. -## Installation +You need to specify the retention days for a category using the expiry key as follows: -Install the package as follows: +``` +{ + id: TrackingCategory.NECESSARY, + required: true, + expiry: 365, + trackers: [ + { + id: 'locale', + type: TrackerType.COOKIE, + expiry: 10, + }, + ], + } -```shell -yarn add @coinbase/cookie-manager +``` -npm install @coinbase/cookie-manager +Each cookie by default will inherit its category's retention duration but you can override this by specifying an expiry (in days) for the cookie: -pnpm install @coinbase/cookie-manager ``` + { + id: 'locale', + type: TrackerType.COOKIE, + expiry: 10 +} +``` + +If you want a cookie to only be valid for a session, it can optionally be marked as a session cookie as follows: + +``` +{ + id: 'some_cookie', + type: TrackerType.COOKIE, + sessionCookie: true +} +``` + +**Note: Session cookies only last as long as your browser is open and are automatically deleted when a user closes the browser** ## Methods @@ -476,4 +523,4 @@ const SomeComponent = () => { ## License -Licensed under the Apache License. See [LICENSE](./LICENSE) for more information. +Licensed under the Apache License. See [LICENSE](./LICENSE.md) for more information. diff --git a/packages/cookie-manager/package.json b/packages/cookie-manager/package.json index 39d6453..65cb805 100644 --- a/packages/cookie-manager/package.json +++ b/packages/cookie-manager/package.json @@ -1,6 +1,6 @@ { "name": "@coinbase/cookie-manager", - "version": "1.0.0", + "version": "1.1.0", "description": "Coinbase Cookie Manager", "main": "dist/index.js", "license": "Apache-2.0", @@ -10,6 +10,7 @@ "test": "jest" }, "devDependencies": { + "@testing-library/react": "^14.1.1", "@types/jest": "^29.5.8", "@types/js-cookie": "^3.0.5", "@types/node": "^20.8.9", @@ -21,9 +22,12 @@ "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "typescript": "^5.2.2", - "@testing-library/react": "^14.1.1" + "wrap-ansi": "^7.0.0", + "wrap-ansi-cjs": "7.0.0" }, "dependencies": { + "@coinbase/cookie-banner": "1.0.1", + "@coinbase/cookie-manager": "1.1.0", "js-cookie": "^3.0.5" } } diff --git a/packages/cookie-manager/src/CookieContext.tsx b/packages/cookie-manager/src/CookieContext.tsx index 98358ff..2ae9f04 100644 --- a/packages/cookie-manager/src/CookieContext.tsx +++ b/packages/cookie-manager/src/CookieContext.tsx @@ -25,6 +25,7 @@ import { getDomainWithoutSubdomain, getHostname } from './utils/getDomain'; import getTrackerInfo from './utils/getTrackerInfo'; import hasConsent from './utils/hasConsent'; import isMaxKBSize from './utils/isMaxKBSize'; +import setExpiryForCookie from './utils/setExpiryForCookie'; import setGTMVariables from './utils/setGTMVariables'; type CookieCache = Record; @@ -161,11 +162,19 @@ const setCookieFunction = ({ if (isMaxKBSize(encodeURIComponent(stringValue) + cookieName, cookieSize)) { onError(new Error(`${cookieName} value exceeds ${cookieSize}KB`)); } else { - const newOptions = options ? { ...options } : undefined; + let newOptions = options ? { ...options } : undefined; if (newOptions?.size) { delete newOptions.size; } + const expiration = setExpiryForCookie(cookieName, config) as number | Date | undefined; + if (expiration) { + if (newOptions) { + newOptions = { ...newOptions, expires: expiration }; + } else { + newOptions = { expires: expiration }; + } + } Cookies.set(cookieName, stringValue, newOptions); } } diff --git a/packages/cookie-manager/src/constants.ts b/packages/cookie-manager/src/constants.ts index 96568a9..14f2c86 100644 --- a/packages/cookie-manager/src/constants.ts +++ b/packages/cookie-manager/src/constants.ts @@ -4,6 +4,7 @@ export const EU_CONSENT_PREFERENCES_COOKIE = 'cm_eu_preferences'; export const DEFAULT_CONSENT_PREFERENCES_COOKIE = 'cm_default_preferences'; export const ADVERTISING_SHARING_ALLOWED = 'advertising_sharing_allowed'; export const IS_MOBILE_APP = 'is_mobile_app'; +export const ONE_DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000; export const GEOLOCATION_RULES: Array = [ { diff --git a/packages/cookie-manager/src/examples/config.ts b/packages/cookie-manager/src/examples/config.ts index 08de610..5591668 100644 --- a/packages/cookie-manager/src/examples/config.ts +++ b/packages/cookie-manager/src/examples/config.ts @@ -4,16 +4,23 @@ export default { categories: [ { id: TrackingCategory.NECESSARY, + expiry: 365, required: true, trackers: [ { id: 'locale', type: TrackerType.COOKIE, }, + { + id: 'test', + type: TrackerType.COOKIE, + expiry: 10, + }, ], }, { id: TrackingCategory.PERFORMANCE, + expiry: 365, trackers: [ { id: 'some_cookie', @@ -31,16 +38,19 @@ export default { }, { id: TrackingCategory.FUNCTIONAL, + expiry: 365, trackers: [ { id: 'mode', // Used to remember if the user dismissed the Advanced mode NUX modal type: TrackerType.COOKIE, + sessionCookie: true, }, ], }, { id: TrackingCategory.TARGETING, + expiry: 365, trackers: [ { id: 'gclid', @@ -55,6 +65,7 @@ export default { }, { id: TrackingCategory.DELETE_IF_SEEN, + expiry: 0, trackers: [ { id: 'cgl_prog', diff --git a/packages/cookie-manager/src/types.ts b/packages/cookie-manager/src/types.ts index b6b39c2..17c1416 100644 --- a/packages/cookie-manager/src/types.ts +++ b/packages/cookie-manager/src/types.ts @@ -41,13 +41,16 @@ export enum TrackerType { export type Tracker = { id: string; type: TrackerType; + sessionCookie?: boolean; regex?: string; + expiry?: number; }; export type ConfigCategoryInfo = { id: TrackingCategory; trackers: Array; required?: boolean; + expiry: number; }; export type Config = { diff --git a/packages/cookie-manager/src/utils/setExpiryForCookie.test.ts b/packages/cookie-manager/src/utils/setExpiryForCookie.test.ts new file mode 100644 index 0000000..e860b1f --- /dev/null +++ b/packages/cookie-manager/src/utils/setExpiryForCookie.test.ts @@ -0,0 +1,21 @@ +import config from '../examples/config'; +import setExpiryForCookie from './setExpiryForCookie'; + +describe('setExpiryForCookie', () => { + it('returns undefined when no cookie config is found', () => { + expect(setExpiryForCookie('cookieName', config)).toEqual(undefined); + }); + it('returns expiry when cookie config has an expiry', () => { + const expiration = new Date(); + expiration.setDate(expiration.getDate() + 10); + expect(setExpiryForCookie('test', config)).toEqual(expiration); + }); + it('returns expiry when cookie is in a category with an expiry', () => { + const expiration = new Date(); + expiration.setDate(expiration.getDate() + 365); + expect(setExpiryForCookie('some_cookie', config)).toEqual(expiration); + }); + it('returns undefined when cookie is a session cookie', () => { + expect(setExpiryForCookie('mode', config)).toEqual(undefined); + }); +}); diff --git a/packages/cookie-manager/src/utils/setExpiryForCookie.ts b/packages/cookie-manager/src/utils/setExpiryForCookie.ts new file mode 100644 index 0000000..bdfb761 --- /dev/null +++ b/packages/cookie-manager/src/utils/setExpiryForCookie.ts @@ -0,0 +1,39 @@ +import { ONE_DAY_IN_MILLISECONDS } from '../constants'; +import { Config } from '../types'; +import getTrackerCategory from './getTrackerCategory'; +import trackerMatches from './trackerMatches'; + +const setExpiryForCookie = (cookieName: string, config: Config) => { + const trackingCategory = getTrackerCategory(cookieName, config); + if (!trackingCategory) { + return undefined; + } + + // if cookie has an expiry, set the expiry to that + const cookieConfig = trackingCategory.trackers.find((tracker) => + trackerMatches(tracker, cookieName) + ); + + if (!cookieConfig) { + return undefined; + } + + const expiration = new Date(); + + // if its a session cookie, do not set an expiry + if (cookieConfig && cookieConfig.sessionCookie) { + return undefined; + } + + if (cookieConfig && cookieConfig.expiry) { + return new Date(expiration.getTime() + cookieConfig.expiry * ONE_DAY_IN_MILLISECONDS); + } + + if (trackingCategory) { + return new Date(expiration.getTime() + trackingCategory.expiry * ONE_DAY_IN_MILLISECONDS); + } + + return undefined; +}; + +export default setExpiryForCookie; diff --git a/yarn.lock b/yarn.lock index 49a9546..7e4966e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6692,8 +6692,7 @@ which@^2.0.1: dependencies: isexe "^2.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: - name wrap-ansi-cjs +wrap-ansi-cjs@7.0.0, "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==