React Snippets

This documentation site uses React and Next.js. Here are some snippets and recipes for how use airgap.js in this codebase:

lib/hooks/useConsentManager.tsx:

import { createContext, useContext, useEffect, useState } from 'react';
import Script from 'next/script';
import type {
  AirgapAPI,
  PreInitTranscendAPI,
  TranscendAPI,
} from '@transcend-io/airgap.js-types';

/** A stub for `window.airgap` to be used before airgap is actually loaded */
type PreInitAirgapAPI = Required<Pick<AirgapAPI, 'readyQueue' | 'ready'>>;

/**
 * Add `window.airgap` and `window.transcend` to the global namespace
 */
declare global {
  interface Window {
    /** airgap.js interface */
    airgap?: PreInitAirgapAPI | AirgapAPI;
    /** Transcend consent manager interface */
    transcend?: PreInitTranscendAPI | TranscendAPI;
  }
}

/**
 * The `airgap` and `transcend` consent management APIs
 *
 * @example const { airgap, transcend } = useConsentManager();
 */
interface ConsentAPI {
  /** The `airgap` consent management instance */
  airgap?: AirgapAPI;
  /** The `transcend` consent manager UI instance */
  transcend?: TranscendAPI;
}

interface ConsentProviderProps {
  /** The children of this context provider */
  children: React.ReactNode;
  /** The src URL from the Transcend CDN */
  airgapSrc: string;
}

/** The React context by the `useConsentManager()` hook */
export const ConsentContext = createContext<ConsentAPI>({});

/**
 * React context provider for `window.airgap` and `window.transcend`
 * @see https://docs.transcend.io/docs/consent/faq
 */
export const ConsentProvider: React.FC<ConsentProviderProps> = ({
  children,
  airgapSrc,
}) => {
  const [airgap, setAirgap] = useState<AirgapAPI | undefined>();
  const [transcend, setTranscend] = useState<TranscendAPI | undefined>();

  // `useEffect` ensures this is only executed in browser
  useEffect(() => {
    // Stub transcend with PreInit API
    if (!self.transcend?.ready) {
      const preInitTranscend: PreInitTranscendAPI = {
        readyQueue: [],
        ready(callback) {
          this.readyQueue.push(callback);
        },
        ...self.transcend,
      };
      self.transcend = preInitTranscend;
    }

    // Stub airgap with PreInit API
    if (!self.airgap?.ready) {
      const preInitAirgap: PreInitAirgapAPI = {
        readyQueue: [],
        ready(callback) {
          this.readyQueue.push(callback);
        },
        ...self.airgap,
      };
      self.airgap = preInitAirgap;
    }

    // Wait for consent manager UI to load, and set it in the React state
    if (!transcend) {
      self.transcend.ready((transcend) => {
        setTranscend(transcend);
      });
    }

    // Wait for airgap.js core to load, and set it in the React state
    if (!airgap) {
      self.airgap.ready((airgap) => {
        setAirgap(airgap);
      });
    }
  }, [airgap, transcend]);

  return (
    <>
      <Script src={airgapSrc} />
      <ConsentContext.Provider value={{ airgap, transcend }}>
        {children}
      </ConsentContext.Provider>
    </>
  );
};

export const useConsentManager = (): ConsentAPI => useContext(ConsentContext);

And wrap your application in the consent provider.

pages/_app.tsx:

import { ConsentProvider } from '../lib/hooks/useConsentManager';

export default function MyApp({ Component, pageProps }) {
  return (
    // Replace `airgapSrc` with your bundle URL
    <ConsentProvider airgapSrc="https://transcend-cdn.com/cm/f8d8c73f-3189-4ab4-c138-fa9922f76812/airgap.js">
      <Component {...pageProps} />;
    </ConsentProvider>
  );
}

Now your components can interact with window.airgap and the consent manager UI (window.transcend) by calling const { transcend, airgap } = useConsentManager();. See the next snippet for an example!

Beware: you are now importing airgap.js asynchronously. Since airgap.js can only regulate network traffic after it is loaded, you are now responsible for ensuring no trackers load before airgap.js! Here is an example of how you could asynchronously load @segment/analytics-next using your new useConsentManager hook:

import { AnalyticsBrowser } from '@segment/analytics-next';

const analytics = new AnalyticsBrowser();

export const AnalyticsProvider: React.FC<{ children: ReactNode }> = ({
  children,
}) => {
  const { airgap } = useConsentManager();

  // Load analytics.js
  useEffect(() => {
    // Make sure Airgap is enabled before attempting the import
    if (airgap) {
      const writeKey = process.env.NEXT_PUBLIC_SEGMENT_WRITE_KEY;
      // Load Segment analytics
      analytics.load({ writeKey });
    }
  }, [airgap]);

  // ...
};

This snippet is based on the Next.js analytics-next example.

Add California's CCPA/CPRA "Your Privacy Choices" opt-out icon to your website footer (replacing the "Do Not Sell or Share My Personal Information" link).

components/YourPrivacyChoices.tsx:

import React, { useEffect, useState } from 'react';

import { useConsentManager } from '../lib/hooks/consent-manager';
/**
 * This import assumes you are using `@svgr/webpack` to load SVGs as React components
 * @see https://www.npmjs.com/package/@svgr/webpack
 * @see https://react-svgr.com/
 */
import PrivacyChoicesIcon from './privacy-choices-icon.svg';

export interface YourPrivacyChoicesProps {}

export const YourPrivacyChoicesInterior: React.FC<
  JSX.IntrinsicElements['button']
> = ({ onClick }) => {
  return (
    <>
      <button onClick={onClick} className="privacy-choices-button">
        <div className="privacy-choices-button-layout">
          <PrivacyChoicesIcon className="privacy-choices-icon" />
          <span>Your Privacy Choices</span>
        </div>
      </button>
      <style jsx>{`
        .privacy-choices-button-layout {
          display: flex;
          align-items: center;
        }

        .privacy-choices-button {
          all: unset;
        }

        .privacy-choices-button {
          height: var(--lineHeight);
          cursor: pointer;
        }

        .privacy-choices-button-layout :global(.privacy-choices-icon) {
          height: 0.9em;
          padding-right: 0.4em;
        }
      `}</style>
    </>
  );
};

/**
 * CPRA Your Privacy Choices button
 *
 * @see https://cppa.ca.gov/regulations/pdf/20221102_mod_text.pdf
 * @see https://oag.ca.gov/privacy/ccpa/icons-download
 */
export const YourPrivacyChoices: React.FC<YourPrivacyChoicesProps> = ({}) => {
  const { transcend, airgap } = useConsentManager();
  const [consentEnabled, setConsentEnabled] = useState<boolean>(false);

  useEffect(() => {
    // Show the 'Your Privacy Choices' button based on `airgap.getRegimes`
    function getConsentEnabled(): boolean {
      if (!airgap) {
        return false;
      }
      const regimes = airgap.getRegimes();

      // We display this button if CPRA is present in the detected regimes
      const consentEnabled =
        typeof regimes !== 'undefined' && regimes.has('CPRA');

      return consentEnabled;
    }
    setConsentEnabled(getConsentEnabled());
  }, [airgap]);

  if (!consentEnabled) {
    return null;
  }

  return (
    <YourPrivacyChoicesInterior
      onClick={() =>
        transcend?.showConsentManager({ viewState: 'CompleteOptions' })
      }
    />
  );
};

And lastly, here is a download link for a compressed + optimized SVG of the CPRA opt out icon (imported above as './privacy-choices-icon.svg')