@revenuecat/purchases-ui-js 4.8.15 → 4.8.16

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.
@@ -1,8 +1,11 @@
1
1
  <script lang="ts">
2
2
  import Stack from "../stack/Stack.svelte";
3
+ import { setStackDimensionContext } from "../stack/stack-dimension-context";
3
4
  import type { FooterProps } from "../../types/components/footer";
4
5
 
5
6
  const { stack }: FooterProps = $props();
7
+ // The wrapper below is a row flex container; publish its axis to the Stack.
8
+ setStackDimensionContext(() => "horizontal");
6
9
  </script>
7
10
 
8
11
  <div>
@@ -1,8 +1,11 @@
1
1
  <script lang="ts">
2
2
  import Stack from "../stack/Stack.svelte";
3
+ import { setStackDimensionContext } from "../stack/stack-dimension-context";
3
4
  import type { HeaderProps } from "../../types/components/header";
4
5
 
5
6
  const { stack }: HeaderProps = $props();
7
+ // The wrapper below is a row flex container; publish its axis to the Stack.
8
+ setStackDimensionContext(() => "horizontal");
6
9
  </script>
7
10
 
8
11
  <div>
@@ -26,6 +26,7 @@
26
26
  import { viewportDecorator } from "../../stories/viewport-decorator";
27
27
  import { BACKGROUND_PAYWALL } from "./fixtures/background-paywall";
28
28
  import { FIXED_HEIGHT_STACK_PAYWALL } from "./fixtures/fixed-height-stack-paywall";
29
+ import { FILL_HEIGHT_WEBVIEW_PAYWALL } from "./fixtures/fill-height-webview-paywall";
29
30
  import { OVERRIDE_PAYWALL } from "./fixtures/override-paywall";
30
31
  import { SHEET_PAYWALL } from "./fixtures/sheet-paywall";
31
32
  import { SHEET_PAYWALL_VIDEO_STACKING } from "./fixtures/sheet-video-stacking-paywall";
@@ -95,6 +96,19 @@
95
96
  }}
96
97
  />
97
98
 
99
+ <!--
100
+ Regression for FUN-2262: fill-height Stack + fill-height WebView must fill
101
+ the space between the fixed header/footer bars. Collapse shows as the bars
102
+ sitting nearly adjacent. Chromatic watches the host layout, not iframe pixels.
103
+ -->
104
+ <Story
105
+ name="Fill-height WebView (regression)"
106
+ decorators={[viewportDecorator(320, 400, 0)]}
107
+ args={{
108
+ paywallData: FILL_HEIGHT_WEBVIEW_PAYWALL,
109
+ }}
110
+ />
111
+
98
112
  <Story
99
113
  name="Override paywall"
