Skip to main content

JavaScript

Upgrade from v2

Upgrade this JavaScript app to c15t v3

Upgrade this JavaScript app from the c15t v2 store to c15t v3.

Read https://v3.c15t.com/docs/frameworks/javascript/upgrade-v3.md first and follow it in order. Do not guess v3 APIs from memory.

  1. Find every use of the c15t package and @c15t/scripts: getOrCreateConsentRuntime, configureConsentManager, consentStore, cookie helpers, script loader and iframe blocker functions.
  2. Decide which v3 entry fits. Use @c15t/browser if the app wants the stock banner, createConsentRuntime from c15t/runtime if it renders its own UI, and createConsentKernel only if it mounts each module itself. Tell me which one you picked and why.
  3. Install c15t@alpha, or @c15t/browser@alpha for the stock banner, plus @c15t/integrations@alpha if the app uses vendor helpers and @c15t/dev-tools@alpha if it uses dev tools.
  4. Run npx @c15t/cli@alpha codemods scripts-to-integrations consent-provider-options policy-packs-to-policy-rules callbacks-to-v3 --dry-run --json. Review the output, then run the same command without --dry-run. The codemods leave a TODO(c15t v3) comment wherever they need you to finish a change.
  5. If you picked createConsentRuntime, call runtime.start() after creating it. Replace each store call with its v3 equivalent from the guide's mapping table.
  6. Update callbacks and policy presets as the guide describes.
  7. Keep the existing backend URL. If the app runs its own c15t backend, stop and tell me, because it has to be upgraded in the same release with https://v3.c15t.com/docs/self-host/upgrade-v3.md.
  8. Run the typecheck and build, and resolve every TODO(c15t v3) comment.

When you finish, list the files you changed, anything you could not migrate, and how you checked that vendor requests still wait for consent.

Before you start

If you run your own c15t backend, upgrade it in the same release. A v3 client can't read a v2 backend. See Upgrade a self-hosted backend. Inth-hosted backends need no change.

Every v3 package ships ESM only. If a CommonJS file in your app calls require() on a c15t package, convert it to ESM.

Pick a replacement

v2's getOrCreateConsentRuntime(), configureConsentManager() and the Zustand consentStore are gone. Pick a replacement by how much you want c15t to do.

You wantUse
The stock banner and dialog@c15t/browser. Call init() from a bundled app, or add the script tag to a site without a build step. See the quickstart.
Your own UI, with persistence, the script loader and the blockers wired for youcreateConsentRuntime() from c15t/runtime, then runtime.start(). See headless.
The engine alone, mounting each module yourselfcreateConsentKernel() from c15t. See the kernel API.

Install c15t for the runtime or the kernel:

npm install c15t@alpha

The runtime takes the same kind of options v2 did. mode and backendURL become one transport, such as hosted({ backendURL }) or offline({ policyRules }), exported from c15t. translations becomes i18n, and iframeBlockerConfig becomes iframeBlocker. See the runtime reference for every option.

createConsentRuntime() only builds the runtime. Call runtime.start() to read stored consent, resolve the policy and mount the script loader and blockers. Until then nothing is gated.

The consent-provider-options codemod rewrites mode, backendURL and iframeBlockerConfig in the object you pass to getOrCreateConsentRuntime() and adds the hosted or offline import from c15t. Rename the call to createConsentRuntime() from c15t/runtime yourself.

Replace store calls

The runtime exposes the kernel as runtime.kernel. Read state from kernel.getSnapshot() and listen with kernel.subscribe().

v2 storev3
consentStore.getState()kernel.getSnapshot()
consentStore.subscribe(listener)kernel.subscribe(listener). The listener gets the whole snapshot, on every change.
subscribeToConsentChanges(listener)kernel.events.on('permissions:changed', ({ snapshot }) => listener(snapshot.effectivePermissions)), or the onPermissionsChanged callback
has(category)kernel.getSnapshot().effectivePermissions[category]
consentskernel.getSnapshot().effectivePermissions
consentInfokernel.getSnapshot().explicitChoice, which is null until the visitor chooses
hasConsented()kernel.getSnapshot().explicitChoice !== null
saveConsents('all')kernel.commands.save('all')
saveConsents('necessary')kernel.commands.save('none')
selectedConsents, setSelectedConsent(name, value)createPreferenceDraft(kernel) from c15t/preference-draft, then draft.getState() and draft.set(name, value)
saveConsents('custom')draft.save() on that preference draft
setConsent(name, value)kernel.commands.save({ [name]: value })
activeUI, setActiveUI(ui)kernel.getSnapshot().activeUI, kernel.set.activeUI(ui)
locationInfokernel.getSnapshot().location
setOverrides(overrides)runtime.setOverrides(overrides), then runtime.reinit()
setLanguage(code)runtime.setLanguage(code)
identifyUser(user)runtime.identify(user)

effectivePermissions is what may run now. explicitChoice is what the visitor decided. Under an opt-out policy a category can be allowed before any choice, so don't treat a true permission as a recorded grant. See snapshot.

Update moved exports

