@revenuecat/purchases-ui-js 4.2.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -56,6 +56,18 @@
56
56
  }),
57
57
  );
58
58
 
59
+ const isVisible = $derived(
60
+ evaluateVisibilityConditions(
61
+ {
62
+ selectedPackageId: $selectedPackageId,
63
+ packageInfo: $packageInfo,
64
+ variables: $variables ?? {},
65
+ },
66
+ props.overrides,
67
+ props.visible,
68
+ ),
69
+ );
70
+
59
71
  const webpUrl = $derived(`${base_url}/${formats.webp}`);
60
72
  const icon = $derived(
61
73
  css({
@@ -75,18 +87,6 @@
75
87
  "-webkit-mask-repeat": "no-repeat",
76
88
  }),
77
89
  );
78
-
79
- const isVisible = $derived(
80
- evaluateVisibilityConditions(
81
- {
82
- selectedPackageId: $selectedPackageId,
83
- packageInfo: $packageInfo,
84
- variables: $variables ?? {},
85
- },
86
- props.overrides,
87
- props.visible,
88
- ),
89
- );
90
90
  </script>
91
91
 
92
92
  {#if isVisible}
@@ -15,6 +15,7 @@
15
15
  type VariablesStore,
16
16
  } from "../../stores/variables";
17
17
  import type { ColorMode } from "../../types";
18
+ import type { ColorScheme } from "../../types/colors";
18
19
  import type {
19
20
  Action,
20
21
  CompleteWorkflowNavigateArgs,
@@ -105,6 +106,12 @@
105
106
  * ```
106
107
  */
107
108
  customVariables?: CustomVariables;
109
+ /**
110
+ * Optional baseline safe-area colour applied when the paywall background
111
+ * can't derive one and `background.safe_area_fallback_color` is unset.
112
+ * Hosts (e.g. workflow runtimes) pass their workflow-level fallback here.
113
+ */
114
+ safeAreaFallbackColor?: ColorScheme | null;
108
115
  }
109
116
 
110
117
  const {
@@ -130,6 +137,7 @@
130
137
  maxContentWidth,
131
138
  initialInputSelections = {},
132
139
  customVariables = {},
140
+ safeAreaFallbackColor,
133
141
  }: Props = $props();
134
142
 
135
143
  const getColorMode = setColorModeContext(() => preferredColorMode);
@@ -140,7 +148,11 @@
140
148
 
141
149
  const instanceId: symbol = Symbol();
142
150
  $effect(() =>
143
- applyDocumentBackground(instanceId, viewportBackdropModel, paywallData),
151
+ applyDocumentBackground(instanceId, viewportBackdropModel, {
152
+ paywallData,
153
+ colorMode: getColorMode(),
154
+ hostFallbackColor: safeAreaFallbackColor,
155
+ }),
144
156
  );
145
157
 
146
158
  const { default_locale, components_config, components_localizations } =
@@ -1,5 +1,6 @@
1
1
  import { type InitialInputSelections } from "../../stores/inputValidation";
2
2
  import type { ColorMode } from "../../types";
3
+ import type { ColorScheme } from "../../types/colors";
3
4
  import type { CompleteWorkflowNavigateArgs } from "../../types/components/button";
4
5
  import type { OnComponentInteraction } from "../../types/paywall-component-interaction";
5
6
  import type { PaywallData } from "../../types/paywall";
@@ -56,6 +57,12 @@ interface Props {
56
57
  * ```
57
58
  */
58
59
  customVariables?: CustomVariables;
60
+ /**
61
+ * Optional baseline safe-area colour applied when the paywall background
62
+ * can't derive one and `background.safe_area_fallback_color` is unset.
63
+ * Hosts (e.g. workflow runtimes) pass their workflow-level fallback here.
64
+ */
65
+ safeAreaFallbackColor?: ColorScheme | null;
59
66
  }
60
67
  declare const Paywall: import("svelte").Component<Props, {}, "">;
61
68
  type Paywall = ReturnType<typeof Paywall>;
@@ -35,6 +35,18 @@
35
35
  };
36
36
  });
37
37
 
38
+ const isVisible = $derived(
39
+ evaluateVisibilityConditions(
40
+ {
41
+ selectedPackageId: $selectedPackageId,
42
+ packageInfo: $packageInfo,
43
+ variables: $variables ?? {},
44
+ },
45
+ props.overrides,
46
+ props.visible,
47
+ ),
48
+ );
49
+
38
50
  const timelineStyles = $derived(
39
51
  css(
40
52
  getTimelineInlineStyles({
@@ -50,18 +62,6 @@
50
62
  }),
51
63
  ),
52
64
  );
