Skip to content

Commit a0a834b

Browse files
committed
feat(scripts): add consent manager provider using c15t
1 parent 4845d87 commit a0a834b

File tree

7 files changed

+1819
-27
lines changed

7 files changed

+1819
-27
lines changed

.changeset/full-bees-worry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@bigcommerce/catalyst-core': minor
3+
---
4+
5+
Adds a new consent manager provider using the `@c15t/nextjs` package to handle cookie consent management with support for multiple consent categories and cookie-based persistence.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { z } from 'zod';
2+
3+
const CONSENT_COOKIE_NAME = 'c15t-consent';
4+
const ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
5+
6+
const ConsentNamesSchema = z.enum(['necessary', 'functionality', 'marketing', 'measurement']);
7+
const ConsentStateSchema = z.record(ConsentNamesSchema, z.boolean());
8+
const ConsentCookieSchema = z.object({
9+
preferences: ConsentStateSchema,
10+
timestamp: z.string().datetime(),
11+
});
12+
13+
type ConsentCookie = z.infer<typeof ConsentCookieSchema>;
14+
15+
const encode = (c: ConsentCookie) => encodeURIComponent(JSON.stringify(c));
16+
const decode = (raw: string) => {
17+
try {
18+
const json: unknown = JSON.parse(decodeURIComponent(raw));
19+
20+
return ConsentCookieSchema.parse(json);
21+
} catch {
22+
return null;
23+
}
24+
};
25+
26+
export const consentCookieClient = () => {
27+
const getCookie = (name: string) => {
28+
if (typeof document === 'undefined') return null;
29+
30+
const pair = document.cookie.split('; ').find((c) => c.startsWith(`${name}=`));
31+
32+
return pair ? pair.slice(name.length + 1) : null;
33+
};
34+
35+
const serialize = (value: string) =>
36+
`${CONSENT_COOKIE_NAME}=${value}; Path=/; Max-Age=${ONE_YEAR_SECONDS}; SameSite=Lax; Secure`;
37+
38+
return {
39+
get: () => {
40+
const raw = getCookie(CONSENT_COOKIE_NAME);
41+
42+
return raw ? decode(raw) : null;
43+
},
44+
set: (consent: ConsentCookie) => {
45+
if (typeof document === 'undefined') return;
46+
document.cookie = serialize(encode(consent));
47+
},
48+
};
49+
};
50+
51+
export const consentCookieServer = async () => {
52+
const { cookies } = await import('next/headers');
53+
const cookieStore = await cookies();
54+
55+
return {
56+
get: () => {
57+
const cookie = cookieStore.get(CONSENT_COOKIE_NAME)?.value;
58+
59+
if (!cookie) return null;
60+
61+
return decode(cookie);
62+
},
63+
set: (consent: ConsentCookie) => {
64+
cookieStore.set({
65+
name: CONSENT_COOKIE_NAME,
66+
value: encode(consent),
67+
path: '/',
68+
maxAge: ONE_YEAR_SECONDS,
69+
sameSite: 'lax',
70+
secure: true,
71+
});
72+
},
73+
};
74+
};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { consentCookieClient } from './cookies';
2+
3+
const ok = <T>(data: T | null = null) => ({
4+
data,
5+
error: null,
6+
ok: true,
7+
response: null,
8+
});
9+
10+
export function showConsentBanner() {
11+
let show = true;
12+
13+
try {
14+
const consent = consentCookieClient().get();
15+
16+
show = !consent;
17+
} catch {
18+
show = false;
19+
}
20+
21+
return ok({
22+
showConsentBanner: show,
23+
branding: 'none',
24+
});
25+
}
26+
27+
export function setConsent(options?: { body?: { preferences?: Record<string, boolean> } }) {
28+
const { preferences } = options?.body ?? {};
29+
30+
consentCookieClient().set({
31+
preferences: preferences ?? {},
32+
timestamp: new Date().toISOString(),
33+
});
34+
35+
return ok();
36+
}
37+
38+
export function verifyConsent() {
39+
const consent = consentCookieClient().get();
40+
41+
if (!consent) {
42+
return ok({
43+
isValid: false,
44+
});
45+
}
46+
47+
return ok({
48+
isValid: true,
49+
});
50+
}

core/lib/consent-manager/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './provider';
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
'use client';
2+
3+
import { ConsentManagerProvider as C15TConsentManagerProvider } from '@c15t/nextjs';
4+
import { type PropsWithChildren } from 'react';
5+
6+
import { setConsent, showConsentBanner, verifyConsent } from './handlers';
7+
8+
export function ConsentManagerProvider({ children }: PropsWithChildren) {
9+
return (
10+
<C15TConsentManagerProvider
11+
options={{
12+
mode: 'custom',
13+
consentCategories: ['necessary', 'functionality', 'marketing', 'measurement'],
14+
15+
// @ts-expect-error endpointHandlers type is not yet exposed by the package
16+
endpointHandlers: {
17+
showConsentBanner,
18+
setConsent,
19+
verifyConsent,
20+
},
21+
}}
22+
>
23+
{children}
24+
</C15TConsentManagerProvider>
25+
);
26+
}

core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
},
1515
"dependencies": {
1616
"@bigcommerce/catalyst-client": "workspace:^",
17+
"@c15t/nextjs": "^1.7.0",
1718
"@conform-to/react": "^1.6.1",
1819
"@conform-to/zod": "^1.6.1",
1920
"@icons-pack/react-simple-icons": "^11.2.0",

0 commit comments

Comments
 (0)