@transcend-io/consent-manager-ui
Version:
Transcend Consent Manager reference consent UI
217 lines (198 loc) • 6.13 kB
text/typescript
/**
* getMergedConfig() returns the final config for the UI: { ...baseConfig, ...bundleConfig, ...scriptConfig }
*/
import type {
ConsentManagerConfig,
ConsentManagerConfigInput,
ExperienceToInitialViewState,
} from '@transcend-io/airgap.js-types';
import { ViewState } from '@transcend-io/airgap.js-types/build/enums/viewState';
import { CONSENT_MANAGER_SUPPORTED_LANGUAGES } from './i18n';
import { logger } from './logger';
import { settings, LOG_LEVELS, extraConfig } from './settings';
import { jsonParseSafe } from './utils/safe-json-parse';
import { MergedConsentManagerConfig } from './types';
const {
privacyCenter,
privacyPolicy = privacyCenter || '/privacy',
secondaryPolicy,
languages,
dismissedViewState = 'Hidden',
nonce,
inlineCss,
} = settings;
/**
* This enum is copied to avoid airgap.js-types
* being a production dependency for this package.
* TODO: https://transcend.height.app/T-20982 - consider
* a simpler option, such as a dedicated package for constants
*/
export const DEFAULT_VIEW_STATE_BY_PRIVACY_REGIME_COPIED: ExperienceToInitialViewState =
{
// EU
GDPR: 'TCF_EU',
// Brazil
LGPD: 'QuickOptions',
// US: California
CPRA: 'Hidden',
// US: Virginia
CDPA: 'Hidden',
// US: Colorado
CPA: 'Hidden',
// US
nFADP: 'Hidden',
// US: Nevada
NEVADA_SB220: 'Hidden',
// Other
Unknown: 'Hidden',
};
// Base configuration
const baseConfig: Omit<
ConsentManagerConfig,
'privacyPolicy' | 'dismissedViewState' | 'secondaryPolicy'
> = {
css: '',
messages: '',
theme: {
primaryColor: '#3333FF',
fontColor: '#010101',
},
breakpoints: {
tablet: '640px',
desktop: '1024px',
},
initialViewStateByPrivacyRegime: DEFAULT_VIEW_STATE_BY_PRIVACY_REGIME_COPIED,
};
/**
* Merges config from defaults and settings. JSON is automatically decoded.
*
* @returns the consent manager config to use in the UI
*/
export function getMergedConfig(): MergedConsentManagerConfig {
const settingsConfig: ConsentManagerConfigInput =
typeof settings === 'string'
? jsonParseSafe(settings, () => ({}))
: settings || {};
const settingsConfigInitialViewStateByPrivacyRegime =
settingsConfig?.initialViewStateByPrivacyRegime;
// Skip initialViewStateByPrivacyRegime config in settings if empty
if (
settingsConfigInitialViewStateByPrivacyRegime &&
Object.keys(settingsConfigInitialViewStateByPrivacyRegime).length === 0
) {
delete settingsConfig?.initialViewStateByPrivacyRegime;
}
// These consent manager settings can be configured through our backend or ag-bundler/config/{site}.json
const config: ConsentManagerConfig = {
...baseConfig,
...settingsConfig,
...extraConfig,
} as ConsentManagerConfig;
config.privacyPolicy ??= privacyPolicy;
config.secondaryPolicy ??= secondaryPolicy;
config.dismissedViewState ??= dismissedViewState;
config.languages ??= languages;
if (typeof config.initialViewStateByPrivacyRegime === 'string') {
config.initialViewStateByPrivacyRegime = JSON.parse(
config.initialViewStateByPrivacyRegime,
);
}
// Determine the language settings to use
const existingLanguages = config.languages
? config.languages.trim().split(/\s*,\s*/)
: [];
const supportedLanguages = !config.languages
? CONSENT_MANAGER_SUPPORTED_LANGUAGES
: CONSENT_MANAGER_SUPPORTED_LANGUAGES.filter((lang) =>
existingLanguages.includes(lang),
);
const safeToContinue = validateConfig(config);
if (!safeToContinue) {
throw new Error('Invalid consent manager config');
}
return { config, supportedLanguages };
}
/**
* Validate recognized view states.
*
* @param viewState - the view state to validate
* @param param - Parameter name to use in error messages
* @param errors - list of errors to append to
* @returns whether viewState is a recognized view state
*/
const validateViewState = (
viewState: ViewState,
param: string,
errors: string[],
): boolean => {
const valid = Object.values(ViewState).some(
(knownViewState) => knownViewState === viewState,
);
if (!valid) {
errors.push(`Unrecognized ${param}: ${viewState}`);
}
return valid;
};
/**
* Validates the configuration
*
* @param config - the provided config to validate
* @returns a boolean on whether this is valid config
*/
function validateConfig(config: ConsentManagerConfig): boolean {
const errors: string[] = [];
const warnings: string[] = [];
/* Validators */
// Check that config is not empty
if (Object.keys(config).length === 0) {
errors.push('Consent manager UI config missing!');
}
const dismissedViewStateParam = 'dismissedViewState';
const initialViewStateByPrivacyRegimeParam =
'initialViewStateByPrivacyRegime';
const {
[dismissedViewStateParam]: dismissedViewState,
[initialViewStateByPrivacyRegimeParam]: initialViewStateByPrivacyRegime,
} = config;
if (dismissedViewState) {
validateViewState(dismissedViewState, dismissedViewStateParam, errors);
}
if (initialViewStateByPrivacyRegime) {
Object.values(initialViewStateByPrivacyRegime).forEach((viewState) => {
if (viewState) {
validateViewState(
viewState,
`${initialViewStateByPrivacyRegimeParam} map value`,
errors,
);
}
});
}
/* Logging */
if (warnings.length > 0 && LOG_LEVELS.has('warn')) {
// Log warnings
warnings.forEach((warning) => {
logger.warn(warning, config);
});
}
if (errors.length > 0) {
// Log errors and return false
if (LOG_LEVELS.has('error')) {
errors.forEach((error) => {
logger.error.styled('color: #686868', error, config);
});
}
return false;
}
return true;
}
export const CSP_NONCE = nonce;
if (CSP_NONCE) {
const currentScriptDataset = document.currentScript?.dataset;
if (currentScriptDataset) {
// hide nonce from other scripts
delete currentScriptDataset.nonce;
}
}
export const ALLOW_INLINE_CSS = inlineCss !== 'off';
export const EXTERNALIZE_INLINE_CSS = inlineCss === 'data:';