v2 export from c15tv3
getCookie, setCookie, deleteCookie, getConsentFromStorage, saveConsentToStorage, deleteConsentFromStorageRemoved. createPersistence() from c15t/modules/persistence owns storage.
loadScripts, unloadScripts, updateScripts, isScriptLoaded, getLoadedScriptIdsRemoved. Pass scripts to createConsentRuntime(), which mounts the loader on start(). To run a loader yourself, createScriptLoader({ kernel, scripts }) from c15t/modules/script-loader returns updateScripts(next), getLoadedScriptIds() and dispose(). Use getLoadedScriptIds().includes(id) for isScriptLoaded(id).
createIframeBlockerc15t/modules/iframe-blocker
getEffectivePolicy, filterConsentCategoriesByPolicy and the other policy helpersresolveConsentPresentation() from c15t and the helpers in c15t/surface-actions
C15tClient, OfflineClient, CustomClient, API_ENDPOINTSTransports: hosted(), offline() and custom() from c15t
policyPackPresetspolicyRulePresets
configureConsentManager, createConsentManagerStore, clearConsentRuntimeCacheRemoved

Rename the integrations dependency

v3 renames @c15t/scripts to @c15t/integrations. Remove @c15t/scripts from your dependencies and install @c15t/integrations from the alpha dist-tag:

npm install @c15t/integrations@alpha

Replace the package name in each import. Vendor subpaths and helper names stay the same.

Before, in v2:

import { googleTagManager } from '@c15t/scripts/google-tag-manager';
import { posthog } from '@c15t/scripts/posthog';

After:

import { googleTagManager } from '@c15t/integrations/google-tag-manager';
import { posthog } from '@c15t/integrations/posthog';

The scripts configuration option and the Script type keep their names.

Two helper behaviors change. A helper that needs an ID, such as metaPixel, googleTagManager or posthog, now logs an error and skips its script when the ID is empty or only whitespace, where v2 loaded a broken script. And if your code reads @c15t/integrations/registry, an entry's consentCategory can now be a compound condition such as { and: ['marketing', 'measurement'] }, not only a category name.

@c15t/scripts stays available as a deprecated compatibility package for all of v3. It re-exports the implementation and types of @c15t/integrations, so you can migrate imports separately from the rest of the upgrade. Compatibility ends in v4; versions already published stay on npm.

To preview the import changes in JavaScript and TypeScript files:

npx @c15t/cli@alpha codemods scripts-to-integrations --dry-run --json

Review the output, then run it again without --dry-run. The codemod runs only when you name it. It does not change package.json, lockfiles, or imports inside .vue, .svelte or .astro files; update those by hand, then reinstall dependencies and build the app.

Replace callbacks

Callbacks stay under callbacks in your options, with new names:

v2 callbackv3 callback
onConsentSetonPermissionsChanged to react to what may run now. onChoiceRecorded to react to the visitor's accept, reject or save.
onConsentChangedonChoiceRecorded. See the note below the table.
onBannerFetchedNo callback. In React and Next.js, read usePolicyResolution() to know when the policy has resolved. In JavaScript, read resolution from the kernel snapshot.
onError, onBeforeConsentRevocationReloadUnchanged

The callbacks-to-v3 codemod renames onConsentChanged to onChoiceRecorded. It leaves a TODO(c15t v3) comment there about the new payload, and on onConsentSet and onBannerFetched, which it can't choose a replacement for:

npx @c15t/cli@alpha codemods callbacks-to-v3 --dry-run --json

v2's onConsentChanged fired only when a save changed at least one category's value, and passed preferences and previousPreferences. v3's onChoiceRecorded fires for every accept, reject or save that confirms a category, including one that saves the same values again, because each save renews the choice's confirmation time. Its payload carries snapshot, confirmed and actionAt. v3 has no callback that fires only when a save changes a value. If you need the before and after values, use onPermissionsChanged: it passes previous and the new snapshot whenever effective permissions change, whether a save or something else changed them.

v2's onConsentSet also fired on initialization and hydration. In v3, onChoiceRecorded runs only when the visitor acts, and onPermissionsChanged runs when effective permissions change for any reason, including expiry, a policy update or a privacy signal. Neither runs at startup if the resolved permissions match the defaults, such as an opt-in policy that leaves every category denied. If an external API needs the starting state, read it once at startup as well: in React and Next.js from useEffectivePermissions() in an effect, which also runs on later changes, and in JavaScript from kernel.events.on('init:applied', ({ snapshot }) => ...).

After (v3): callbacks in your options
callbacks: {
  onChoiceRecorded(event) {
    console.log('Visitor recorded a choice', event);
  },
  onPermissionsChanged(event) {
    console.log('Effective permissions changed', event);
  },
},

Move policy packs to policy rules

v2 policy packs become policy rules, and policyPackPresets becomes policyRulePresets, exported from c15t. Where the rules live depends on your backend:

  • Inth: set the rules in your Inth project. Rules in client code do not change a hosted policy.
  • Self-hosted: move the backend's policyPacks to manifest.policyRules. See policy configuration.
  • Offline: move offlinePolicy.policyPacks to offline({ policyRules }).