100
114
  args={{
@@ -47,6 +47,7 @@
47
47
  import { onMount } from "svelte";
48
48
  import { derived, readable, writable } from "svelte/store";
49
49
  import Stack from "../stack/Stack.svelte";
50
+ import { mapPaywallContentStackStyle } from "./paywall-utils";
50
51
  import Sheet from "./Sheet.svelte";
51
52
  import ViewportBackdrop from "./ViewportBackdrop.svelte";
52
53
  import {
@@ -401,12 +402,11 @@
401
402
  bottom: (stack.padding?.bottom ?? 0) + footerHeight,
402
403
  }
403
404
  : stack.padding}
404
- style={{
405
- height: "auto",
406
- ...(pullContentUnderHeader
407
- ? { "margin-top": `-${headerHeight}px` }
408
- : {}),
409
- }}
405
+ style={mapPaywallContentStackStyle(
406
+ stack.size.height,
407
+ pullContentUnderHeader,
408
+ headerHeight,
409
+ )}
410
410
  />
411
411
  </div>
412
412
  {#if sticky_footer}
@@ -0,0 +1,2 @@
1
+ import type { PaywallData } from "../../../types/paywall";
2
+ export declare const FILL_HEIGHT_WEBVIEW_PAYWALL: PaywallData;
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Regression fixture for FUN-2262: a fill-height Stack wrapping a fill-height
3
+ * WebView must consume the space between fixed header/footer bars.
4
+ *
5
+ * Pair with `viewportDecorator` so the paywall has a definite height. If either
6
+ * fill level collapses, the colored bars sit nearly adjacent in Chromatic.
7
+ *
8
+ * Uses a tiny stable https SVG so Chromatic cares about the host box height,
9
+ * not iframe content pixels.
10
+ */
11
+ const STABLE_SVG_URL = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/2705.svg";
12
+ const ZERO_SPACING = {
13
+ bottom: 0,
14
+ leading: 0,
15
+ top: 0,
16
+ trailing: 0,
17
+ };
18
+ const NO_CORNERS = {
19
+ bottom_leading: 0,
20
+ bottom_trailing: 0,
21
+ top_leading: 0,
22
+ top_trailing: 0,
23
+ };
24
+ const solidBar = (id, height, hex) => ({
25
+ type: "stack",
26
+ id,
27
+ name: id,
28
+ components: [],
29
+ dimension: {
30
+ type: "vertical",
31
+ alignment: "center",
32
+ distribution: "center",
33
+ },
34
+ size: {
35
+ width: { type: "fill" },
36
+ height: { type: "fixed", value: height },
37
+ },
38
+ background: null,
39
+ background_color: { light: { type: "hex", value: hex } },
40
+ badge: null,
41
+ border: null,
42
+ margin: { ...ZERO_SPACING },
43
+ padding: { ...ZERO_SPACING },
44
+ shadow: null,
45
+ shape: { type: "rectangle", corners: { ...NO_CORNERS } },
46
+ spacing: 0,
47
+ });
48
+ const fillWebView = {
49
+ type: "web_view",
50
+ id: "fill_web_view",
51
+ name: "Fill WebView",
52
+ protocol_version: 1,
53
+ url: STABLE_SVG_URL,
54
+ size: {
55
+ width: { type: "fill" },
56
+ height: { type: "fill" },
57
+ },
58
+ };
59
+ const fillHostStack = {
60
+ type: "stack",
61
+ id: "fill_host",
62
+ name: "Fill host",
63
+ components: [fillWebView],
64
+ dimension: {
65
+ type: "vertical",
66
+ alignment: "center",
67
+ distribution: "start",
68
+ },
69
+ size: {
70
+ width: { type: "fill" },
71
+ height: { type: "fill" },
72
+ },
73
+ // Distinct fill so a collapsed host is obvious next to the bars.
74
+ background: null,
75
+ background_color: { light: { type: "hex", value: "#c7d2fe" } },
76
+ badge: null,
77
+ border: null,
78
+ margin: { ...ZERO_SPACING },
79
+ padding: { ...ZERO_SPACING },
80
+ shadow: null,
81
+ shape: { type: "rectangle", corners: { ...NO_CORNERS } },
82
+ spacing: 0,
83
+ };
84
+ export const FILL_HEIGHT_WEBVIEW_PAYWALL = {
85
+ id: "fill_height_webview_paywall",
86
+ default_locale: "en_US",
87
+ components_localizations: { en_US: {} },
88
+ components_config: {
89
+ base: {
90
+ background: {
91
+ type: "color",
92
+ value: { light: { type: "hex", value: "#ffffff" } },
93
+ },
94
+ stack: {
95
+ type: "stack",
96
+ id: "root",
97
+ name: "Root",
98
+ components: [
99
+ solidBar("header_bar", 48, "#ff5f5f"),
100
+ fillHostStack,
101
+ solidBar("footer_bar", 48, "#11d483"),
102
+ ],
103
+ dimension: {
104
+ type: "vertical",
105
+ alignment: "center",
106
+ distribution: "start",
107
+ },
108
+ size: {
109
+ width: { type: "fill" },
110
+ height: { type: "fill" },
111
+ },
112
+ background: null,
113
+ background_color: null,
114
+ badge: null,
115
+ border: null,
116
+ margin: { ...ZERO_SPACING },
117
+ padding: { ...ZERO_SPACING },
118
+ shadow: null,
119
+ shape: { type: "rectangle", corners: { ...NO_CORNERS } },
120
+ spacing: 0,
121
+ },
122
+ sticky_footer: null,
123
+ },
124
+ },
125
+ };
@@ -0,0 +1,3 @@
1
+ import type { Size } from "../../types";
2
+ import type { CSS } from "../../utils/base-utils";
3
+ export declare function mapPaywallContentStackStyle(height: Size, pullContentUnderHeader: boolean, headerHeight: number): CSS;
@@ -0,0 +1,8 @@
1
+ export function mapPaywallContentStackStyle(height, pullContentUnderHeader, headerHeight) {
2
+ return {
3
+ // Nested fill children need a definite root height; other roots keep
4
+ // the existing content-sized scrolling behavior.
5
+ ...(height.type === "fill" ? {} : { height: "auto" }),
6
+ ...(pullContentUnderHeader ? { "margin-top": `-${headerHeight}px` } : {}),
7
+ };
8
+ }
@@ -27,10 +27,12 @@
27
27
  import {
28
28
  mapBadge,
29
29
  mapDimension,
30
+ mapFillHeight,
30
31
  mapFixedHeightBounds,
31
32
  mapFixedWidthBounds,
32
33
  mapLayerAlignment,
33
34
  } from "./stack-utils";
35
+ import { setStackDimensionContext } from "./stack-dimension-context";
34
36
 
35
37
  interface MiscProps {
36
38
  onclick?: () => void;
@@ -67,6 +69,9 @@
67
69
  ...resolveOverrideProperties(buildConditionContext(), props.overrides),
68
70
  };
69
71
  });