53
-
54
- const isVisible = $derived(
55
- evaluateVisibilityConditions(
56
- {
57
- selectedPackageId: $selectedPackageId,
58
- packageInfo: $packageInfo,
59
- variables: $variables ?? {},
60
- },
61
- props.overrides,
62
- props.visible,
63
- ),
64
- );
65
65
  </script>
66
66
 
67
67
  {#if isVisible && items.length}
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import Paywall from "../paywall/Paywall.svelte";
3
3
  import type { CompleteWorkflowNavigateArgs } from "../../types/components/button";
4
+ import type { ColorScheme } from "../../types/colors";
4
5
  import type { InitialInputSelections } from "../../stores/inputValidation";
5
6
  import type { OnComponentInteraction } from "../../types/paywall-component-interaction";
6
7
  import type { WorkflowScreen } from "../../types/workflow";
@@ -32,6 +33,7 @@
32
33
  args: CompleteWorkflowNavigateArgs,
33
34
  ) => void | Promise<void>;
34
35
  walletButtonRender?: WalletButtonRender;
36
+ safeAreaFallbackColor?: ColorScheme | null;
35
37
  }
36
38
  const {
37
39
  paywallComponents,
@@ -49,6 +51,7 @@
49
51
  onInputChanged,
50
52
  onCompleteWorkflowNavigate,
51
53
  walletButtonRender,
54
+ safeAreaFallbackColor,
52
55
  }: Props = $props();
53
56
  </script>
54
57
 
@@ -76,6 +79,7 @@
76
79
  {onPurchaseClicked}
77
80
  {onInputChanged}
78
81
  {walletButtonRender}
82
+ {safeAreaFallbackColor}
79
83
  onError={(error) => {
80
84
  console.error("Paywall error:", error);
81
85
  }}
@@ -1,4 +1,5 @@
1
1
  import type { CompleteWorkflowNavigateArgs } from "../../types/components/button";
2
+ import type { ColorScheme } from "../../types/colors";
2
3
  import type { InitialInputSelections } from "../../stores/inputValidation";
3
4
  import type { OnComponentInteraction } from "../../types/paywall-component-interaction";
4
5
  import type { WorkflowScreen } from "../../types/workflow";
@@ -21,6 +22,7 @@ interface Props {
21
22
  onInputChanged?: (fieldId: string, value: string, actionId?: string) => void;
22
23
  onCompleteWorkflowNavigate?: (args: CompleteWorkflowNavigateArgs) => void | Promise<void>;
23
24
  walletButtonRender?: WalletButtonRender;
25
+ safeAreaFallbackColor?: ColorScheme | null;
24
26
  }
25
27
  declare const Screen: import("svelte").Component<Props, {}, "">;
26
28
  type Screen = ReturnType<typeof Screen>;
@@ -19129,6 +19129,7 @@ export const paywallWithFooter = {
19129
19129
  updated_at: "2024-12-10T19:23:17Z",
19130
19130
  };
