@revenuecat/purchases-ui-js
Version:
Web components for Paywalls. Powered by RevenueCat
415 lines (414 loc) • 14.5 kB
JavaScript
import { DEFAULT_COLOR_MODE, DEFAULT_TEXT_COLOR, } from "./constants.js";
import { FontSizes, FontSizeTags, FontWeights, StackAlignment, StackDirection, StackDistribution, TextAlignments, } from "../types.js";
/**
* Generates CSS spacing styles for margin or padding
* @param spacing - The spacing object containing top, trailing, bottom, and leading values
* @param spacingKey - The type of spacing ('margin' or 'padding')
* @returns CSS style object with logical properties for spacing
*/
function getSpacingStyle(spacing, spacingKey) {
const styles = {
[`--${spacingKey}-block-start`]: "0px",
[`--${spacingKey}-inline-end`]: "0px",
[`--${spacingKey}-block-end`]: "0px",
[`--${spacingKey}-inline-start`]: "0px",
};
if (!spacing || !spacingKey)
return styles;
Object.assign(styles, {
[`--${spacingKey}-block-start`]: `${spacing.top ?? 0}px`,
[`--${spacingKey}-inline-end`]: `${spacing.trailing ?? 0}px`,
[`--${spacingKey}-block-end`]: `${spacing.bottom ?? 0}px`,
[`--${spacingKey}-inline-start`]: `${spacing.leading ?? 0}px`,
});
return styles;
}
/**
* Maps font size to appropriate HTML heading tag
* @param fontSize - Key from FontSizeTags enum
* @returns Corresponding HTML heading tag
*/
export function getTextComponentTag(fontSize) {
return FontSizeTags[fontSize];
}
/**
* Gets color value based on color mode with fallback
* @param params - Object containing color map, mode and fallback color
* @returns Color value as string
*/
export function getColor({ colorMap, colorMode = DEFAULT_COLOR_MODE, fallback = "FFFFFF", }) {
if (!colorMap)
return fallback;
const color = colorMap[colorMode] || colorMap[DEFAULT_COLOR_MODE];
let colorPoints = "";
switch (color.type) {
case "hex":
case "alias":
return color.value ?? fallback;
case "linear":
colorPoints = (color.points || [])
.map((point) => `${point.color} ${point.percent}%`)
.join(", ");
return `linear-gradient(${color.degrees}deg, ${colorPoints})`;
case "radial":
colorPoints = (color.points || [])
.map((point) => `${point.color} ${point.percent}%`)
.join(", ");
return `radial-gradient(${colorPoints})`;
default:
return fallback;
}
}
/**
* Generates CSS border style string
* @param border - Border configuration object
* @param colorMode - Color mode (light/dark)
* @returns CSS border style string
*/
export function getBorderStyle(border, colorMode = DEFAULT_COLOR_MODE) {
if (!border)
return "";
const color = getColor({ colorMap: border.color, colorMode });
return `${border.width}px solid ${color}`;
}
/**
* Generates CSS border radius style for corners
* @param corners - Corner radius configuration
* @returns CSS border radius string
*/
export function getCornerRadiusStyle(corners) {
return {
"--border-start-start-radius": `${corners.top_leading}px`,
"--border-start-end-radius": `${corners.top_trailing}px`,
"--border-end-start-radius": `${corners.bottom_leading}px`,
"--border-end-end-radius": `${corners.bottom_trailing}px`,
};
}
/**
* Generates comprehensive component styles including spacing, colors, borders and shadows
* @param params - Component style configuration object
* @returns CSS style object with component styles
*/
export function getComponentStyles({ background_color, border, margin, padding, color, colorMode = DEFAULT_COLOR_MODE, shape, shadow, }) {
const stylesObject = {
"--margin-block-start": "0px",
"--margin-inline-end": "0px",
"--margin-block-end": "0px",
"--margin-inline-start": "0px",
"--padding-block-start": "0px",
"--padding-inline-end": "0px",
"--padding-block-end": "0px",
"--padding-inline-start": "0px",
"--background": "initial",
"--text-color": "initial",
"--border": "none",
"--border-end-start-radius": "0px",
"--border-end-end-radius": "0px",
"--border-start-start-radius": "0px",
"--border-start-end-radius": "0px",
"--shadow": "none",
};
if (padding) {
Object.assign(stylesObject, getSpacingStyle(padding, "padding"));
}
if (margin) {
Object.assign(stylesObject, getSpacingStyle(margin, "margin"));
}
if (background_color) {
stylesObject["--background"] = getColor({
colorMap: background_color,
colorMode,
fallback: "transparent",
});
}
if (color) {
stylesObject["--text-color"] = getColor({
colorMap: color,
colorMode,
fallback: DEFAULT_TEXT_COLOR,
});
}
if (border) {
stylesObject["--border"] = getBorderStyle(border, colorMode);
}
if (shape?.type === "rectangle" && shape.corners) {
Object.assign(stylesObject, getCornerRadiusStyle(shape.corners));
}
if (shape?.type === "pill") {
Object.assign(stylesObject, getCornerRadiusStyle({
bottom_leading: 9999,
bottom_trailing: 9999,
top_leading: 9999,
top_trailing: 9999,
}));
}
if (shadow) {
stylesObject["--shadow"] = `${shadow.x}px ${shadow.y}px ${shadow.radius}px
${getColor({ colorMap: shadow.color, colorMode, fallback: DEFAULT_TEXT_COLOR })}`;
}
return stylesObject;
}
function getSizeValue(size) {
if (size.type === "fixed") {
return `${size.value}px`;
}
if (size.type === "fit") {
return "fit-content";
}
if (size.type === "fill") {
const userAgent = navigator.userAgent;
const isFirefox = userAgent.match(/firefox|fxios/i);
return isFirefox ? "-moz-available" : "-webkit-fill-available";
}
return "initial";
}
/**
* Generates size-related CSS styles for components
* @param size - Size configuration object
* @returns CSS style object with size properties
*/
export function getSizeStyle(size) {
const styles = {
"--width": "initial",
"--height": "initial",
"--flex": "initial",
};
const width = getSizeValue(size.width);
const height = getSizeValue(size.height);
const isGrow = size.width.type === "fill" || size.height.type === "fill";
Object.assign(styles, {
"--width": width,
"--height": height,
"--flex": isGrow ? "initial" : "0 1 auto",
});
return styles;
}
export function getInsetStyles(dimension) {
const defaultStyles = {
"--inset": "initial",
"--transform": "initial",
};
switch (dimension.alignment) {
case "top_leading":
defaultStyles["--inset"] = "0 auto auto 0";
break;
case "top":
defaultStyles["--inset"] = "auto auto auto 50%";
defaultStyles["--transform"] = "translate(-50%, 0)";
break;
case "top_trailing":
defaultStyles["--inset"] = "0 0 auto auto";
break;
case "leading":
defaultStyles["--inset"] = "50% 0 50% 0";
defaultStyles["--transform"] = "translate(0, -50%)";
break;
case "center":
defaultStyles["--inset"] = "50% auto auto 50%";
defaultStyles["--transform"] = "translate(-50%, -50%)";
break;
case "trailing":
defaultStyles["--inset"] = "50% 0 50% auto";
defaultStyles["--transform"] = "translate(0, -50%)";
break;
case "bottom_leading":
defaultStyles["--inset"] = "auto auto 0 0";
break;
case "bottom":
defaultStyles["--inset"] = "auto 50% 0 auto";
defaultStyles["--transform"] = "translate(50%, 0)";
break;
case "bottom_trailing":
defaultStyles["--inset"] = "auto 0 0 auto";
break;
}
return defaultStyles;
}
/**
* Generates dimension-related styles for stack components
* @param dimension - Dimension configuration object
* @returns CSS style object with flex layout properties
*/
export function getDimensionStyle(dimension) {
const styles = {
"--direction": "initial",
"--alignment": "initial",
"--distribution": "initial",
"--position": "relative",
"--inset": "initial",
"--transform": "initial",
};
if (dimension.type !== "zlayer") {
Object.assign(styles, {
"--direction": StackDirection[dimension.type],
"--alignment": StackAlignment[dimension.alignment],
});
if (dimension.distribution) {
Object.assign(styles, {
"--distribution": StackDistribution[dimension.distribution],
});
}
}
return styles;
}
/**
* Generates text-related styles
* @param props - Text component properties
* @param colorMode - The currently selected ColorMode (dark/light)
* @returns CSS style object with text formatting properties
*/
export function getTextStyles(props, colorMode = DEFAULT_COLOR_MODE) {
const { font_size, horizontal_alignment, font_weight, font_name, color } = props;
const styles = {
"--text-align": "initial",
"--font-weight": "initial",
"--font-size": "initial",
"--font-family": "sans-serif",
"--background-clip": "none",
"--text-fill-color": "none",
"--background": "unset",
};
Object.assign(styles, {
"--text-align": TextAlignments[horizontal_alignment] || TextAlignments.leading,
"--font-weight": FontWeights[font_weight] || FontWeights.regular,
"--font-size": FontSizes[font_size] || FontSizes.body_m,
"--font-family": font_name || "sans-serif",
});
if (color &&
(color[colorMode]?.type === "linear" || color[colorMode]?.type === "radial")) {
Object.assign(styles, {
"--background-clip": "text",
"--text-fill-color": "transparent",
"--background": getColor({ colorMap: color, colorMode }),
});
}
return styles;
}
/**
* Converts a style object to a CSS string
* @param styles - Object containing CSS properties and values
* @returns CSS string
*/
export function stringifyStyles(styles) {
return Object.entries(styles)
.map(([key, value]) => `${key}: ${value}`)
.join("; ");
}
/**
* Given an instance of PaywallData, returns the id of the first package marked as `is_selected_by_default` if any.
* @param paywallData
* @returns the id of the first package marked as `is_selected_by_default` or undefined
*/
export function findSelectedPackageId(paywallData) {
const traverseNode = (node) => {
if (node.type === "package" &&
node.is_selected_by_default) {
return node;
}
if (node.components && Array.isArray(node.components)) {
for (const c of node.components) {
const pkg = traverseNode(c);
if (pkg) {
return pkg;
}
}
}
if (node.stack !== undefined) {
const pkg = traverseNode(node.stack);
if (pkg) {
return pkg;
}
}
return undefined;
};
const p = traverseNode(paywallData.components_config.base.stack);
if (p === undefined) {
return undefined;
}
return p.package_id;
}
export const getActiveStateProps = (overrides, componentState) => {
if (!componentState)
return {};
const activeStateKeys = getComponentActiveStateKeys(componentState);
const activeStateProps = activeStateKeys.reduce((props, key) => {
if (overrides) {
const styles = overrides?.states?.[key] || {};
return { ...props, ...styles };
}
return props;
}, {});
return activeStateProps;
};
const getComponentActiveStateKeys = (componentState) => {
if (!componentState)
return [];
const stateKeys = Object.entries(componentState).reduce((activeStates, [stateKey, stateValue]) => {
if (stateValue)
activeStates.push(stateKey);
return activeStates;
}, []);
return stateKeys;
};
export function prefixObject(object, prefix) {
if (!object)
return {};
if (!prefix)
return object;
return Object.entries(object).reduce((acc, [key, value]) => {
const replacedKey = key.replace(/^--/, `${prefix}-`);
acc[`--${replacedKey}`] = value;
return acc;
}, {});
}
export function getMaskPath(props) {
const { mask_shape: maskShape, imageAspectRatio } = props;
let maskPath = "";
if (maskShape?.type === "concave") {
maskPath = `M 0 0
H 100
V ${imageAspectRatio * 100}
Q 50 ${imageAspectRatio * 80} 0 ${imageAspectRatio * 100}
Z`;
}
else if (maskShape?.type === "convex") {
maskPath = `M 0 0
H 100
V ${imageAspectRatio * 80}
Q 50 ${imageAspectRatio * 120} 0 ${imageAspectRatio * 80}
Z`;
}
else {
maskPath = `M 0 0 H 100 V ${imageAspectRatio * 100} H 0 Z`;
}
return maskPath;
}
/**
* Generates mask styles for images
* @param maskShape - Shape configuration for image mask
* @returns CSS style object with mask properties
*/
export const getMaskStyle = (maskShape) => {
const maskStyles = {
"--border-end-start-radius": "0px",
"--border-end-end-radius": "0px",
"--border-start-start-radius": "0px",
"--border-start-end-radius": "0px",
};
if (maskShape?.corners) {
Object.assign(maskStyles, getCornerRadiusStyle(maskShape.corners));
}
return maskStyles;
};
export function getLinearGradientAngle(props) {
if (props.color_overlay?.[props.purchaseState.colorMode]?.type !== "linear") {
return { x1: "0%", y1: "0%", x2: "0%", y2: "0%" };
}
const { color_overlay: colorOverlay } = props;
const angle = colorOverlay?.[DEFAULT_COLOR_MODE]?.degrees || 0;
const x1 = "50%";
const y1 = "0%";
const x2 = `${Math.round(50 + Math.sin(((angle + 90) * Math.PI) / 90) * 50)}%`;
const y2 = `${Math.round(50 - Math.cos(((angle + 90) * Math.PI) / 90) * 50)}%`;
return { x1, y1, x2, y2 };
}