72
+ const getParentStackDimension = setStackDimensionContext(
73
+ () => dimension.type,
74
+ );
70
75
 
71
76
  const getColorMode = getColorModeContext();
72
77
  const colorMode = $derived(getColorMode());
@@ -80,12 +85,18 @@
80
85
  });
81
86
  const borderRadiusCss = $derived(mapBorderRadius(shape));
82
87
 
88
+ const heightFill = $derived(size.height.type === "fill");
89
+
83
90
  const stackStyle = $derived(
84
91
  css({
85
92
  display: "flex",
86
93
  position: "relative",
87
94
  width: mapSize(size.width),
88
- height: mapSize(size.height),
95
+ // Fill height: definite height + growth in columns; stretch in rows
96
+ // (see mapFillHeight). Lets nested fill children (e.g. web_view) resolve.
97
+ ...(heightFill
98
+ ? mapFillHeight(getParentStackDimension?.())
99
+ : { height: mapSize(size.height) }),
89
100
  ...mapFixedWidthBounds(size.width),
90
101
  ...mapFixedHeightBounds(size.height),
91
102
  ...mapDimension(dimension),
@@ -0,0 +1,12 @@
1
+ import type { Dimension } from "../../types/alignment";
2
+ export type StackDimension = Dimension["type"];
3
+ type GetStackDimension = () => StackDimension | undefined;
4
+ /**
5
+ * Publishes the nearest flex container's axis and returns the parent-axis getter.
6
+ *
7
+ * Reading before writing is required because Svelte seeds a component's context
8
+ * map from its parent. Flex wrappers around Stack must publish their own axis.
9
+ */
10
+ export declare function setStackDimensionContext(context: GetStackDimension): GetStackDimension | undefined;
11
+ export declare function getStackDimensionContext(): GetStackDimension | undefined;
12
+ export {};
@@ -0,0 +1,16 @@
1
+ import { getContext, setContext } from "svelte";
2
+ const key = Symbol("stack-dimension");
3
+ /**
4
+ * Publishes the nearest flex container's axis and returns the parent-axis getter.
5
+ *
6
+ * Reading before writing is required because Svelte seeds a component's context
7
+ * map from its parent. Flex wrappers around Stack must publish their own axis.
8
+ */
9
+ export function setStackDimensionContext(context) {
10
+ const parentContext = getStackDimensionContext();
11
+ setContext(key, context);
12
+ return parentContext;
13
+ }
14
+ export function getStackDimensionContext() {
15
+ return getContext(key);
16
+ }
@@ -4,6 +4,7 @@ import type { Badge } from "../../types/components/stack";
4
4
  import { type CSS } from "../../utils/base-utils";
5
5
  export declare function mapFixedWidthBounds(width: Size): CSS;
6
6
  export declare function mapFixedHeightBounds(height: Size): CSS;
7
+ export declare function mapFillHeight(parentDimension?: Dimension["type"]): CSS;
7
8
  export declare function mapDimension(dimension: Dimension): Record<string, string>;
8
9
  type JustifyAlign = {
9
10
  justifyContent: string;
@@ -28,6 +28,28 @@ export function mapFixedHeightBounds(height) {
28
28
  const value = px(height.value);
29
29
  return { "min-height": value, "max-height": value };
30
30
  }
31
+ export function mapFillHeight(parentDimension) {
32
+ // In a row, fill-height is a cross-axis problem. flex-grow would steal width;
33
+ // height:100% often fails under align-items:flex-start (e.g. alignment:"top").
34
+ // align-self:stretch fills the line so nested center/space_around can work.
35
+ if (parentDimension === "horizontal") {
36
+ return {
37
+ "align-self": "stretch",
38
+ "min-height": 0,
39
+ };
40
+ }
41
+ return {
42
+ height: "100%",
43
+ "min-height": 0,
44
+ ...(parentDimension === "vertical"
45
+ ? {
46
+ "flex-grow": 1,
47
+ "flex-shrink": 1,
48
+ "flex-basis": "0%",
49
+ }
50
+ : {}),
51
+ };
52
+ }
31
53
  export function mapDimension(dimension) {
32
54
  if (dimension.type === "zlayer") {
33
55
  return {};
@@ -2,6 +2,8 @@
2
2
  import { onMount } from "svelte";
3
3
  import type { WebViewProps } from "../../types/components/web-view";
4
4
  import { css, mapSize, px } from "../../utils/base-utils";
5
+ import { getStackDimensionContext } from "../stack/stack-dimension-context";
6
+ import { mapFillHeight } from "../stack/stack-utils";
5
7
  import { isSameOriginAsHost, sanitizeWebViewUrl } from "./web-view-utils";
6
8
  import {
7
9
  FIT_MESSAGE,
@@ -24,6 +26,7 @@
24
26
  typeof value === "number" && Number.isFinite(value) && value > 0;
25
27
 
26
28
  const props: WebViewProps = $props();
29
+ const getParentStackDimension = getStackDimensionContext();
27
30
 
28
31
  // Gate the origin guard on a post-mount flag (not `typeof window`) so SSR and the
29
32
  // first client render match; otherwise a cross-origin URL mismatches on hydration.
@@ -57,6 +60,9 @@
57
60
  const { width, height } = props.size;
58
61
  const widthFit = width.type === "fit";
59
62
  const heightFit = height.type === "fit";
63
+ const heightFill = height.type === "fill";
64
+ const growsInVerticalParent =
65
+ heightFill && getParentStackDimension?.() === "vertical";
60
66
  // Pin a `fit` axis with an explicit size. `flex-shrink: 0` keeps flex parents
61
67
  // from collapsing the box; avoid mirroring the size in `min-*` or it beats `max-*`.
62
68
  // Prefer author-declared `default` when present; else the hard 300px fallback.
@@ -68,12 +74,23 @@
68
74
  : null;
69
75
  return css({
70
76
  width: fitWidthPx != null ? px(fitWidthPx) : mapSize(width),
71
- height: fitHeightPx != null ? px(fitHeightPx) : mapSize(height),
77
+ // Fill height via mapFillHeight: grow in columns, stretch in rows.
78
+ // Growing on a row main axis would change this box's width.
79
+ ...(heightFill
80
+ ? mapFillHeight(getParentStackDimension?.())
81
+ : {
82
+ height: fitHeightPx != null ? px(fitHeightPx) : mapSize(height),
83
+ }),
72
84
  // Cap a fit axis to the parent. Do not mirror the reported size in `min-*`:
73
85
  // that defeats `max-*` and clips content when the report exceeds the viewport.
74
86
  ...(fitWidthPx != null ? { "max-width": "100%" } : {}),
75
87
  ...(fitHeightPx != null ? { "max-height": "100%" } : {}),
76
- "flex-shrink": width.type === "fixed" || widthFit || heightFit ? 0 : 1,
88
+ ...(growsInVerticalParent
89
+ ? {}
90
+ : {
91
+ "flex-shrink":
92
+ width.type === "fixed" || widthFit || heightFit ? 0 : 1,
93
+ }),
77
94
  "line-height": 0,
78
95
  // No transition: the iframe is height:100% of this box, so animating the
79
96
  // resize would flash the iframe's own scrollbar mid-change.
@@ -2,11 +2,20 @@
2
2
  import { setPackageInfoContext } from "../../stores/packageInfo";
3
3
  import { setPaywallContext } from "../../stores/paywall";
4
4
  import { setVariablesContext } from "../../stores/variables";
5
+ import {
6
+ setStackDimensionContext,
7
+ type StackDimension,
8
+ } from "../stack/stack-dimension-context";
5
9
  import type { WebViewProps } from "../../types/components/web-view";
6
10
  import { readable, writable } from "svelte/store";
7
11
  import WebView from "./WebView.svelte";
8
12
 
9
- const props: WebViewProps = $props();
13
+ const {
14
+ parentStackDimension,
15
+ ...props
16
+ }: WebViewProps & { parentStackDimension?: StackDimension } = $props();
17
+
18
+ setStackDimensionContext(() => parentStackDimension);
10
19
 
11
20
  setPaywallContext({
12
21
  defaultPackageId: undefined,
@@ -1,4 +1,8 @@
1
+ import { type StackDimension } from "../stack/stack-dimension-context";
1
2
  import type { WebViewProps } from "../../types/components/web-view";
2
- declare const WebViewTestWrapper: import("svelte").Component<WebViewProps, {}, "">;
3
+ type $$ComponentProps = WebViewProps & {
4
+ parentStackDimension?: StackDimension;
5
+ };
6
+ declare const WebViewTestWrapper: import("svelte").Component<$$ComponentProps, {}, "">;
3
7
  type WebViewTestWrapper = ReturnType<typeof WebViewTestWrapper>;
4
8
  export default WebViewTestWrapper;
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.8.15",
5
+ "version": "4.8.16",
6
6
  "author": {
7
7
  "name": "RevenueCat, Inc."
8
8
  },