After (v3): offline policy rules
import { offline, policyRulePresets } from 'c15t';

const mode = offline({
	policyRules: [
		policyRulePresets.europeOptIn(),
		policyRulePresets.worldOptOutNoPrompt(),
	],
});

In Next.js, import offline from c15t/next instead and set the result as mode in c15t.config.ts. Both take the same policyRules.

v2 presetv3 preset
europeOptIn(), europeIab()Same names
californiaOptIn(), californiaOptOut()Same names
quebecOptIn()Same name
worldNoBanner()worldOptOutNoPrompt()

worldNone() is a separate, new preset: it shows nothing and grants no rights, while worldOptOutNoPrompt() keeps v2's opt-out and preference rights.

The policy-packs-to-policy-rules codemod renames policyPackPresets and worldNoBanner(), and moves the import to c15t where it came from c15t/react or c15t/next. consent-provider-options moves offlinePolicy.policyPacks into offline({ policyRules }). Preset calls need no other change, but a hand-written v2 pack uses consent and ui keys, which v3 rules replace with flat keys such as model and prompt; the codemods mark those with a TODO(c15t v3) comment.

v3 adds presets for more regions. offline() without policyRules uses recommendedPolicyRules(). Read policies before you change a rule's model or prompt, and how consent works for the difference between a permission and a recorded choice.

Update complete translation objects

Partial overrides through i18n.messages and objects typed as Translations need no change. A complete translation object typed as CompleteTranslations fails type-checking until you add the new sections and keys: consentGate, which replaces frame, rights, the notice and dismiss strings such as common.acknowledge and cookieBanner.noticeTitle, and the vendor list copy under consentManagerDialog.vendors. migrateLegacyTranslationKeys() moves frame copy to consentGate.

i18n.messages for the visitor's language now merge over the backend's copy instead of being replaced by it.

Keep visitors' existing choices

v3 uses the same c15t cookie and storage key as v2 and reads v2 records. It does not rewrite them on page load; the next accept, reject or save writes a v3 record.

A v2 denial keeps restricting its category. A v2 grant keeps applying until the policy's validity period runs out, and then c15t asks again. If the rule comes from a preset and the v2 record noted the policy it was made under, c15t also asks again when that policy has changed. Under custom rules, a v2 grant applies until it expires. Do not copy permissions into new records yourself; c15t decides which v2 choices still count.

Update dev tools

Dev tools no longer depend on React. Upgrade @c15t/dev-tools to @c15t/dev-tools@alpha with the rest of c15t, then call createDevTools({ kernel }) with your runtime's kernel. See dev tools.

Upgrade from an earlier v3 alpha

The v3 alphas renamed and removed APIs that never shipped in a stable release. Names that were in stable v2 keep a deprecated alias. Every other renamed name is removed with no alias, so a build that still uses one fails to type-check or throws at runtime.

Removed

Modes and build integrations:

RemovedUse instead
hosted({ url })hosted({ backendURL }), or the framework's backend URL variable
createManifestTransport({ manifest })createManifestTransport({ snapshot })
assertDecisionInputs defaulting to false on hosted()It now defaults to true whenever initURL is set. Pass false to turn it off.
hostedModes from c15t/runtime/providerreadHostedMode(mode)
buildManifest: true in Astro and NuxtonBuildError: 'fail', the default for production builds. buildManifest: false is manifest({ source: 'runtime' }).
A generated c15t-manifest.ts and its .gitignore entryimport { snapshot } from 'c15t/generated'. Most apps no longer import it at all.
The build options outputFile, exportName, importSource and rootDirRemoved. The snapshot is a virtual module, or a file under node_modules/.cache/c15t/ in Next.js.

@c15t/browser:

RemovedUse instead
init({ backendURL })init({ mode: hosted() }), or init({ mode: manifest() }) with consentManifest() from c15t/build
init({ mode: 'hosted' }), 'manifest', 'offline'init({ mode: hosted() }), manifest(), offline(). Names work only in the script-tag builds.
init({ manifest, manifestURL })init({ mode: manifest({ snapshot, manifestURL }) })
init({ policyRules: ['europeOptIn'] })init({ mode: offline({ policyRules: [policyRulePresets.europeOptIn()] }) })
manifest({ manifest })manifest({ snapshot })

The script-tag builds and the @c15t/browser/hosted and @c15t/browser/offline entries keep backendURL, manifest, manifestURL, policyRules and mode names.

Update the CLI

  • Run c15t login again. The CLI signs in through Inth now and no longer reads sessions saved by v2.
  • c15t setup no longer writes .env.local or .env.example and no longer accepts --env. It writes the backend URL into the files it generates.
  • c15t setup no longer detects Remix or Gatsby. Pick a guide from Choose your setup for those apps.

Check the migration

  1. Run your typecheck and build.
  2. Load the app with a v2 consent cookie from before the upgrade. The banner stays closed if that choice still applies, and a rejected category stays blocked.
  3. In a private window, check in DevTools Network that vendor requests wait for consent, start after you accept, and stay absent after you reject and reload.
  4. Reopen preferences and change a category.

The verification guide has the full release checklist.