@revenuecat/purchases-ui-js
Version:
Web components for Paywalls. Powered by RevenueCat
195 lines (194 loc) • 8.12 kB
JavaScript
import { DEFAULT_FORM_COLORS, DEFAULT_INFO_COLORS, FormColorsToBrandingAppearanceMapping, InfoColorsToBrandingAppearanceMapping, } from "./colors";
import { DefaultShape, PillsShape, RectangularShape, RoundedShape, } from "./shapes";
import { DEFAULT_FONT_FAMILY } from "./text";
const hexToRGB = (color) => {
if (color.length == 7)
return {
r: parseInt(color.slice(1, 3), 16),
g: parseInt(color.slice(3, 5), 16),
b: parseInt(color.slice(5, 7), 16),
};
if (color.length == 4)
return {
r: parseInt(color[1], 16),
g: parseInt(color[2], 16),
b: parseInt(color[3], 16),
};
return null;
};
const isLightColor = ({ r, g, b, luminanceThreshold, }) => {
// Gamma correction
const gammaCorrect = (color) => {
color = color / 255;
return color <= 0.03928
? color / 12.92
: Math.pow((color + 0.055) / 1.055, 2.4);
};
// Calculate relative luminance with gamma correction
const luminance = 0.2126 * gammaCorrect(r) +
0.7152 * gammaCorrect(g) +
0.0722 * gammaCorrect(b);
// Return whether the background is light
return luminance > luminanceThreshold;
};
const DEFAULT_LUMINANCE_THRESHOLD = 0.37;
const rgbToTextColors = (rgb, luminanceThreshold = DEFAULT_LUMINANCE_THRESHOLD) => {
const baseColor = isLightColor({ ...rgb, luminanceThreshold })
? "0,0,0"
: "255,255,255";
return {
"grey-text-dark": `rgb(${baseColor})`,
"grey-text-light": `rgba(${baseColor},0.70)`,
"grey-ui-dark": `rgba(${baseColor},0.3)`,
"grey-ui-light": `rgba(${baseColor},0.1)`,
};
};
function overlayColor(baseColor, overlay, alpha) {
const base = hexToRGB(baseColor) || { r: 0, g: 0, b: 0 };
const over = hexToRGB(overlay) || { r: 255, g: 255, b: 255 };
const r = Math.round(over.r * alpha + base.r * (1 - alpha));
const g = Math.round(over.g * alpha + base.g * (1 - alpha));
const b = Math.round(over.b * alpha + base.b * (1 - alpha));
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
/**
* Applies an alpha value to a color.
* If the base color is light, the overlay color is black.
* If the base color is dark, the overlay color is white.
*/
function applyAlpha(baseColor, alpha) {
const defaultRgb = { r: 255, g: 255, b: 255 };
const normalizedAlpha = Math.max(0, Math.min(1, alpha));
let appliedBaseColor = baseColor;
let baseRgb = hexToRGB(baseColor) || defaultRgb;
if (isNaN(baseRgb.r) || isNaN(baseRgb.g) || isNaN(baseRgb.b)) {
baseRgb = defaultRgb;
appliedBaseColor = "#FFFFFF";
}
const baseIsLight = isLightColor({
...baseRgb,
luminanceThreshold: DEFAULT_LUMINANCE_THRESHOLD,
});
const overlay = baseIsLight ? "#000000" : "#FFFFFF";
return overlayColor(appliedBaseColor, overlay, normalizedAlpha);
}
function toHex(val) {
return val.toString(16).padStart(2, "0").toUpperCase();
}
const textColorsForBackground = (backgroundColor, primaryColor, primaryTextColorOverride, defaultColors, luminanceThreshold = DEFAULT_LUMINANCE_THRESHOLD) => {
const textColors = {
"grey-text-dark": defaultColors["grey-text-dark"],
"grey-text-light": defaultColors["grey-text-light"],
"grey-ui-dark": defaultColors["grey-ui-dark"],
"grey-ui-light": defaultColors["grey-ui-light"],
"primary-text": defaultColors["primary-text"],
};
// Find the text colors for the background
if (backgroundColor?.startsWith("#")) {
const rgb = hexToRGB(backgroundColor);
if (rgb !== null) {
Object.assign(textColors, rgbToTextColors(rgb));
}
}
// Find the text color for the primary color
if (primaryTextColorOverride) {
textColors["primary-text"] = primaryTextColorOverride;
}
else if (primaryColor?.startsWith("#")) {
const rgb = hexToRGB(primaryColor);
if (rgb !== null) {
textColors["primary-text"] = isLightColor({ ...rgb, luminanceThreshold })
? "black"
: "white";
}
}
return textColors;
};
const colorsForButtonStates = (primaryColor) => {
return {
"primary-hover": applyAlpha(primaryColor, 0.1),
"primary-pressed": applyAlpha(primaryColor, 0.15),
};
};
const fallback = (somethingNullable, defaultValue) => {
return somethingNullable ? somethingNullable : defaultValue;
};
const mapColors = (colorsMapping, defaultColors, brandingAppearance) => {
const mappedColors = Object.entries(colorsMapping).map(([target, source]) => [
target,
fallback(brandingAppearance
? brandingAppearance[source]
: null, defaultColors[target]),
]);
return Object.fromEntries(mappedColors);
};
const toColors = (colorsMapping, defaultColors, brandingAppearance) => {
const mappedColors = mapColors(colorsMapping, defaultColors, brandingAppearance);
return brandingAppearance
? {
...defaultColors,
...mappedColors,
...textColorsForBackground(mappedColors.background, mappedColors.primary, brandingAppearance.color_buttons_primary_text, defaultColors),
...colorsForButtonStates(mappedColors.primary),
}
: { ...defaultColors }; //copy, do not reference.
};
const toProductInfoColors = (brandingAppearance) => {
return toColors(InfoColorsToBrandingAppearanceMapping, DEFAULT_INFO_COLORS, brandingAppearance);
};
export const toFormColors = (brandingAppearance) => {
return toColors(FormColorsToBrandingAppearanceMapping, DEFAULT_FORM_COLORS, brandingAppearance);
};
export const toShape = (brandingAppearance) => {
if (!brandingAppearance) {
return DefaultShape;
}
switch (brandingAppearance.shapes) {
case "rounded":
return RoundedShape;
case "rectangle":
return RectangularShape;
case "pill":
return PillsShape;
default:
return DefaultShape;
}
};
const toStyleVar = (prefix = "", entries) => entries.map(([key, value]) => `--rc-${prefix}-${key}: ${value}`).join("; ");
/**
* Assigns values to the css variables given the branding appearance customization.
* @param appearance BrandingAppearance
* @return a style parameter compatible string.
*/
export const toProductInfoStyleVar = (appearance) => {
const colorVariablesString = toStyleVar("color", Object.entries(toProductInfoColors(appearance)));
const shapeVariableString = toStyleVar("shape", Object.entries(toShape(appearance)));
return [colorVariablesString, shapeVariableString].join("; ");
};
/**
* Assigns values to the css variables given the branding appearance customization.
* @param appearance BrandingAppearance
* @return a style parameter compatible string.
*/
export const toFormStyleVar = (appearance) => {
const colorVariablesString = toStyleVar("color", Object.entries(toFormColors(appearance)));
const shapeVariableString = toStyleVar("shape", Object.entries(toShape(appearance)));
return [colorVariablesString, shapeVariableString].join("; ");
};
/**
* Convert text styles into CSS variables for both desktop and mobile.
*/
export const toTextStyleVar = (prefix = "", textStyles) => Object.entries(textStyles)
.flatMap(([key, { desktop, mobile }]) => [
`--rc-${prefix}-${key}-desktop: normal normal ${desktop.fontWeight} ${desktop.fontSize}/${desktop.lineHeight} ${DEFAULT_FONT_FAMILY}`,
`--rc-${prefix}-${key}-mobile: normal normal ${mobile.fontWeight} ${mobile.fontSize}/${mobile.lineHeight} ${DEFAULT_FONT_FAMILY}`,
`--rc-${prefix}-${key}-desktop-font-size: ${desktop.fontSize}`,
`--rc-${prefix}-${key}-mobile-font-size: ${mobile.fontSize}`,
])
.join("; ");
/**
* Generates CSS variables for the spacing system.
*/
export const toSpacingVars = (prefix = "", spacing) => Object.entries(spacing)
.map(([key, { mobile, desktop }]) => `--rc-${prefix}-${key}-mobile: ${mobile}; --rc-${prefix}-${key}-desktop: ${desktop};`)
.join(" ");