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';
// Add type for Airgap before initialization
type PreInitAirgapAPI = Required<Pick<AirgapAPI, 'readyQueue' | 'ready'>>;
declare global {
interface Window {
/** airgap.js interface */
airgap?: PreInitAirgapAPI | AirgapAPI;
/** Transcend Consent Management interface */
transcend?: PreInitTranscendAPI | TranscendAPI;
}
}
interface ConsentAPI {
airgap?: AirgapAPI;
transcend?: TranscendAPI;
}
interface ConsentProviderProps {
children: React.ReactNode;
airgapSrc: string;
}
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>(undefined);
const [transcend, setTranscend] = useState<TranscendAPI | undefined>(
undefined
);
// `useEffect` ensures this is only executed in browser
useEffect(() => {
// Stub transcend with PreInit API
if (!window.transcend) {
const preInitTranscend: PreInitTranscendAPI = {
readyQueue: [],
ready(callback) {
this.readyQueue.push(callback);
},
...(window.transcend || {}),
};
window.transcend = preInitTranscend;
}
if (!window.airgap) {
const preInitAirgap: PreInitAirgapAPI = {
readyQueue: [],
ready(callback) {
this.readyQueue.push(callback);
},
...(window.airgap || {}),
};
window.airgap = preInitAirgap;
}
// Wait for airgap.js core to load, and set it in the React state
window.airgap.ready((airgap) => {
setAirgap(airgap);
});
// Wait for consent manager UI to load, and set it in the React state
window.transcend.ready((transcend) => {
setTranscend(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://cdn.transcend.io/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')