19131
19131
  export const paywallWithHeader = {
19132
+ id: "paywall_with_header",
19132
19133
  asset_base_url: "https://assets.pawwalls.com",
19133
19134
  components_config: {
19134
19135
  base: {
@@ -20697,6 +20698,7 @@ export const paywallWithHeader = {
20697
20698
  updated_at: "2026-04-14T09:50:13Z",
20698
20699
  };
20699
20700
  export const paywallWithTransparentHeaderAndTopImage = {
20701
+ id: "paywall_with_transparent_header_and_top_image",
20700
20702
  asset_base_url: "https://assets.pawwalls.com",
20701
20703
  components_config: {
20702
20704
  base: {
@@ -1,4 +1,4 @@
1
- type GradientPoint = {
1
+ export type GradientPoint = {
2
2
  percent: number;
3
3
  color: string;
4
4
  };
@@ -1,4 +1,5 @@
1
1
  import type { BaseComponent } from "../base";
2
+ import type { Overrides } from "../overrides";
2
3
  import type { StackProps } from "./stack";
3
4
  export interface PackageProps extends BaseComponent {
4
5
  type: "package";
@@ -6,4 +7,5 @@ export interface PackageProps extends BaseComponent {
6
7
  is_selected_by_default: boolean;
7
8
  apple_promo_offer_product_code?: string | null;
8
9
  stack: StackProps;
10
+ overrides?: Overrides<PackageProps>;
9
11
  }
@@ -47,7 +47,7 @@ type VariableCondition = {
47
47
  type: typeof RuleKey.VariableCondition;
48
48
  operator: EqualityOperator;
49
49
  variable: string;
50
- value: CustomVariableValue;
50
+ value: CustomVariableValue | string | number | boolean;
51
51
  };
52
52
  type OverrideCondition = IntroOfferCondition | MultipleIntroOffersCondition | SelectedCondition | HoverCondition | FocusCondition | ErrorCondition | PromoOfferCondition | IntroductoryOfferCondition | PromotionalOfferCondition | SelectedPackageCondition | VariableCondition;
53
53
  /**
@@ -1,4 +1,5 @@
1
1
  import type { Background } from "./background";
2
+ import type { ColorScheme } from "./colors";
2
3
  import type { FooterProps } from "./components/footer";
3
4
  import type { HeaderProps } from "./components/header";
4
5
  import type { StackProps } from "./components/stack";
@@ -8,6 +9,7 @@ export interface RootPaywall {
8
9
  stack: StackProps;
9
10
  sticky_footer?: FooterProps | null;
10
11
  header?: HeaderProps | null;
12
+ safe_area_fallback_color?: ColorScheme | null;
11
13
  }
12
14
  export interface ComponentConfig {
13
15
  colors?: Record<string, string>;
package/dist/types.d.ts CHANGED
@@ -120,4 +120,5 @@ export declare enum StackDistribution {
120
120
  end = "flex-end"
121
121
  }
122
122
  export type { WorkflowScreen } from "./types/workflow";
123
+ export type { ColorScheme } from "./types/colors";
123
124
  export type { ComponentInteractionData, ComponentInteractionType, OnComponentInteraction, } from "./types/paywall-component-interaction";
@@ -1,3 +1,9 @@
1
1
  import type { PaywallData } from "../types/paywall";
2
2
  import type { PaywallRootBackgroundModel } from "./background-utils";
3
- export declare function applyDocumentBackground(instanceId: symbol, model: PaywallRootBackgroundModel, paywallData: PaywallData | null | undefined): () => void;
3
+ import type { ColorMode } from "../types";
4
+ import type { ColorScheme } from "../types/colors";
5
+ export declare function applyDocumentBackground(instanceId: symbol, model: PaywallRootBackgroundModel, options: {
6
+ paywallData: PaywallData | null | undefined;
7
+ colorMode: ColorMode;
8
+ hostFallbackColor?: ColorScheme | null;
9
+ }): () => void;
@@ -1,40 +1,14 @@
1
+ import { getBackgroundSafeAreaColors, resolveSafeAreaFallbackCss, SAFE_AREA_FALLBACK_COLOR_CSS_VAR, } from "./safe-area-background";
1
2
  // Last writer wins, but cleanup only fires for the last writer — otherwise an
2
- // unmounting paywall would clear a variable set by another paywall that overlapped
3
- // it during a transition.
3
+ // unmounting paywall would clear a variable set by another paywall that
4
+ // overlapped it during a transition.
4
5
  let lastBgWriter = null;
5
- // iOS promotes sticky header/footer to its own compositor layer, which breaks
6
- // safe-area painting for the opposite strip. A vertical gradient's edge stop
7
- // can stand in as a solid fallback for that strip; any other angle would smear
8
- // a horizontal colour range across the strip that no single colour represents.
9
- function gradientSafeAreaFallbackColour(gradient, hasHeader, hasFooter) {
10
- if (!hasHeader && !hasFooter)
11
- return null;
12
- if (hasHeader && hasFooter)
13
- return null;
14
- const angleMatch = gradient.match(/linear-gradient\((\d+)deg/);
15
- if (!angleMatch)
16
- return null;
17
- if (parseInt(angleMatch[1], 10) % 180 !== 0)
18
- return null;
19
- const stops = gradient.match(/#[0-9a-fA-F]{6,8}/g);
20
- if (!stops || stops.length < 2)
21
- return null;
22
- const candidate = hasHeader ? stops[stops.length - 1] : stops[0];
23
- if (candidate.length === 9 && candidate.slice(-2).toLowerCase() !== "ff") {
24
- return null;
25
- }
26
- return candidate;
27
- }
28
- // Published as a CSS variable so consumer surfaces can opt in to safe-area
29
- // painting (`background: var(--rc-purchases-ui-bg-color, Canvas)` on any
30
- // element that extends into the safe-area canvas — typically html, body, or
31
- // a position:fixed root). Writing to documentElement.backgroundColor directly
32
- // would bleed the paywall colour onto host page chrome that shouldn't take
33
- // it on, so we expose the value rather than apply it.
34
- export function applyDocumentBackground(instanceId, model, paywallData) {
6
+ export function applyDocumentBackground(instanceId, model, options) {
35
7
  if (typeof document === "undefined")
36
8
  return () => { };
9
+ const { paywallData, colorMode, hostFallbackColor } = options;
37
10
  const root = document.documentElement;
11
+ const base = paywallData?.components_config?.base;
38
12
  let value = null;
39
13
  if (model.kind === "style" && model.style.background) {
40
14
  const bg = model.style.background;
@@ -42,18 +16,26 @@ export function applyDocumentBackground(instanceId, model, paywallData) {
42
16
  value = bg;
43
17
  }
44
18
  else {
45
- const base = paywallData?.components_config?.base;
46
- value = gradientSafeAreaFallbackColour(bg, !!base?.header, !!base?.sticky_footer);
19
+ const edges = getBackgroundSafeAreaColors(base?.background, colorMode, { width: window.innerWidth, height: window.innerHeight }, {
20
+ stickyComponents: {
21
+ hasHeader: base?.header != null,
22
+ hasFooter: base?.sticky_footer != null,
23
+ },
24
+ });
25
+ value = edges.top ?? edges.bottom;
47
26
  }
48
27
  }
28
+ if (value === null) {
29
+ value = resolveSafeAreaFallbackCss(base?.safe_area_fallback_color, hostFallbackColor, colorMode);
30
+ }
49
31
  if (value !== null) {
50
32
  lastBgWriter = instanceId;
51
- root.style.setProperty("--rc-purchases-ui-bg-color", value);
33
+ root.style.setProperty(SAFE_AREA_FALLBACK_COLOR_CSS_VAR, value);
52
34
  }
53
35
  return () => {
54
36
  if (lastBgWriter === instanceId) {
55
37
  lastBgWriter = null;
56
- root.style.removeProperty("--rc-purchases-ui-bg-color");
38
+ root.style.removeProperty(SAFE_AREA_FALLBACK_COLOR_CSS_VAR);
57
39
  }
58
40
  };
59
41
  }
@@ -0,0 +1,21 @@
1
+ import type { ColorMode } from "../types";
2
+ import type { Background } from "../types/background";
3
+ import type { ColorScheme } from "../types/colors";
4
+ import type { PaywallData } from "../types/paywall";
5
+ type Viewport = {
6
+ width: number;
7
+ height: number;
8
+ };
9
+ type SafeAreaEdge = "top" | "bottom";
10
+ export declare const SAFE_AREA_FALLBACK_COLOR_CSS_VAR = "--rc-purchases-ui-bg-color";
11
+ export declare function resolveSafeAreaFallbackCss(paywallOverride: ColorScheme | null | undefined, hostFallback: ColorScheme | null | undefined, colorMode: ColorMode): string | null;
12
+ export declare function getSafeAreaFallbackColorHeadStyles(paywallData: PaywallData | null | undefined, hostFallbackColor?: ColorScheme | null): string;
13
+ export declare function getBackgroundSafeAreaColors(background: Background | null | undefined, colorMode: ColorMode, viewport: Viewport, options?: {
14
+ stickyComponents?: {
15
+ hasHeader?: boolean;
16
+ hasFooter?: boolean;
17
+ };
18
+ paywallFallbackColor?: ColorScheme | null;
19
+ hostFallbackColor?: ColorScheme | null;
20
+ }): Record<SafeAreaEdge, string | null>;
21
+ export {};
@@ -0,0 +1,211 @@
1
+ import { mapColorInfo, mapColorMode } from "./base-utils";
2
+ export const SAFE_AREA_FALLBACK_COLOR_CSS_VAR = "--rc-purchases-ui-bg-color";
3
+ // Light-only schemes must not bleed into the dark media query — a dark rule
4
+ // reading from `light` would override the light value.
5
+ function getSafeAreaFallbackColorInfoForMode(paywallOverride, hostFallback, colorMode) {
6
+ const colorAtMode = (scheme) => colorMode === "dark" ? scheme?.dark : scheme?.light;
7
+ return colorAtMode(paywallOverride) ?? colorAtMode(hostFallback) ?? null;
8
+ }
9
+ const HEX_COLOR_REGEX = /^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/;
10
+ function isHexColor(value) {
11
+ return HEX_COLOR_REGEX.test(value);
12
+ }
13
+ function isOpaqueHexColor(value) {
14
+ const match = HEX_COLOR_REGEX.exec(value);
15
+ return match != null && (match[1] == null || match[1].toLowerCase() === "ff");
16
+ }
17
+ function normalizeHexColor(color) {
18
+ return color.length === 7 ? `${color.toLowerCase()}ff` : color.toLowerCase();
19
+ }
20
+ function getCssHexColor(value) {
21
+ return value?.type === "hex" && isHexColor(value.value) ? value.value : null;
22
+ }
23
+ function isAxisAlignedVerticalGradient(degrees) {
24
+ return Math.abs(((degrees % 180) + 180) % 180) < 1e-10;
25
+ }
26
+ // Alias-aware counterpart for the SDK runtime; mapColorInfo resolves aliases
27
+ // that the hex-only path can't.
28
+ export function resolveSafeAreaFallbackCss(paywallOverride, hostFallback, colorMode) {
29
+ const info = getSafeAreaFallbackColorInfoForMode(paywallOverride, hostFallback, colorMode);
30
+ return info ? mapColorInfo(info) : null;
31
+ }
32
+ // SSR has no real viewport (no off-axis/radial sampling). Hex and alias
33
+ // backgrounds short-circuit immediately; axis-aligned vertical gradients go
34
+ // through the sticky-component disambiguation since edges may differ. Alias
35
+ // values are emitted as var() references — valid CSS that resolves once the
36
+ // SDK stylesheet is present. Invalid hex is skipped rather than emitted.
37
+ function getSsrSafeAreaCssForMode(paywallData, hostFallback, colorMode) {
38
+ const base = paywallData?.components_config?.base;
39
+ const background = base?.background;
40
+ if (background?.type === "color") {
41
+ const color = mapColorMode(colorMode, background.value);
42
+ if (color.type === "hex") {
43
+ const hex = getCssHexColor(color);
44
+ if (hex)
45
+ return hex;
46
+ }
47
+ else if (color.type === "alias") {
48
+ return mapColorInfo(color);
49
+ }
50
+ else if (color.type === "linear" &&
51
+ isAxisAlignedVerticalGradient(color.degrees)) {
52
+ const edges = getBackgroundSafeAreaColors(background, colorMode, { width: 1, height: 1 }, {
53
+ stickyComponents: {
54
+ hasHeader: base?.header != null,
55
+ hasFooter: base?.sticky_footer != null,
56
+ },
57
+ });
58
+ const sampled = edges.top ?? edges.bottom;
59
+ if (sampled && isHexColor(sampled))
60
+ return sampled;
61
+ }
62
+ }
63
+ const fallbackInfo = getSafeAreaFallbackColorInfoForMode(base?.safe_area_fallback_color, hostFallback, colorMode);
64
+ if (fallbackInfo == null)
65
+ return null;
66
+ return fallbackInfo.type === "alias"
67
+ ? mapColorInfo(fallbackInfo)
68
+ : getCssHexColor(fallbackInfo);
69
+ }
70
+ export function getSafeAreaFallbackColorHeadStyles(paywallData, hostFallbackColor) {
71
+ return ["light", "dark"]
72
+ .map((mode) => {
73
+ const value = getSsrSafeAreaCssForMode(paywallData, hostFallbackColor, mode);
74
+ return value
75
+ ? `@media (prefers-color-scheme: ${mode}) { html, body { ${SAFE_AREA_FALLBACK_COLOR_CSS_VAR}: ${value}; } }`
76
+ : null;
77
+ })
78
+ .filter(Boolean)
79
+ .join("\n");
80
+ }
81
+ function colorAtPercent(sortedPoints, percent) {
82
+ const firstPoint = sortedPoints[0];
83
+ const lastPoint = sortedPoints.at(-1);
84
+ if (firstPoint == null || lastPoint == null) {
85
+ return null;
86
+ }
87
+ if (percent <= firstPoint.percent) {
88
+ return normalizeHexColor(firstPoint.color);
89
+ }
90
+ if (percent >= lastPoint.percent) {
91
+ return normalizeHexColor(lastPoint.color);
92
+ }
93
+ const nextPointIndex = sortedPoints.findIndex((point) => point.percent >= percent);
94
+ const previousPoint = sortedPoints[nextPointIndex - 1];
95
+ const nextPoint = sortedPoints[nextPointIndex];
96
+ if (previousPoint == null || nextPoint == null) {
97
+ return null;
98
+ }
99
+ if (previousPoint.color.toLowerCase() === nextPoint.color.toLowerCase()) {
100
+ return normalizeHexColor(previousPoint.color);
101
+ }
102
+ if (previousPoint.percent === nextPoint.percent) {
103
+ return normalizeHexColor(nextPoint.color);
104
+ }
105
+ return null;
106
+ }
107
+ function getSortedGradientPoints(points) {
108
+ return points
109
+ .filter((point) => Number.isFinite(point.percent) && isHexColor(point.color))
110
+ .slice()
111
+ .sort((a, b) => a.percent - b.percent);
112
+ }
113
+ function solidColorForRange(sortedPoints, [rangeStart, rangeEnd]) {
114
+ const expectedColor = colorAtPercent(sortedPoints, rangeStart);
115
+ if (expectedColor == null || !isOpaqueHexColor(expectedColor)) {
116
+ return null;
117
+ }
118
+ const breakpoints = [
119
+ rangeStart,
120
+ ...sortedPoints
121
+ .map((point) => point.percent)
122
+ .filter((percent) => percent > rangeStart && percent < rangeEnd),
123
+ rangeEnd,
124
+ ];
125
+ const isSolid = breakpoints.every((breakpoint, index) => {
126
+ if (colorAtPercent(sortedPoints, breakpoint) !== expectedColor) {
127
+ return false;
128
+ }
129
+ const nextBreakpoint = breakpoints[index + 1];
130
+ if (nextBreakpoint == null || nextBreakpoint === breakpoint) {
131
+ return true;
132
+ }
133
+ return (colorAtPercent(sortedPoints, (breakpoint + nextBreakpoint) / 2) ===
134
+ expectedColor);
135
+ });
136
+ return isSolid ? expectedColor : null;
137
+ }
138
+ function snapToZero(value) {
139
+ return Math.abs(value) < 1e-10 ? 0 : value;
140
+ }
141
+ function linearGradientPositionPercent(degrees, x, y, viewport) {
142
+ const radians = (degrees * Math.PI) / 180;
143
+ // Math.sin(Math.PI) is ~1.22e-16, not 0. Snap so a 180° gradient's edge
144
+ // sits exactly on a stop instead of just barely missing it.
145
+ const dx = snapToZero(Math.sin(radians));
146
+ const dy = snapToZero(-Math.cos(radians));
147
+ const gradientLength = Math.abs(dx) * viewport.width + Math.abs(dy) * viewport.height;
148
+ const offset = (x - viewport.width / 2) * dx + (y - viewport.height / 2) * dy;
149
+ return ((offset + gradientLength / 2) / gradientLength) * 100;
150
+ }
151
+ function linearEdgeRangePercent(degrees, edge, viewport) {
152
+ const y = edge === "top" ? 0 : viewport.height;
153
+ const start = linearGradientPositionPercent(degrees, 0, y, viewport);
154
+ const end = linearGradientPositionPercent(degrees, viewport.width, y, viewport);
155
+ return [Math.min(start, end), Math.max(start, end)];
156
+ }
157
+ function radialGradientPositionPercent(x, y, viewport) {
158
+ const radius = Math.hypot(viewport.width / 2, viewport.height / 2);
159
+ const distance = Math.hypot(x - viewport.width / 2, y - viewport.height / 2);
160
+ return (distance / radius) * 100;
161
+ }
162
+ function radialEdgeRangePercent(edge, viewport) {
163
+ const y = edge === "top" ? 0 : viewport.height;
164
+ const center = radialGradientPositionPercent(viewport.width / 2, y, viewport);
165
+ const corner = radialGradientPositionPercent(0, y, viewport);
166
+ return [Math.min(center, corner), Math.max(center, corner)];
167
+ }
168
+ function getSolidEdgeColor(color, edge, viewport, sortedPoints) {
169
+ switch (color.type) {
170
+ case "hex":
171
+ case "alias":
172
+ return mapColorInfo(color);
173
+ case "linear":
174
+ return solidColorForRange(sortedPoints ?? [], linearEdgeRangePercent(color.degrees, edge, viewport));
175
+ case "radial":
176
+ return solidColorForRange(sortedPoints ?? [], radialEdgeRangePercent(edge, viewport));
177
+ }
178
+ }
179
+ // `position: sticky` promotes a header/footer to its own compositor layer
180
+ // regardless of fill, so even transparent sticky elements break the opposite
181
+ // safe-area strip. Pass `stickyComponents` when painting into a single CSS
182
+ // variable (SDK runtime + SSR head) so the covered edge is nulled and the
183
+ // caller can fall through to the override/host chain; editor previews leave
184
+ // it absent.
185
+ export function getBackgroundSafeAreaColors(background, colorMode, viewport, options) {
186
+ let top = null;
187
+ let bottom = null;
188
+ if (background?.type === "color") {
189
+ const color = mapColorMode(colorMode, background.value);
190
+ const sortedPoints = color.type === "linear" || color.type === "radial"
191
+ ? getSortedGradientPoints(color.points)
192
+ : null;
193
+ top = getSolidEdgeColor(color, "top", viewport, sortedPoints);
194
+ bottom = getSolidEdgeColor(color, "bottom", viewport, sortedPoints);
195
+ }
196
+ if (options?.stickyComponents) {
197
+ const hasHeader = options.stickyComponents.hasHeader === true;
198
+ const hasFooter = options.stickyComponents.hasFooter === true;
199
+ // A single CSS variable can only paint one strip; emit only when exactly
200
+ // one edge is exposed (the opposite edge is occluded by a sticky element).
201
+ top = hasFooter && !hasHeader ? top : null;
202
+ bottom = hasHeader && !hasFooter ? bottom : null;
203
+ }
204
+ if (options?.paywallFallbackColor != null ||
205
+ options?.hostFallbackColor != null) {
206
+ const fallback = resolveSafeAreaFallbackCss(options.paywallFallbackColor, options.hostFallbackColor, colorMode);
207
+ top = top ?? fallback;
208
+ bottom = bottom ?? fallback;
209
+ }
210
+ return { top, bottom };
211
+ }
@@ -0,0 +1 @@
1
+ export { getBackgroundSafeAreaColors, getSafeAreaFallbackColorHeadStyles, } from "./safe-area-background";
@@ -0,0 +1,4 @@
1
+ // Public surface for `@revenuecat/purchases-ui-js/safe-area`. The CSS var
2
+ // name and helpers used internally by `applyDocumentBackground` are
3
+ // deliberately not re-exported.
4
+ export { getBackgroundSafeAreaColors, getSafeAreaFallbackColorHeadStyles, } from "./safe-area-background";
@@ -177,8 +177,13 @@ const conditionMatches = (condition, context) => {
177
177
  return !condition.packages.includes(selected);
178
178
  }
179
179
  if (condition.type === "variable_condition") {
180
- const currentValue = context.variables[condition.variable];
181
- const conditionValue = String(condition.value.value);
180
+ const variableName = condition.variable.replace(/^\$?custom\./, "");
181
+ const currentValue = context.variables[variableName];
182
+ // API sends value as a plain scalar; type definition wraps it in CustomVariableValue
183
+ const rawValue = condition.value;
184
+ const conditionValue = String(typeof rawValue === "object" && rawValue !== null && "value" in rawValue
185
+ ? rawValue.value
186
+ : rawValue);
182
187
  if (condition.operator === "=")
183
188
  return currentValue === conditionValue;
184
189
  if (condition.operator === "!=")
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@revenuecat/purchases-ui-js",
3
3
  "description": "Web components for Paywalls. Powered by RevenueCat",
4
4
  "private": false,
5
- "version": "4.2.0",
5
+ "version": "4.4.0",
6
6
  "author": {
7
7
  "name": "RevenueCat, Inc."
8
8
  },
@@ -65,6 +65,10 @@
65
65
  },
66
66
  "./web-components": {
67
67
  "default": "./dist/web-components/index.js"
68
+ },
69
+ "./safe-area": {
70
+ "types": "./dist/utils/safe-area.d.ts",
71
+ "default": "./dist/utils/safe-area.js"
68
72
  }
69
73
  },
70
74
  "engines": {