Creating a Custom Consent UI

Transcend's Consent Manager is split up into a handful of files:

  • airgap.js: This is the core CMP code that is necessary to regulate cookies and data flows based on the output of airgap.getConsent()
  • xdi.js: This file is responsible for the code that is used to sync dhrnize consent preferences across subdomains.
  • ui.js: This is a lazy-loaded discrete JavaScript file that is capable of rendering the interface(s) necessary to collect consent. We provide an open source reference UI package that can be extended however you see fit. This package provides a strong foundation with full regime-specific UI integration, compatibility with our auto-prompt functionality, and translations for 54 languages.
  • cm.css: This is the stylesheet that is loaded by the default consent manager UI ui.js
  • translations/*.json: These are a series of files that contain the translations used in the open source consent manager ui.js file.
    • e.g. translations/en.json
    • e.g. translations/es-ES.json
    • See the full list of locales here

When deciding to build a custom consent interface, you have the following options that get increasingly more custom:

The simplest way to customize a consent interface is to override the cm.css stylesheet or translations/*.json files. This can be done by modifying your Display Settings in the Transcend Dashboard. When you customize your stylesheet or messages and publish those changes live, the cm.css and translations/*.json files will be updated. These files sit in the same folder as your airgap.js file, e.g. https://transcend-cdn.com/cm/6efb0409-a0f6-4d3c-90dd-0a03f909dd68/airgap.js and https://transcend-cdn.com/cm/6efb0409-a0f6-4d3c-90dd-0a03f909dd68/cm.css and https://transcend-cdn.com/cm/6efb0409-a0f6-4d3c-90dd-0a03f909dd68/translations/en.json. These files are consumed by the default ui.js file or any other custom interface that you decide to develop.

See this doc to learn more about customizing the stylesheet (CSS) for our out-of-the-box UIs rather than building an entirely custom UI.

In some situations, you may want to override certain banner messages and styles for specific websites. Instead of managing the stylesheet and messages from the Transcend Dashboard, you could instead host these files on your own CDN and point your airgap script tag to load the stylesheet and messages from your custom location.

To begin you would

  1. Copy the folder of translations onto your own CDN. Updating the message as desired.
  2. Copy the default stylesheet onto your own CDN. Updating the stylesheet as desired.
  3. Pass the location of your stylesheet and translation files to the airgap.js script tag.
<script
  data-cfasync="false"
  src="https://transcend-cdn.com/cm/6efb0409-a0f6-4d3c-90dd-0a03f909dd68/airgap.js"
  data-css="https://my.cdn/cm.css"
  data-translations="https://my.cdn/translations"
></script>

An example of this can also be found in the index.html file of the consent-manager-ui GitHub repo.

This approach can be useful if you dynamically want to change the stylesheet or messages.

In some situations, it may makes sense to clone our open source reference UI package to build a fully custom UI. This is a great solution as it will allow you to have a solid foundation for building a consent UI that is regime-specific, fully compatibile with our auto-prompt functionality in your, and able to load a customizable stylesheet and translations from the Transcend dashboard. This means you can continue to control which banners are displayed for different locations from your Regional Experiences section of the dashboard, as well as customizing any language or stylesheets from the Display Settings. This is often beneficial to allow your privacy or legal team to make change without requiring an engineering ticket. In addition, by implementing the standard Transcend interfaces and cloning the UI and bundling into a custom JavaScript file, you will be able to easily distribute your custom UI to all of your websites as that UI code is not tightly coupled to the website it renders on.

To begin you will first want to clone the git repository

git clone https://github.com/transcend-io/consent-manager-ui

This will copy the code to a local folder consent-manager-ui. You could then incorporate this into an existing git repository, or fork the consent-manager-ui repository to create a new repository for this project.

Next, you will want to decide which view states you will want to override. Start by reviewing the existing view states and finding the set of interface(s) that most closely align with the layout that you wish to achive.

Once you determine the view state that you want to override, you will want to open up the src/components folder to find the file that maps to your view state.

To start development, you will want to install yarn and node. Many versions of yarn and node may work, but if you are stuck, the following instructions can guide you:

  • Install Node v18.14.1
    • Install nvm using these docs
    • Run: nvm install 18.14.1
    • Run: nvm use 18.14.1
    • Run nvm alias default 18.14.1
    • Run node –version, this should output v18.14.1
  • Install yarn v3 or v4
    • Run: npm install --global yarn
    • Run: cd ./consent-manager-ui
    • Note you may need to change directory to wherever you cloned the consent-manager-ui repo
    • Run: yarn --version, it should output 4.5.1
    • If you see a version that is yarn v1, you likely have a version of yarn higher up in the path on your computer. Look for a file called .yarnrc, its often in your home directory e.g. ~/.yarnrc - if you delete this file and re-run yarn --version it should work

Once yarn and node are installed, you can run the following to install dependency, build the application, and start an auto-reloading sessions

yarn install
yarn build
yarn start

Once you run yarn start this will open the consent UI on localhost and you will be able to modify the files within the src/ folder as needed and the localhost session will auto-reload.

Once you've completed your changes and are ready to bundle your code, you can start the yarn start session and instead run:

yarn build

This will compile any changes that you have made in the src/*.ts files into the build/ folder. The majority of your JavaScript/TypeScript changes will be in build/ui.js but if you decided to modify the cm.css and translations/*.json files locally - these will also be copied to the build folder.

Once compiled, you can then take that ui.js file, host it on your CDN in a public location, and pass it to your airgap.js file using the data-ui script tag:

<script
  data-cfasync="false"
  src="https://transcend-cdn.com/cm/6efb0409-a0f6-4d3c-90dd-0a03f909dd68/airgap.js"
  data-ui="https://my.cdn/ui.js"
></script>

If desired, you can reach out to Transcend support to ask them to configure a default value for your data-ui parameter such that it always points to your self hosted UI by default. This is useful when you have many different websites loading the airgap.js script and you do not want to update all of them to point to your self-hosted UI file.

You may want to setup a continuous build process to publish your changes to a CDN whenever a pull request is merged. Here is an example GitHub action that can be used to get started:

name: ci
on: push

jobs:
  build-and-upload-artifacts:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      # 'Install yarn v2 dependencies'
      - uses: actions/setup-node@v3.1.1
        with:
          node-version: '20.18.0'
      - run: corepack enable
        shell: bash
      - uses: nick-fields/retry@v2
        with:
          timeout_minutes: 6
          max_attempts: 3
          retry_on: error
          command: yarn install --immutable

      - name: 'Run tsc'
        run: yarn tsc:build
      - name: Build the typescript code
        run: yarn build
      # <insert logic to push to your CDN>

Ultimately, you could build your consent UI natively into the website that you are integrating. The benefit of this would be that you could re-use existing components and create a seamless experience for your users (e.g. seeing consent values natively in the settings page). This brings the ultimate freedom of integration, but has the downside of requiring most engineering maintainence, and also not being portable to multiple websites loading your airgap.js script. This option works best when you only care to override the UI for the one specific site that you are looking to customize.

In this situation, you could completely disable the Transcend UI using data-ui="off":

<script
  data-cfasync="false"
  src="https://transcend-cdn.com/cm/6efb0409-a0f6-4d3c-90dd-0a03f909dd68/airgap.js"
  data-ui="off"
></script>

If your UI setup code runs before airgap.js core is initialized, you will need to use airgap.ready() as demonstrated below:

// Stub airgap.js ready queue
if (!self?.airgap?.ready) {
  self.airgap = {
    readyQueue: [],
    ready(callback) {
      this.readyQueue.push(callback);
    },
    ...self?.airgap,
  };
}

// Wait for airgap.js core to load
self.airgap.ready((airgap) => {
  // Init your UI DOM here
});

Every airgap.js API that can mutate consent state requires a genuine user gesture (event.isTrusted == true) which can be either any UIEvent (e.g. click) or a form submit event.

Script load events that fired prior to airgap.js initialization can also be used as valid consent authorization.

<script
  data-cfasync="false"
  src="https://transcend-cdn.com/cm/443312ef-12f9-494d-85f5-969894260cc7/airgap.js"
  onload="setAirgapAuth(event);"
></script>

You should pass the load event value to your own trusted code for safekeeping in setAirgapAuth(event). That event can later be used when authorization is required, like for airgap.getConsent() (see below)

airgap.setConsent(getAirgapAuth(), consent);

airgap.getConsent() returns an TrackingConsentDetails descriptor. This descriptor has a purposes field which returns an object keyed by all consentable tracking purpose types. The descriptor also has an ISO 8601 timestamp accessible via the timestamp field.

getConsent() Example

await airgap.sync();
const consent = airgap.getConsent().purposes;
console.log(JSON.stringify(consent, null, 2));

airgap.getConsent().purposes output:

{
  "Functional": false,
  "Analytics": false,
  "Advertising": false
}

airgap.setConsent() sets user consent for individual tracking purposes. A trusted interaction Event (or the script load event for the airgap.js script) is required by this API.

Example

saveConsentButton.addEventListener('click', (interaction) => {
  airgap.setConsent(interaction, serializeConsent());
});

ℹ️ The first argument to airgap.setConsent must be a native click or submit Event. If you're using React, your event may be a SyntheticEvent. In this case, you will need to pass interaction.nativeEvent.

await airgap.sync();

const hasPrivacyRights = airgap.getRegimePurposes().size !== 0;

// Is user fully opted-in to core unessential tracking purposes:
// Functional, Analytics, and Advertising
const isOptedIn = airgap.isOptedIn();

// a similar utility is available for detecting full opt-out
// of these purposes:
// const isOptedOut = airgap.isOptedOut();

const consentForm = document.getElementById('...');
const consentCheckbox = document.getElementById('...');
consentCheckbox.checked = isOptedIn;

if (hasPrivacyRights) {
  consentCheckbox.addEventListener('change', (interaction) => {
    if (interaction.target.checked) {
      // Consent to all user-configurable tracking purposes
      airgap.optIn({ interaction });
    } else {
      // Opt-out of all user-configurable tracking purposes
      airgap.optOut({ interaction });
    }
  });
} else {
  consentForm.style.display = 'none';
  consentCheckbox.disabled = true;
}

Consent can be set on a per-purpose level using airgap.getConsent()airgap.setConsent(), and airgap.getPurposesTypes().

airgap.getPurposesTypes() returns airgap's current TrackingPurposesTypes config. This config data can be used in conjunction with airgap.getConsent() to inform the content of a custom consent UI.

const purposes = airgap.getPurposesTypes();
console.log(JSON.stringify(purposes, null, 2));

airgap.getPurposesTypes() example output:

{
  "Functional": {
    "name": "Functionality",
    "description": "Personalization, autofilled forms, etc.",
    "defaultConsent": false,
    "showInConsentManager": true,
    "configurable": true,
    "essential": false
  },
  "Analytics": {
    "name": "Analytics + Performance",
    "description": "Help us learn how our site is used and how it performs.",
    "defaultConsent": false,
    "showInConsentManager": true,
    "configurable": true,
    "essential": false
  },
  "Advertising": {
    "name": "Targeting / Advertising",
    "description": "Helps us and others serve ads relevant to you.",
    "defaultConsent": false,
    "showInConsentManager": true,
    "configurable": true,
    "essential": false
  },
  "Essential": {
    "name": "Essential",
    "description": "",
    "defaultConsent": true,
    "showInConsentManager": false,
    "configurable": false,
    "essential": true
  },
  "Unknown": {
    "name": "Unknown",
    "description": "",
    "defaultConsent": false,
    "showInConsentManager": false,
    "configurable": false,
    "essential": false
  }
}

ℹ️ You can find additional useful airgap command in our debugging and testing guide.