@revenuecat/purchases-ui-js 3.12.1 → 4.0.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.
@@ -0,0 +1,263 @@
1
+ <script module lang="ts">
2
+ import { defineMeta } from "@storybook/addon-svelte-csf";
3
+ import type { ComponentProps } from "svelte";
4
+ import Main from "./Main.svelte";
5
+ import Screen from "../../workflows/Screen.svelte";
6
+ import type { Background } from "../../../types/background";
7
+ import type { Component } from "../../../types/component";
8
+ import type { HeaderProps } from "../../../types/components/header";
9
+ import type { FooterProps } from "../../../types/components/footer";
10
+ import type { TextNodeProps } from "../../../types/components/text";
11
+ import type { WorkflowScreen } from "../../../types/workflow";
12
+ import { uiConfigData } from "../../../stories/fixtures";
13
+
14
+ function textComponent(id: string, textLid: string): TextNodeProps {
15
+ return {
16
+ type: "text",
17
+ id,
18
+ name: "",
19
+ text_lid: textLid,
20
+ font_name: null,
21
+ font_size: "body_m",
22
+ font_weight: "bold",
23
+ horizontal_alignment: "center",
24
+ color: { light: { type: "hex", value: "#ffffffff" } },
25
+ background_color: null,
26
+ padding: { top: 0, bottom: 0, leading: 0, trailing: 0 },
27
+ margin: { top: 0, bottom: 0, leading: 0, trailing: 0 },
28
+ size: { width: { type: "fit" }, height: { type: "fit" } },
29
+ };
30
+ }
31
+
32
+ function visibleStack(components: Component[] = []) {
33
+ return {
34
+ type: "stack" as const,
35
+ id: "s",
36
+ name: "",
37
+ components,
38
+ size: {
39
+ width: { type: "fill" as const },
40
+ height: { type: "fit" as const },
41
+ },
42
+ dimension: {
43
+ type: "vertical" as const,
44
+ alignment: "center" as const,
45
+ distribution: "center" as const,
46
+ },
47
+ spacing: 0,
48
+ margin: { top: 0, bottom: 0, leading: 0, trailing: 0 },
49
+ padding: { top: 16, bottom: 16, leading: 0, trailing: 0 },
50
+ background_color: { light: { type: "hex" as const, value: "#00000033" } },
51
+ background: null,
52
+ border: null,
53
+ shape: null,
54
+ shadow: null,
55
+ badge: null,
56
+ };
57
+ }
58
+
59
+ const minimalHeader: HeaderProps = {
60
+ id: "header",
61
+ name: "Header",
62
+ type: "header",
63
+ stack: visibleStack([textComponent("header-label", "header_label")]),
64
+ };
65
+
66
+ const minimalFooter: FooterProps = {
67
+ id: "footer",
68
+ name: "Footer",
69
+ type: "footer",
70
+ stack: visibleStack([textComponent("footer-label", "footer_label")]),
71
+ };
72
+
73
+ function shellData(
74
+ background: Background,
75
+ hasHeader: boolean,
76
+ hasFooter: boolean,
77
+ ): WorkflowScreen {
78
+ return {
79
+ id: "main-story",
80
+ default_locale: "en_US",
81
+ components_localizations: {
82
+ en_US: { header_label: "Header", footer_label: "Footer" },
83
+ },
84
+ asset_base_url: "https://assets.pawwalls.com",
85
+ config: {},
86
+ localized_strings: {},
87
+ localized_strings_by_tier: {},
88
+ name: "Main story",
89
+ offering_id: null,
90
+ revision: 1,
91
+ template_name: "stack",
92
+ components_config: {
93
+ base: {
94
+ background,
95
+ header: hasHeader ? minimalHeader : null,
96
+ sticky_footer: hasFooter ? minimalFooter : null,
97
+ stack: {
98
+ type: "stack" as const,
99
+ id: "content",
100
+ name: "",
101
+ components: [],
102
+ size: {
103
+ width: { type: "fill" as const },
104
+ height: { type: "fill" as const },
105
+ },
106
+ dimension: {
107
+ type: "vertical" as const,
108
+ alignment: "center" as const,
109
+ distribution: "start" as const,
110
+ },
111
+ spacing: 0,
112
+ margin: { top: 0, bottom: 0, leading: 0, trailing: 0 },
113
+ padding: { top: 0, bottom: 0, leading: 0, trailing: 0 },
114
+ background_color: null,
115
+ background: null,
116
+ border: null,
117
+ shape: null,
118
+ shadow: null,
119
+ badge: null,
120
+ },
121
+ },
122
+ },
123
+ };
124
+ }
125
+
126
+ const solidColour: Background = {
127
+ type: "color",
128
+ value: { light: { type: "hex", value: "#6B4FBBFF" } },
129
+ };
130
+
131
+ const gradient: Background = {
132
+ type: "color",
133
+ value: {
134
+ light: {
135
+ type: "linear",
136
+ degrees: 180,
137
+ points: [
138
+ { percent: 0, color: "#3B82F6FF" },
139
+ { percent: 100, color: "#9333EAFF" },
140
+ ],
141
+ },
142
+ },
143
+ };
144
+
145
+ const image: Background = {
146
+ type: "image",
147
+ fit_mode: "fill",
148
+ color_overlay: null,
149
+ value: {
150
+ light: {
151
+ width: 1170,
152
+ height: 2532,
153
+ original: "https://placehold.co/1170x2532",
154
+ heic: "https://placehold.co/1170x2532",
155
+ heic_low_res: "https://placehold.co/1170x2532",
156
+ webp: "https://placehold.co/1170x2532",
157
+ webp_low_res: "https://placehold.co/1170x2532",
158
+ },
159
+ },
160
+ };
161
+
162
+ const { Story } = defineMeta({
163
+ title: "Components/Layout/Main",
164
+ component: Main,
165
+ render: template,
166
+ args: { paywallData: shellData(solidColour, false, false) },
167
+ });
168
+ </script>
169
+
170
+ {#snippet template({
171
+ paywallData,
172
+ preferredColorMode,
173
+ }: ComponentProps<typeof Main>)}
174
+ <div class="viewport-frame">
175
+ <Main {paywallData} {preferredColorMode}>
176
+ <div class="content-wrapper">
177
+ <Screen paywallComponents={paywallData} uiConfig={uiConfigData} />
178
+ </div>
179
+ </Main>
180
+ </div>
181
+ {/snippet}
182
+
183
+ <Story
184
+ name="Solid colour"
185
+ args={{ paywallData: shellData(solidColour, false, false) }}
186
+ />
187
+ <Story
188
+ name="Solid colour — header"
189
+ args={{ paywallData: shellData(solidColour, true, false) }}
190
+ />
191
+ <Story
192
+ name="Solid colour — footer"
193
+ args={{ paywallData: shellData(solidColour, false, true) }}
194
+ />
195
+ <Story
196
+ name="Solid colour — header and footer"
197
+ args={{ paywallData: shellData(solidColour, true, true) }}
198
+ />
199
+
200
+ <Story
201
+ name="Gradient"
202
+ args={{ paywallData: shellData(gradient, false, false) }}
203
+ />
204
+ <Story
205
+ name="Gradient — header"
206
+ args={{ paywallData: shellData(gradient, true, false) }}
207
+ />
208
+ <Story
209
+ name="Gradient — footer"
210
+ args={{ paywallData: shellData(gradient, false, true) }}
211
+ />
212
+ <Story
213
+ name="Gradient — header and footer"
214
+ args={{ paywallData: shellData(gradient, true, true) }}
215
+ />
216
+
217
+ <Story name="Image" args={{ paywallData: shellData(image, false, false) }} />
218
+ <Story
219
+ name="Image — header"
220
+ args={{ paywallData: shellData(image, true, false) }}
221
+ />
222
+ <Story
223
+ name="Image — footer"
224
+ args={{ paywallData: shellData(image, false, true) }}
225
+ />
226
+ <Story
227
+ name="Image — header and footer"
228
+ args={{ paywallData: shellData(image, true, true) }}
229
+ />
230
+
231
+ <style>
232
+ /* A fixed-position frame that emulates rc-workflows' <body> (see app.css).
233
+ Using position:fixed escapes Storybook's DOM chain (.sb-show-main, #storybook-root, PaywallWrapper) */
234
+ .viewport-frame {
235
+ position: fixed;
236
+ inset: 0;
237
+ display: flex;
238
+ flex-direction: column;
239
+ padding: env(safe-area-inset-top) env(safe-area-inset-right)
240
+ env(safe-area-inset-bottom) env(safe-area-inset-left);
241
+ background-color: var(--rc-purchases-ui-bg-color, Canvas);
242
+ color-scheme: light dark;
243
+ overflow: hidden;
244
+ }
245
+
246
+ .content-wrapper {
247
+ width: 100%;
248
+ display: flex;
249
+ flex: 1 1 auto;
250
+ flex-direction: column;
251
+ min-height: 0;
252
+ overflow: hidden;
253
+ }
254
+
255
+ /* Screen's containerId is "screen-container" by default */
256
+ :global(#screen-container) {
257
+ display: flex;
258
+ flex-direction: column;
259
+ flex: 1 1 auto;
260
+ min-height: 0;
261
+ overflow: hidden;
262
+ }
263
+ </style>
@@ -0,0 +1,19 @@
1
+ import Main from "./Main.svelte";
2
+ interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
3
+ new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
4
+ $$bindings?: Bindings;
5
+ } & Exports;
6
+ (internal: unknown, props: {
7
+ $$events?: Events;
8
+ $$slots?: Slots;
9
+ }): Exports & {
10
+ $set?: any;
11
+ $on?: any;
12
+ };
13
+ z_$$bindings?: Bindings;
14
+ }
15
+ declare const Main: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
16
+ [evt: string]: CustomEvent<any>;
17
+ }, {}, {}, string>;
18
+ type Main = InstanceType<typeof Main>;
19
+ export default Main;
@@ -0,0 +1,117 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { ColorMode } from "../../../types";
4
+ import type { PaywallData } from "../../../types/paywall";
5
+ import { paywallRootBackgroundModel } from "../../../utils/background-utils";
6
+ import { MediaQuery } from "svelte/reactivity";
7
+ import ViewportBackdrop from "./ViewportBackdrop.svelte";
8
+
9
+ // Tracks the last Main instance that wrote --rc-purchases-ui-bg-color.
10
+ // Cleanup only removes the variable if this instance is still the last writer,
11
+ // so an unmounting shell won't clear a value set by a shell that overlapped it
12
+ // during a page transition.
13
+ let lastBgWriter: symbol | null = null;
14
+
15
+ interface Props {
16
+ paywallData: PaywallData | null | undefined;
17
+ preferredColorMode?: ColorMode;
18
+ children: Snippet;
19
+ }
20
+
21
+ const { paywallData, preferredColorMode, children }: Props = $props();
22
+
23
+ const instanceId: symbol = Symbol();
24
+
25
+ const prefersDark = new MediaQuery("prefers-color-scheme: dark", false);
26
+ const colorMode: ColorMode = $derived(
27
+ preferredColorMode ?? (prefersDark.current ? "dark" : "light"),
28
+ );
29
+
30
+ const viewportBackdropModel = $derived(
31
+ paywallRootBackgroundModel(paywallData, colorMode),
32
+ );
33
+
34
+ // When a sticky header or footer is present, iOS promotes it to its own compositor
35
+ // layer which breaks the fixed backdrop's safe-area painting for the opposite strip.
36
+ // Returns the edge stop colour to paint on body to patch the uncovered strip, or null.
37
+ // Only works for exactly vertical gradients (0°/180°) — any other angle produces a
38
+ // horizontal colour range across the strip that no single solid colour can represent.
39
+ function gradientSafeAreaFallbackColour(
40
+ gradient: string,
41
+ hasHeader: boolean,
42
+ hasFooter: boolean,
43
+ ): string | null {
44
+ if (!hasHeader && !hasFooter) return null;
45
+ if (hasHeader && hasFooter) return null;
46
+ const angleMatch = gradient.match(/linear-gradient\((\d+)deg/);
47
+ if (!angleMatch) return null;
48
+ if (parseInt(angleMatch[1], 10) % 180 !== 0) return null;
49
+ const stops = gradient.match(/#[0-9a-fA-F]{6,8}/g);
50
+ if (!stops || stops.length < 2) return null;
51
+ const candidate = hasHeader ? stops[stops.length - 1] : stops[0];
52
+ if (candidate.length === 9 && candidate.slice(-2).toLowerCase() !== "ff") {
53
+ return null;
54
+ }
55
+ return candidate;
56
+ }
57
+
58
+ // Solid colours use --rc-purchases-ui-bg-color on body — iOS reliably propagates
59
+ // background-color to the viewport canvas (safe-area strips). Gradients and images
60
+ // are handled by ViewportBackdrop (position:fixed) instead.
61
+ $effect(() => {
62
+ if (typeof document === "undefined") return;
63
+ const model = viewportBackdropModel;
64
+ const root = document.documentElement;
65
+
66
+ let value: string | null = null;
67
+ if (model.kind === "style" && model.style.background) {
68
+ const bg = model.style.background;
69
+ if (!bg.includes("gradient")) {
70
+ value = bg;
71
+ } else {
72
+ const base = paywallData?.components_config?.base;
73
+ value = gradientSafeAreaFallbackColour(
74
+ bg,
75
+ !!base?.header,
76
+ !!base?.sticky_footer,
77
+ );
78
+ }
79
+ }
80
+
81
+ if (value !== null) {
82
+ lastBgWriter = instanceId;
83
+ root.style.setProperty("--rc-purchases-ui-bg-color", value);
84
+ }
85
+
86
+ return () => {
87
+ if (lastBgWriter === instanceId) {
88
+ lastBgWriter = null;
89
+ root.style.removeProperty("--rc-purchases-ui-bg-color");
90
+ }
91
+ };
92
+ });
93
+ </script>
94
+
95
+ <div class="main">
96
+ <ViewportBackdrop model={viewportBackdropModel} />
97
+ <main class="main-content">
98
+ {@render children()}
99
+ </main>
100
+ </div>
101
+
102
+ <style>
103
+ .main {
104
+ flex: 1 1 auto;
105
+ display: flex;
106
+ flex-direction: column;
107
+ min-height: 0;
108
+ }
109
+
110
+ .main-content {
111
+ position: relative;
112
+ z-index: 1;
113
+ display: flex;
114
+ flex: 1 1 auto;
115
+ min-height: 0;
116
+ }
117
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { ColorMode } from "../../../types";
3
+ import type { PaywallData } from "../../../types/paywall";
4
+ interface Props {
5
+ paywallData: PaywallData | null | undefined;
6
+ preferredColorMode?: ColorMode;
7
+ children: Snippet;
8
+ }
9
+ declare const Main: import("svelte").Component<Props, {}, "">;
10
+ type Main = ReturnType<typeof Main>;
11
+ export default Main;
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ import type { PaywallRootBackgroundModel } from "../../../utils/background-utils";
3
+
4
+ // This component exists because iOS WebKit does not reliably extend body's
5
+ // background-image (gradients, bitmaps) into safe-area strips — only background-color
6
+ // propagates reliably via the viewport canvas. A dedicated position:fixed element that
7
+ // explicitly fills the full viewport (including safe areas) is the consistent paint
8
+ // surface for gradients and images on iOS Safari.
9
+ //
10
+ // Solid colours are NOT handled here — they use background-color on body instead,
11
+ // which avoids compositor surface churn on navigation.
12
+ const { model }: { model: PaywallRootBackgroundModel } = $props();
13
+
14
+ const backdropStyle = $derived.by((): string => {
15
+ if (
16
+ model.kind === "style" &&
17
+ model.style.background?.includes("gradient")
18
+ ) {
19
+ return Object.entries(model.style)
20
+ .map(([k, v]) => `${k}:${v}`)
21
+ .join(";");
22
+ }
23
+ if (model.kind === "image") {
24
+ return [
25
+ `background-image:url("${model.src}")`,
26
+ `background-size:${model.fit}`,
27
+ `background-position:${model.position}`,
28
+ `background-repeat:no-repeat`,
29
+ ].join(";");
30
+ }
31
+ return "";
32
+ });
33
+
34
+ const overlayStyle = $derived(
35
+ model.kind === "image" && model.overlay && model.overlay !== "none"
36
+ ? `background:${model.overlay}`
37
+ : null,
38
+ );
39
+
40
+ const shouldRender = $derived(backdropStyle !== "");
41
+ </script>
42
+
43
+ {#if shouldRender}
44
+ <div class="viewport-backdrop" style={backdropStyle}>
45
+ {#if overlayStyle}
46
+ <div class="viewport-backdrop-overlay" style={overlayStyle}></div>
47
+ {/if}
48
+ </div>
49
+ {/if}
50
+
51
+ <style>
52
+ .viewport-backdrop {
53
+ position: fixed;
54
+ inset: 0;
55
+ z-index: 0;
56
+ pointer-events: none;
57
+ overflow: hidden;
58
+ }
59
+
60
+ .viewport-backdrop-overlay {
61
+ position: absolute;
62
+ inset: 0;
63
+ pointer-events: none;
64
+ }
65
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { PaywallRootBackgroundModel } from "../../../utils/background-utils";
2
+ type $$ComponentProps = {
3
+ model: PaywallRootBackgroundModel;
4
+ };
5
+ declare const ViewportBackdrop: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type ViewportBackdrop = ReturnType<typeof ViewportBackdrop>;
7
+ export default ViewportBackdrop;
@@ -1,7 +1,9 @@
1
1
  <script module lang="ts">
2
2
  import { defineMeta } from "@storybook/addon-svelte-csf";
3
+ import type { ComponentProps } from "svelte";
3
4
 
4
5
  import Paywall from "./Paywall.svelte";
6
+ import Main from "../layout/Main/Main.svelte";
5
7
  import {
6
8
  alignmentPaywallData,
7
9
  calmPaywallData,
@@ -31,12 +33,16 @@
31
33
  import { CUSTOM_VARIABLES_PAYWALL } from "./fixtures/custom-variables-paywall";
32
34
  import { COUNTDOWN_PAYWALL } from "../countdown/fixtures/countdown-paywall";
33
35
  import { mockDateDecorator } from "storybook-mock-date-decorator";
34
- import { IndividualPackageVariables } from "./fixtures/express-purchase-button-paywall";
36
+ import {
37
+ IndividualPackageVariables,
38
+ DUELINGUE_PAYWALL,
39
+ } from "./fixtures/express-purchase-button-paywall";
35
40
  import { CustomVariableValue } from "../../types/variables";
36
41
 
37
42
  const { Story } = defineMeta({
38
43
  title: "Example/Paywall",
39
44
  component: Paywall,
45
+ render: template,
40
46
  args: {
41
47
  onPurchaseClicked: (selectedPackageId: string, actionId: string) =>
42
48
  alert(
@@ -55,9 +61,13 @@
55
61
  });
56
62
  </script>
57
63
 
58
- <script>
59
- import { DUELINGUE_PAYWALL } from "./fixtures/express-purchase-button-paywall";
60
- </script>
64
+ {#snippet template(props: ComponentProps<typeof Paywall>)}
65
+ <div class="paywall-story-frame">
66
+ <Main paywallData={props.paywallData}>
67
+ <Paywall {...props} />
68
+ </Main>
69
+ </div>
70
+ {/snippet}
61
71
 
62
72
  <Story
63
73
  name="Stack paywall"
@@ -491,3 +501,19 @@
491
501
  },
492
502
  }}
493
503
  />
504
+
505
+ <style>
506
+ .paywall-story-frame {
507
+ flex: 1 1 auto;
508
+ display: flex;
509
+ flex-direction: column;
510
+ background-color: var(--rc-purchases-ui-bg-color, Canvas);
511
+ color-scheme: light dark;
512
+ }
513
+
514
+ /* Allow chromatic to capture the full content snapshot */
515
+ .paywall-story-frame :global(.paywall-content-scroll) {
516
+ flex: 1 1 auto;
517
+ overflow-y: visible;
518
+ }
519
+ </style>
@@ -32,8 +32,6 @@
32
32
  type PackageInfo,
33
33
  type VariableDictionary,
34
34
  } from "../../types/variables";
35
- import { mapBackground } from "../../utils/background-utils";
36
- import { css } from "../../utils/base-utils";
37
35
  import { STICKY_OVERLAY_Z_INDEX } from "../../utils/constants";
38
36
  import { registerFonts } from "../../utils/font-utils";
39
37
  import { findSelectedPackageId } from "../../utils/style-utils";
@@ -130,8 +128,7 @@
130
128
  customVariables = {},
131
129
  }: Props = $props();
132
130
 
133
- const getColorMode = setColorModeContext(() => preferredColorMode);
134
- const colorMode = $derived(getColorMode());
131
+ setColorModeContext(() => preferredColorMode);
135
132
 
136
133
  const { default_locale, components_config, components_localizations } =
137
134
  paywallData;
@@ -285,8 +282,6 @@
285
282
 
286
283
  setPackageInfoContext(packageInfo);
287
284
 
288
- const style = $derived(css(mapBackground(colorMode, null, base.background)));
289
-
290
285
  onMount(() => {
291
286
  registerFonts(uiConfig);
292
287
  });
@@ -305,7 +300,7 @@
305
300
  </script>
306
301
 
307
302
  <svelte:boundary onerror={onError}>
308
- <div class={paywallClass} {style}>
303
+ <div class={paywallClass}>
309
304
  {#if header}
310
305
  <div
311
306
  class="header-wrapper"
@@ -321,12 +316,20 @@
321
316
  maxContentWidth
322
317
  ? `max-width: ${maxContentWidth}; margin-inline: auto;`
323
318
  : "",
324
- pullContentUnderHeader ? `margin-top: -${headerHeight}px;` : "",
325
319
  ]
326
320
  .filter(Boolean)
327
321
  .join(" ")}
328
322
  >
329
- <Stack {...stack} class="paywall-content-scroll" />
323
+ <Stack
324
+ {...stack}
325
+ class="paywall-content-scroll"
326
+ style={{
327
+ height: "auto",
328
+ ...(pullContentUnderHeader
329
+ ? { "margin-top": `-${headerHeight}px` }
330
+ : {}),
331
+ }}
332
+ />
330
333
  {#if sticky_footer}
331
334
  <Footer {...sticky_footer} />
332
335
  {/if}
@@ -342,27 +345,16 @@
342
345
  .paywall {
343
346
  position: relative;
344
347
  display: flex;
348
+ flex: 1 1 auto;
345
349
  flex-direction: column;
346
350
  align-items: stretch;
347
- height: 100%;
351
+ min-height: 0;
348
352
 
349
353
  transition-property: filter, transform;
350
354
  transition-duration: 0.1s;
351
355
  transition-timing-function: ease-in-out;
352
356
  transform-origin: center;
353
357
 
354
- &::before {
355
- content: "";
356
- position: absolute;
357
- top: 0;
358
- left: 0;
359
- width: 100%;
360
- height: 100%;
361
- background: var(--overlay);
362
- pointer-events: none;
363
- z-index: 1;
364
- }
365
-
366
358
  &:global(.blur) {
367
359
  filter: blur(10px) brightness(0.8);
368
360
  transform: scale(1.045);
@@ -384,7 +376,7 @@
384
376
  min-height: 0;
385
377
 
386
378
  & > :global(.paywall-content-scroll) {
387
- flex-grow: 1;
379
+ flex: 1 1 0;
388
380
  min-height: 0;
389
381
  overflow-y: auto;
390
382
  }
@@ -1,7 +1,9 @@
1
1
  <script module lang="ts">
2
2
  import { defineMeta } from "@storybook/addon-svelte-csf";
3
+ import type { ComponentProps } from "svelte";
3
4
 
4
5
  import Screen from "./Screen.svelte";
6
+ import Main from "../layout/Main/Main.svelte";
5
7
  import { paywallData, uiConfigData } from "../../stories/fixtures";
6
8
  import type { WorkflowScreen } from "../../types/workflow";
7
9
 
@@ -20,6 +22,7 @@
20
22
  const { Story } = defineMeta({
21
23
  title: "Components/Screen",
22
24
  component: Screen,
25
+ render: template,
23
26
  args: {
24
27
  paywallComponents: defaultScreen,
25
28
  selectedLocale: defaultScreen.default_locale,
@@ -28,4 +31,43 @@
28
31
  });
29
32
  </script>
30
33
 
34
+ {#snippet template(props: ComponentProps<typeof Screen>)}
35
+ <div class="viewport-frame">
36
+ <Main paywallData={props.paywallComponents}>
37
+ <div class="content-wrapper">
38
+ <Screen {...props} />
39
+ </div>
40
+ </Main>
41
+ </div>
42
+ {/snippet}
43
+
31
44
  <Story name="Default" />
45
+
46
+ <style>
47
+ .viewport-frame {
48
+ position: fixed;
49
+ inset: 0;
50
+ display: flex;
51
+ flex-direction: column;
52
+ background-color: var(--rc-purchases-ui-bg-color, Canvas);
53
+ color-scheme: light dark;
54
+ overflow: hidden;
55
+ }
56
+
57
+ .content-wrapper {
58
+ width: 100%;
59
+ display: flex;
60
+ flex: 1 1 auto;
61
+ flex-direction: column;
62
+ min-height: 0;
63
+ overflow: hidden;
64
+ }
65
+
66
+ :global(#screen-container) {
67
+ display: flex;
68
+ flex-direction: column;
69
+ flex: 1 1 auto;
70
+ min-height: 0;
71
+ overflow: hidden;
72
+ }
73
+ </style>
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export { default as Stack } from "./components/stack/Stack.svelte";
13
13
  export { default as Text } from "./components/text/Text.svelte";
14
14
  export { default as Timeline } from "./components/timeline/Timeline.svelte";
15
15
  export { default as Screen } from "./components/workflows/Screen.svelte";
16
+ export { default as Main } from "./components/layout/Main/Main.svelte";
16
17
  export { default as Video } from "./components/video/Video.svelte";
17
18
  export * from "./types";
18
19
  export { type PaywallData } from "./types/paywall";
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ export { default as Stack } from "./components/stack/Stack.svelte";
14
14
  export { default as Text } from "./components/text/Text.svelte";
15
15
  export { default as Timeline } from "./components/timeline/Timeline.svelte";
16
16
  export { default as Screen } from "./components/workflows/Screen.svelte";
17
+ export { default as Main } from "./components/layout/Main/Main.svelte";
17
18
  export { default as Video } from "./components/video/Video.svelte";
18
19
  export * from "./types";
19
20
  export {} from "./types/paywall";
@@ -1,4 +1,19 @@
1
1
  import type { ColorMode } from "../types";
2
2
  import type { Background } from "../types/background";
3
3
  import type { ColorGradientScheme } from "../types/colors";
4
+ import type { PaywallData } from "../types/paywall";
5
+ export type PaywallRootBackgroundModel = {
6
+ kind: "none";
7
+ } | {
8
+ kind: "style";
9
+ style: Record<string, string>;
10
+ overlay: string;
11
+ } | {
12
+ kind: "image";
13
+ src: string;
14
+ fit: string;
15
+ position: string;
16
+ overlay: string;
17
+ };
18
+ export declare function paywallRootBackgroundModel(paywallData: PaywallData | null | undefined, colorMode: ColorMode): PaywallRootBackgroundModel;
4
19
  export declare function mapBackground(colorMode: ColorMode, background_color: ColorGradientScheme | null | undefined, background: Background | null | undefined): Record<string, string>;
@@ -23,6 +23,27 @@ function mapBackgroundValue(colorMode, background_color, background) {
23
23
  }
24
24
  return mapBackgroundVideo(colorMode, background.value);
25
25
  }
26
+ export function paywallRootBackgroundModel(paywallData, colorMode) {
27
+ const background = paywallData?.components_config?.base?.background;
28
+ if (background == null) {
29
+ return { kind: "none" };
30
+ }
31
+ if (background.type !== "image") {
32
+ const styles = mapBackground(colorMode, null, background);
33
+ const overlay = styles["--overlay"] ?? "none";
34
+ const style = { ...styles };
35
+ delete style["--overlay"];
36
+ return { kind: "style", style, overlay };
37
+ }
38
+ const files = mapColorMode(colorMode, background.value);
39
+ return {
40
+ kind: "image",
41
+ src: files.webp,
42
+ fit: mapFitMode(background.fit_mode),
43
+ position: background.fit_mode === "fill" ? "center" : "top center",
44
+ overlay: mapOverlay(colorMode, background.color_overlay),
45
+ };
46
+ }
26
47
  export function mapBackground(colorMode, background_color, background) {
27
48
  const value = mapBackgroundValue(colorMode, background_color, background);
28
49
  if (background?.type !== "image") {
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": "3.12.1",
5
+ "version": "4.0.0",
6
6
  "author": {
7
7
  "name": "RevenueCat, Inc."
8
8
  },
@@ -93,6 +93,7 @@
93
93
  "@testing-library/svelte": "^5.3.1",
94
94
  "@types/node": "24.9.2",
95
95
  "@types/qrcode": "^1.5.6",
96
+ "@types/react": "^19.2.14",
96
97
  "@typescript-eslint/parser": "8.57.2",
97
98
  "chromatic": "13.3.2",
98
99
  "eslint": "9.38.0",
@@ -122,4 +123,4 @@
122
123
  "eslint --fix"
123
124
  ]
124
125
  }
125
- }
126
+ }