Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/full-bees-worry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@bigcommerce/catalyst-core': minor
---

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.
31 changes: 31 additions & 0 deletions core/lib/consent-manager/cookies/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { z } from 'zod';

import { decode } from '../decoder';
import { encode } from '../encoder';
import { ConsentCookieSchema } from '../schema';

import { CONSENT_COOKIE_NAME, ONE_YEAR_SECONDS } from './constants';

type ConsentCookie = z.infer<typeof ConsentCookieSchema>;

const getCookieValueByName = (name: string) => {
if (typeof document === 'undefined') return null;

const pair = document.cookie.split('; ').find((c) => c.startsWith(`${name}=`));

return pair ? pair.slice(name.length + 1) : null;
};

const serialize = (value: string) =>
`${CONSENT_COOKIE_NAME}=${value}; Path=/; Max-Age=${ONE_YEAR_SECONDS}; SameSite=Lax; Secure`;

export const getConsentCookie = () => {
const raw = getCookieValueByName(CONSENT_COOKIE_NAME);

return raw ? decode(raw) : null;
};

export const setConsentCookie = (consent: ConsentCookie) => {
if (typeof document === 'undefined') return;
document.cookie = serialize(encode(consent));
};
2 changes: 2 additions & 0 deletions core/lib/consent-manager/cookies/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const CONSENT_COOKIE_NAME = 'c15t-consent';
export const ONE_YEAR_SECONDS = 60 * 60 * 24 * 365;
32 changes: 32 additions & 0 deletions core/lib/consent-manager/cookies/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { cookies } from 'next/headers';
import { z } from 'zod';

import { decode } from '../decoder';
import { encode } from '../encoder';
import { ConsentCookieSchema } from '../schema';

import { CONSENT_COOKIE_NAME, ONE_YEAR_SECONDS } from './constants';

type ConsentCookie = z.infer<typeof ConsentCookieSchema>;

export const getConsentCookie = async () => {
const cookieStore = await cookies();
const cookie = cookieStore.get(CONSENT_COOKIE_NAME)?.value;

if (!cookie) return null;

return decode(cookie);
};

export const setConsentCookie = async (consent: ConsentCookie) => {
const cookieStore = await cookies();

cookieStore.set({
name: CONSENT_COOKIE_NAME,
value: encode(consent),
path: '/',
maxAge: ONE_YEAR_SECONDS,
sameSite: 'lax',
secure: true,
});
};
11 changes: 11 additions & 0 deletions core/lib/consent-manager/decoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ConsentCookieSchema } from './schema';

export const decode = (raw: string) => {
try {
const json: unknown = JSON.parse(decodeURIComponent(raw));

return ConsentCookieSchema.parse(json);
} catch {
return null;
}
};
7 changes: 7 additions & 0 deletions core/lib/consent-manager/encoder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from 'zod';

import { ConsentCookieSchema } from './schema';

type ConsentCookie = z.infer<typeof ConsentCookieSchema>;

export const encode = (c: ConsentCookie) => encodeURIComponent(JSON.stringify(c));
58 changes: 58 additions & 0 deletions core/lib/consent-manager/handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { getConsentCookie, setConsentCookie } from './cookies/client';

const ok = <T>(data: T | null = null) => ({
data,
error: null,
ok: true,
response: null,
});

export function showConsentBanner() {
let show = true;
let language = 'en';

if (typeof document !== 'undefined') {
language = document.documentElement.lang;
}

try {
const consent = getConsentCookie();

show = !consent;
} catch {
show = false;
}

return ok({
showConsentBanner: show,
translations: {
language,
},
branding: 'none',
});
}

export function setConsent(options?: { body?: { preferences?: Record<string, boolean> } }) {
const { preferences } = options?.body ?? {};

setConsentCookie({
preferences: preferences ?? {},
timestamp: new Date().toISOString(),
});

return ok();
}

export function verifyConsent() {
const consent = getConsentCookie();

if (!consent) {
return ok({
isValid: false,
});
}

return ok({
isValid: true,
});
}
1 change: 1 addition & 0 deletions core/lib/consent-manager/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './provider';
26 changes: 26 additions & 0 deletions core/lib/consent-manager/provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'use client';

import { ConsentManagerProvider as C15TConsentManagerProvider } from '@c15t/nextjs';
import { type PropsWithChildren } from 'react';

import { setConsent, showConsentBanner, verifyConsent } from './handlers';

export function ConsentManagerProvider({ children }: PropsWithChildren) {
return (
<C15TConsentManagerProvider
options={{
mode: 'custom',
consentCategories: ['necessary', 'functionality', 'marketing', 'measurement'],

// @ts-expect-error endpointHandlers type is not yet exposed by the package
endpointHandlers: {
showConsentBanner,
setConsent,
verifyConsent,
},
}}
>
{children}
</C15TConsentManagerProvider>
);
}
9 changes: 9 additions & 0 deletions core/lib/consent-manager/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { z } from 'zod';

const ConsentNamesSchema = z.enum(['necessary', 'functionality', 'marketing', 'measurement']);
const ConsentStateSchema = z.record(ConsentNamesSchema, z.boolean());

export const ConsentCookieSchema = z.object({
preferences: ConsentStateSchema,
timestamp: z.string().datetime(),
});
1 change: 1 addition & 0 deletions core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"dependencies": {
"@bigcommerce/catalyst-client": "workspace:^",
"@c15t/nextjs": "^1.7.0",
"@conform-to/react": "^1.6.1",
"@conform-to/zod": "^1.6.1",
"@icons-pack/react-simple-icons": "^11.2.0",
Expand Down
Loading