Skip to content

chore(core): upgrade c15t to 1.8.2#2768

Merged
jorgemoya merged 1 commit intocanaryfrom
catalyst-1606-bump-c15t
Dec 15, 2025
Merged

chore(core): upgrade c15t to 1.8.2#2768
jorgemoya merged 1 commit intocanaryfrom
catalyst-1606-bump-c15t

Conversation

@jorgemoya
Copy link
Contributor

@jorgemoya jorgemoya commented Dec 12, 2025

What/Why?

Upgrade c15t to 1.8.2, migrate from custom mode to offline mode, refactor consent cookie handling to use c15t's compact format, add script location support for HEAD/BODY rendering, and add privacy policy link support to CookieBanner.

What Changed

  • Upgraded @c15t/nextjs to version 1.8.2
  • Changed consent manager mode from custom (with endpoint handlers) to offline mode
    • Removed custom handlers.ts implementation
  • Added enabled prop to C15TConsentManagerProvider to control consent manager functionality
  • Removed custom consent cookie encoder/decoder implementations (decoder.ts, encoder.ts)
  • Added parse-compact-format.ts to handle c15t's compact cookie format
    • Compact format: i.t:timestamp,c.necessary:1,c.functionality:1,etc...
  • Updated cookie parsing logic in both client and server to use the new compact format parser
  • Scripts now support location field from BigCommerce API and can be rendered in <head> or <body> based on the target property
  • CookieBanner now supports the privacyPolicyUrl field from BigCommerce API and will be rendered in the banner description if available.

Testing

Kapture.2025-12-12.at.11.07.17.mp4

Privacy Policy

Screenshot 2025-12-12 at 1 00 55 PM

Migration

Consent Manager Provider Changes

The ConsentManagerProvider now uses offline mode instead of custom mode with endpoint handlers. The provider configuration has been simplified:

Before:

<C15TConsentManagerProvider
  options={{
    mode: 'custom',
    consentCategories: ['necessary', 'functionality', 'marketing', 'measurement'],
    endpointHandlers: {
      showConsentBanner: () => showConsentBanner(isCookieConsentEnabled),
      setConsent,
      verifyConsent,
    },
  }}
>
  <ClientSideOptionsProvider scripts={scripts}>
    {children}
  </ClientSideOptionsProvider>
</C15TConsentManagerProvider>

After:

<C15TConsentManagerProvider
  options={{
    mode: 'offline',
    storageConfig: {
      storageKey: CONSENT_COOKIE_NAME,
      crossSubdomain: true,
    },
    consentCategories: ['necessary', 'functionality', 'marketing', 'measurement'],
    enabled: isCookieConsentEnabled,
  }}
>
  <ClientSideOptionsProvider scripts={scripts}>
    {children}
  </ClientSideOptionsProvider>
</C15TConsentManagerProvider>

Key changes:

  • mode changed from 'custom' to 'offline'
  • Removed endpointHandlers - no longer needed in offline mode
  • Added enabled prop to control consent manager functionality
  • Added storageConfig for cookie storage configuration

Cookie Handling

If you have custom code that directly reads or writes consent cookies, you'll need to update it:

Before:
The previous implementation used custom encoding/decoding. If you were directly accessing consent cookie values, you would have needed to use the custom decoder.

After:
The consent cookie now uses c15t's compact format. The public API for reading cookies remains the same:

import { getConsentCookie } from '~/lib/consent-manager/cookies/client'; // client-side
// or
import { getConsentCookie } from '~/lib/consent-manager/cookies/server'; // server-side

const consent = getConsentCookie();

The getConsentCookie() function now internally uses parseCompactFormat() to parse the compact format cookie string. If you were directly parsing cookie values, you should now use the getConsentCookie() helper instead.

getConsentCookie now returns a compact version of the consent values:

{
  i.t: 123456789,
  c.necessary: true,
  c.functionality: true,
  c.marketing: false,
  c.measurment: false
}

Updated instances where getConsentCookie is used to reflect this new schema.

Removed setConsentCookie from server and client since this is now handled by the c15t library.

Script Location Support

Scripts now support rendering in either <head> or <body> based on the location field from the BigCommerce API:

// Scripts transformer now includes target based on location
target: script.location === 'HEAD' ? 'head' : 'body'

The ScriptsFragment GraphQL query now includes the location field, allowing scripts to be placed in the appropriate DOM location. FOOTER location is still not supported.

Privacy Policy

The RootLayoutMetadataQuery GraphQL query now includes the privacyPolicyUrl field, which renders a provicy policy link in the CookieBanner description.

<CookieBanner 
  privacyPolicyUrl="https://example.com/privacy-policy"
  // ... other props
/>

The privacy policy link:

  • Opens in a new tab (target="_blank")
  • Only renders if privacyPolicyUrl is provided as a non-empty string

Add translatable privacyPolicy field to Components.ConsentManager.CookieBanner translation namespace for the privacy policy link text.

@vercel
Copy link

vercel bot commented Dec 12, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
catalyst-b2b Ready Ready Preview, Comment Dec 15, 2025 4:39pm
catalyst-canary Ready Ready Preview, Comment Dec 15, 2025 4:39pm
2 Skipped Deployments
Project Deployment Review Updated (UTC)
catalyst Ignored Ignored Dec 15, 2025 4:39pm
catalyst-uplift-vertex Ignored Ignored Dec 15, 2025 4:39pm

@changeset-bot
Copy link

changeset-bot bot commented Dec 12, 2025

🦋 Changeset detected

Latest commit: 7116d40

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@bigcommerce/catalyst-core Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@jorgemoya jorgemoya force-pushed the catalyst-1606-bump-c15t branch from e313647 to e030925 Compare December 12, 2025 16:37
@jorgemoya jorgemoya force-pushed the catalyst-1606-bump-c15t branch from e030925 to 9a8e8c0 Compare December 12, 2025 16:52
@jorgemoya jorgemoya force-pushed the catalyst-1606-bump-c15t branch from 9a8e8c0 to 04b42ea Compare December 12, 2025 17:10
@jorgemoya jorgemoya force-pushed the catalyst-1606-bump-c15t branch 2 times, most recently from 4493dae to e5b3335 Compare December 12, 2025 20:25
@jorgemoya jorgemoya marked this pull request as ready for review December 12, 2025 20:29
@jorgemoya jorgemoya requested a review from a team as a code owner December 12, 2025 20:29
Copy link
Contributor

@matthewvolk matthewvolk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just want to make sure that if someone changes the default cookie name to match stencil, the crossSubdomain property doesn't cause spooky behavior

Comment on lines +79 to +81
analyticsConsent: consent?.['c.measurement'] ?? false,
functionalConsent: consent?.['c.functionality'] ?? false,
targetingConsent: consent?.['c.marketing'] ?? false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍹 Is there a good reason to mix dot and bracket property accessors like this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just not possible with the way the cookie schema is defined

mode: 'offline',
storageConfig: {
storageKey: CONSENT_COOKIE_NAME,
crossSubdomain: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enabling cross-subdomain doesn't interfere with consent cookie names that may be set on the hosted checkout, does it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per an offline conversation, it could conflict but cookie names are different so it should be fine.

const baseConfig: C15tScript = {
category: mapConsentCategory(script.consentCategory),
id: script.entityId,
target: script.location === 'HEAD' ? 'head' : 'body',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏 Awesome that we support this now. Maybe you already mentioned it to Chris on the call we had, but we should include in the release notes since the last release notes mentioned this feature was coming soon

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically it's not rendered in the footer but I believe the behavior works as expected (priority loading).

@@ -0,0 +1,25 @@
/**
* Parses the compact format: i.t:1765485149496,c.necessary:1,c.functionality:1,etc.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah... maybe this is the answer to my previous comment about property accessors

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, c.necessary is the key

@jorgemoya jorgemoya force-pushed the catalyst-1606-bump-c15t branch from e5b3335 to 7116d40 Compare December 15, 2025 16:36
@jorgemoya jorgemoya added this pull request to the merge queue Dec 15, 2025
Merged via the queue into canary with commit 9782209 Dec 15, 2025
12 checks passed
@jorgemoya jorgemoya deleted the catalyst-1606-bump-c15t branch December 15, 2025 16:46
jamesqquick pushed a commit that referenced this pull request Feb 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants