@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.
- package/dist/components/layout/Main/Main.stories.svelte +263 -0
- package/dist/components/layout/Main/Main.stories.svelte.d.ts +19 -0
- package/dist/components/layout/Main/Main.svelte +117 -0
- package/dist/components/layout/Main/Main.svelte.d.ts +11 -0
- package/dist/components/layout/Main/ViewportBackdrop.svelte +65 -0
- package/dist/components/layout/Main/ViewportBackdrop.svelte.d.ts +7 -0
- package/dist/components/paywall/Paywall.stories.svelte +30 -4
- package/dist/components/paywall/Paywall.svelte +15 -23
- package/dist/components/workflows/Screen.stories.svelte +42 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/background-utils.d.ts +15 -0
- package/dist/utils/background-utils.js +21 -0
- package/package.json +3 -2
|
@@ -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 {
|
|
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
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
|
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:
|
|
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
|
|
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": "
|
|
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
|
+
}
|