@revenuecat/purchases-ui-js
Version:
Web components for Paywalls. Powered by RevenueCat
194 lines (193 loc) • 7.48 kB
JavaScript
function getPaywallComponentChildNodes(node) {
switch (node.type) {
case "stack": {
const children = [...node.components];
if (node.badge) {
children.push(node.badge.stack);
}
return children;
}
case "button":
case "purchase_button":
case "redemption_button":
return [node.stack];
case "carousel":
return node.pages;
case "countdown": {
const children = [node.countdown_stack];
if (node.end_stack) {
children.push(node.end_stack);
}
return children;
}
case "footer":
return [node.stack];
case "input_multiple_choice":
case "input_single_choice":
case "input_option":
return [node.stack];
case "tabs":
return [node.control.stack, ...node.tabs];
case "tab":
return [node.stack];
case "tab_control_button":
return [node.stack];
case "timeline":
return node.items.flatMap((item) => {
const children = [item.icon, item.title];
if (item.description) {
children.push(item.description);
}
return children;
});
case "express_purchase_button":
return [
node.wallet_unknown_stack,
node.wallet_available_stack,
node.wallet_unavailable_stack,
];
default:
return [];
}
}
function findFirstPackageMatching(root, predicate) {
const stack = [root];
while (stack.length > 0) {
const node = stack.pop();
if (node.type === "package" && predicate(node)) {
return node;
}
const children = getPaywallComponentChildNodes(node);
for (let i = children.length - 1; i >= 0; i--) {
stack.push(children[i]);
}
}
return undefined;
}
/**
* Given an instance of PaywallData, returns the id of the first package marked as `is_selected_by_default` if any.
* If none are marked, returns the first package encountered in traversal order (root stack, then sticky footer).
* @param paywallData
* @returns the id of the first package marked as `is_selected_by_default`, otherwise the first package id, or undefined
*/
export function findSelectedPackageId({ stack, sticky_footer, }) {
const defaultPkg = findFirstPackageMatching(stack, (pkg) => pkg.is_selected_by_default);
if (defaultPkg !== undefined) {
return defaultPkg.package_id;
}
const firstPkg = findFirstPackageMatching(stack, () => true);
if (firstPkg !== undefined) {
return firstPkg.package_id;
}
if (sticky_footer != null) {
const stickyDefault = findFirstPackageMatching(sticky_footer, (pkg) => pkg.is_selected_by_default);
if (stickyDefault !== undefined) {
return stickyDefault.package_id;
}
const stickyFirst = findFirstPackageMatching(sticky_footer, () => true);
if (stickyFirst !== undefined) {
return stickyFirst.package_id;
}
}
return undefined;
}
export const getActiveStateProps = (selectedState, overrides) => {
if (!selectedState) {
return {};
}
const override = overrides?.find((override) => override.conditions.find((condition) => condition.type === "selected"));
return override?.properties ?? {};
};
export const getHoverStateProps = (hoverState, overrides) => {
if (!hoverState) {
return {};
}
const override = overrides?.find((override) => override.conditions.find((condition) => condition.type === "hover"));
return override?.properties ?? {};
};
export const getFocusStateProps = (focusState, overrides) => {
if (!focusState) {
return {};
}
const override = overrides?.find((override) => override.conditions.find((condition) => condition.type === "focus"));
return override?.properties ?? {};
};
export const getErrorStateProps = (errorState, overrides) => {
if (!errorState) {
return {};
}
const override = overrides?.find((override) => override.conditions.find((condition) => condition.type === "error"));
return override?.properties ?? {};
};
export const getIntroOfferStateProps = (hasIntroOffer, overrides) => {
if (!hasIntroOffer) {
return {};
}
const override = overrides?.find((override) => override.conditions.find((condition) => condition.type === "intro_offer"));
return override?.properties ?? {};
};
export const getPromoOfferStateProps = (hasPromoOffer, overrides) => {
if (!hasPromoOffer) {
return {};
}
const override = overrides?.find((override) => override.conditions.find((condition) => condition.type === "promo_offer"));
return override?.properties ?? {};
};
/**
* Evaluates visibility for an element.
*
* A component is considered visible unless:
* - baseVisible is explicitly false, or
* - an override sets `visible: false` in its properties and all of its
* conditions are satisfied by the current context.
*
* If baseVisible is undefined or null, the component defaults to visible.
*/
export const evaluateVisibilityConditions = (context, overrides, baseVisible) => {
if (baseVisible === false)
return false;
const hidingOverride = overrides?.find((override) => {
const properties = override.properties;
if (properties.visible !== false)
return false;
return override.conditions.every((condition) => conditionMatches(condition, context));
});
return !hidingOverride;
};
const conditionMatches = (condition, context) => {
if (condition.type === "intro_offer_condition") {
const hasIntroOffer = !!context.packageInfo?.hasIntroOffer || !!context.packageInfo?.hasTrial;
if (condition.operator === "=")
return hasIntroOffer === condition.value;
if (condition.operator === "!=")
return hasIntroOffer !== condition.value;
}
if (condition.type === "promo_offer_condition") {
const hasPromoOffer = !!context.packageInfo?.hasPromoOffer;
if (condition.operator === "=")
return hasPromoOffer === condition.value;
if (condition.operator === "!=")
return hasPromoOffer !== condition.value;
}
if (condition.type === "selected_package_condition") {
const selected = context.selectedPackageId ?? "";
if (condition.operator === "in")
return condition.packages.includes(selected);
if (condition.operator === "not in")
return !condition.packages.includes(selected);
}
if (condition.type === "variable_condition") {
const variableName = condition.variable.replace(/^\$?custom\./, "");
const currentValue = context.variables[variableName];
// API sends value as a plain scalar; type definition wraps it in CustomVariableValue
const rawValue = condition.value;
const conditionValue = String(typeof rawValue === "object" && rawValue !== null && "value" in rawValue
? rawValue.value
: rawValue);
if (condition.operator === "=")
return currentValue === conditionValue;
if (condition.operator === "!=")
return currentValue !== conditionValue;
}
return false;
};