@revenuecat/purchases-ui-js 4.14.2 → 4.16.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/button/ButtonNode.svelte +6 -0
- package/dist/components/package/Package.svelte +82 -10
- package/dist/components/paywall/Paywall.stories.svelte +335 -1
- package/dist/components/paywall/Paywall.svelte +95 -4
- package/dist/components/paywall/Paywall.svelte.d.ts +1 -0
- package/dist/components/paywall/Sheet.svelte +31 -1
- package/dist/components/paywall/fixtures/stack-scroll-paywall.d.ts +40 -0
- package/dist/components/paywall/fixtures/stack-scroll-paywall.js +280 -0
- package/dist/components/paywall/fixtures/window-rule-packages-paywall.d.ts +14 -0
- package/dist/components/paywall/fixtures/window-rule-packages-paywall.js +89 -0
- package/dist/components/paywall/fixtures/window-size-paywall.d.ts +2 -0
- package/dist/components/paywall/fixtures/window-size-paywall.js +48 -0
- package/dist/components/stack/Stack.svelte +87 -9
- package/dist/components/stack/Stack.svelte.d.ts +2 -0
- package/dist/components/stack/stack-scroll.d.ts +59 -0
- package/dist/components/stack/stack-scroll.js +162 -0
- package/dist/components/tabs/Tabs.svelte +11 -0
- package/dist/components/workflows/Screen.svelte +3 -0
- package/dist/components/workflows/Screen.svelte.d.ts +1 -0
- package/dist/components/workflows/Workflow.svelte +3 -0
- package/dist/components/workflows/Workflow.svelte.d.ts +1 -0
- package/dist/stores/condition-context.svelte.js +3 -0
- package/dist/stores/default-selection.d.ts +17 -0
- package/dist/stores/default-selection.js +21 -0
- package/dist/stores/rendered-packages.d.ts +10 -0
- package/dist/stores/rendered-packages.js +17 -0
- package/dist/stores/window-size.d.ts +10 -0
- package/dist/stores/window-size.js +10 -0
- package/dist/types/components/button.d.ts +1 -1
- package/dist/types/components/package.d.ts +1 -0
- package/dist/types/components/stack.d.ts +6 -0
- package/dist/types/overrides.d.ts +15 -1
- package/dist/types/overrides.js +3 -0
- package/dist/types/paywall-component-interaction.d.ts +1 -1
- package/dist/utils/package-selection.d.ts +26 -0
- package/dist/utils/package-selection.js +47 -0
- package/dist/utils/style-utils.d.ts +12 -0
- package/dist/utils/style-utils.js +59 -0
- package/package.json +2 -2
|
@@ -89,6 +89,12 @@
|
|
|
89
89
|
componentName: props.name,
|
|
90
90
|
componentValue: "screen_redirect",
|
|
91
91
|
};
|
|
92
|
+
case "cookie_settings":
|
|
93
|
+
return {
|
|
94
|
+
componentType: "button",
|
|
95
|
+
componentName: props.name,
|
|
96
|
+
componentValue: "navigate_to_cookie_settings",
|
|
97
|
+
};
|
|
92
98
|
case "privacy_policy":
|
|
93
99
|
case "terms":
|
|
94
100
|
case "url": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
setVariablesContext,
|
|
9
9
|
} from "../../stores/variables";
|
|
10
10
|
import type { PackageProps } from "../../types/components/package";
|
|
11
|
-
import { untrack } from "svelte";
|
|
11
|
+
import { onDestroy, untrack } from "svelte";
|
|
12
12
|
import { writable } from "svelte/store";
|
|
13
13
|
import {
|
|
14
14
|
getPackageInfoContext,
|
|
@@ -20,9 +20,29 @@
|
|
|
20
20
|
getSelectedPackageScopeContext,
|
|
21
21
|
} from "../../stores/relativeDiscount";
|
|
22
22
|
import { withScopedRelativeDiscount } from "../../utils/relative-discount";
|
|
23
|
+
import { useConditionContext } from "../../stores/condition-context.svelte";
|
|
24
|
+
import {
|
|
25
|
+
getOptionalRenderedPackagesContext,
|
|
26
|
+
getPackageSheetContext,
|
|
27
|
+
} from "../../stores/rendered-packages";
|
|
28
|
+
import { getDefaultSelectionContext } from "../../stores/default-selection";
|
|
29
|
+
import {
|
|
30
|
+
resolveOverrideProperties,
|
|
31
|
+
type ConditionContext,
|
|
32
|
+
} from "../../utils/style-utils";
|
|
33
|
+
|
|
34
|
+
const {
|
|
35
|
+
id,
|
|
36
|
+
visible,
|
|
37
|
+
overrides,
|
|
38
|
+
stack,
|
|
39
|
+
package_id,
|
|
40
|
+
is_selected_by_default,
|
|
41
|
+
name,
|
|
42
|
+
}: PackageProps = $props();
|
|
23
43
|
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
// Without state, as the SDKs evaluate a package's own rules.
|
|
45
|
+
const buildConditionContext = useConditionContext();
|
|
26
46
|
|
|
27
47
|
// Expose the current package_id via a store so descendants stay in sync
|
|
28
48
|
// when this component is reused across tab switches
|
|
@@ -41,16 +61,19 @@
|
|
|
41
61
|
infoPerPackage,
|
|
42
62
|
} = getPaywallContext();
|
|
43
63
|
|
|
44
|
-
// When a package with is_selected_by_default appears,
|
|
45
|
-
// This handles tab switches where a new default package
|
|
46
|
-
// selectedPackageId is deliberately not tracked so that manually
|
|
47
|
-
// selecting another package does not re-trigger this effect and
|
|
48
|
-
//
|
|
64
|
+
// When a package with is_selected_by_default appears with its paywall, tab
|
|
65
|
+
// or sheet, select it. This handles tab switches where a new default package
|
|
66
|
+
// is shown. selectedPackageId is deliberately not tracked so that manually
|
|
67
|
+
// selecting another package does not re-trigger this effect and snap
|
|
68
|
+
// selection back to the default.
|
|
69
|
+
const isDefaultSelectionOpen = getDefaultSelectionContext();
|
|
49
70
|
$effect(() => {
|
|
50
71
|
if (is_selected_by_default) {
|
|
51
72
|
const newPackageId = package_id;
|
|
52
73
|
untrack(() => {
|
|
53
|
-
|
|
74
|
+
if (isDefaultSelectionOpen()) {
|
|
75
|
+
$selectedPackageId = newPackageId;
|
|
76
|
+
}
|
|
54
77
|
});
|
|
55
78
|
}
|
|
56
79
|
});
|
|
@@ -127,6 +150,55 @@
|
|
|
127
150
|
packageInfoStore.set(computedPackageInfo);
|
|
128
151
|
});
|
|
129
152
|
setPackageInfoContext(packageInfoStore);
|
|
153
|
+
|
|
154
|
+
// buildConditionContext() reads the contexts above this package, so pass its
|
|
155
|
+
// own selected state, package info and variables.
|
|
156
|
+
const visibleWith = (
|
|
157
|
+
context: Partial<Pick<ConditionContext, "selected" | "selectedPackageId">>,
|
|
158
|
+
) => {
|
|
159
|
+
const resolved = resolveOverrideProperties(
|
|
160
|
+
{
|
|
161
|
+
...buildConditionContext(),
|
|
162
|
+
packageInfo: computedPackageInfo,
|
|
163
|
+
...(computedVariables ? { variables: computedVariables } : {}),
|
|
164
|
+
...context,
|
|
165
|
+
},
|
|
166
|
+
overrides,
|
|
167
|
+
);
|
|
168
|
+
return (resolved.visible ?? visible) !== false;
|
|
169
|
+
};
|
|
170
|
+
const isVisible = $derived(visibleWith({ selected: isSelected }));
|
|
171
|
+
// Only a package that shows both now, unselected, and once it's selected can
|
|
172
|
+
// take the selection; one hidden once selected would just hide again.
|
|
173
|
+
const isSelectable = $derived(
|
|
174
|
+
visibleWith({ selected: false }) &&
|
|
175
|
+
visibleWith({ selected: true, selectedPackageId: package_id }),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const renderedPackages = getOptionalRenderedPackagesContext();
|
|
179
|
+
const sheetId = getPackageSheetContext();
|
|
180
|
+
if (renderedPackages) {
|
|
181
|
+
const registration = Symbol("package");
|
|
182
|
+
$effect(() => {
|
|
183
|
+
// Getters, so the paywall reads the current visibility even before this
|
|
184
|
+
// effect re-runs.
|
|
185
|
+
renderedPackages.set(registration, {
|
|
186
|
+
componentId: id,
|
|
187
|
+
packageId: package_id,
|
|
188
|
+
isSelectedByDefault: is_selected_by_default,
|
|
189
|
+
sheetId,
|
|
190
|
+
get visible() {
|
|
191
|
+
return isVisible;
|
|
192
|
+
},
|
|
193
|
+
get selectable() {
|
|
194
|
+
return isSelectable;
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
onDestroy(() => renderedPackages.delete(registration));
|
|
199
|
+
}
|
|
130
200
|
</script>
|
|
131
201
|
|
|
132
|
-
|
|
202
|
+
{#if isVisible}
|
|
203
|
+
<Stack {...stack} onclick={onPackageClick} />
|
|
204
|
+
{/if}
|
|
@@ -23,13 +23,29 @@
|
|
|
23
23
|
zStackTemplate,
|
|
24
24
|
} from "../../stories/fixtures";
|
|
25
25
|
import { waitForAnimations } from "storybook/internal/preview-api";
|
|
26
|
-
import { expect, userEvent, within } from "storybook/test";
|
|
26
|
+
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
|
|
27
27
|
import { viewportDecorator } from "../../stories/viewport-decorator";
|
|
28
28
|
import { BACKGROUND_PAYWALL } from "./fixtures/background-paywall";
|
|
29
29
|
import { FIXED_HEIGHT_STACK_PAYWALL } from "./fixtures/fixed-height-stack-paywall";
|
|
30
|
+
import {
|
|
31
|
+
ALIGNED_SCROLL_PAYWALL,
|
|
32
|
+
CENTERED_ROW_SCROLL_PAYWALL,
|
|
33
|
+
DECORATED_SCROLL_PAYWALL,
|
|
34
|
+
fitAroundFitScrollPaywall,
|
|
35
|
+
HORIZONTAL_SCROLL_PAYWALL,
|
|
36
|
+
ROOT_NO_SCROLL_PAYWALL,
|
|
37
|
+
SCROLLING_FILL_IN_FIT_SHEET_PAYWALL,
|
|
38
|
+
SCROLLING_FILL_SHEET_PAYWALL,
|
|
39
|
+
SCROLLING_FIT_SHEET_PAYWALL,
|
|
40
|
+
SIDE_BY_SIDE_SCROLL_PAYWALL,
|
|
41
|
+
STACKED_FIT_SCROLL_PAYWALL,
|
|
42
|
+
WINDOW_RULE_COLUMNS_PAYWALL,
|
|
43
|
+
} from "./fixtures/stack-scroll-paywall";
|
|
30
44
|
import { FILL_HEIGHT_WEBVIEW_PAYWALL } from "./fixtures/fill-height-webview-paywall";
|
|
45
|
+
import { WINDOW_RULE_PACKAGES_PAYWALL } from "./fixtures/window-rule-packages-paywall";
|
|
31
46
|
import { createHeroMediaPaywall } from "./fixtures/hero-media-paywall";
|
|
32
47
|
import { OVERRIDE_PAYWALL } from "./fixtures/override-paywall";
|
|
48
|
+
import { WINDOW_SIZE_PAYWALL } from "./fixtures/window-size-paywall";
|
|
33
49
|
import { SHEET_PAYWALL } from "./fixtures/sheet-paywall";
|
|
34
50
|
import { SHEET_PAYWALL_VIDEO_STACKING } from "./fixtures/sheet-video-stacking-paywall";
|
|
35
51
|
import { GRADIENT_SHEET_PAYWALL } from "./fixtures/gradient-sheet-paywall";
|
|
@@ -56,6 +72,30 @@
|
|
|
56
72
|
paywallWithHeaderAndFooter,
|
|
57
73
|
} from "./fixtures/transparent-footer-paywall";
|
|
58
74
|
|
|
75
|
+
// Boxes inside `root` that are scrolling containers with content to scroll.
|
|
76
|
+
const scrollers = (root: Element) =>
|
|
77
|
+
[...root.querySelectorAll<HTMLElement>("*")].filter((el) => {
|
|
78
|
+
const style = getComputedStyle(el);
|
|
79
|
+
const scrolls = (overflow: string) =>
|
|
80
|
+
["auto", "scroll"].includes(overflow);
|
|
81
|
+
return (
|
|
82
|
+
(scrolls(style.overflowY) && el.scrollHeight > el.clientHeight + 1) ||
|
|
83
|
+
(scrolls(style.overflowX) && el.scrollWidth > el.clientWidth + 1)
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// The fixed green box below a scrolling column is fully in view.
|
|
88
|
+
const expectOfferInView = (root: Element) => {
|
|
89
|
+
const offer = [...root.querySelectorAll(".stack")].find(
|
|
90
|
+
(el) => getComputedStyle(el).backgroundColor === "rgb(17, 212, 131)",
|
|
91
|
+
)!;
|
|
92
|
+
const paywall = root.querySelector(".paywall-content-scroll")!;
|
|
93
|
+
expect(offer.getBoundingClientRect().height).toBe(120);
|
|
94
|
+
expect(offer.getBoundingClientRect().bottom).toBeLessThanOrEqual(
|
|
95
|
+
paywall.getBoundingClientRect().bottom,
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
59
99
|
const { Story } = defineMeta({
|
|
60
100
|
title: "Example/Paywall",
|
|
61
101
|
component: Paywall,
|
|
@@ -105,6 +145,273 @@
|
|
|
105
145
|
}}
|
|
106
146
|
/>
|
|
107
147
|
|
|
148
|
+
<!--
|
|
149
|
+
Scroll stories render in a frame bounded to the viewport, so scrolling stacks
|
|
150
|
+
have a size to stop at. The shared frame grows with its content.
|
|
151
|
+
-->
|
|
152
|
+
{#snippet boundedFrame(props: ComponentProps<typeof Paywall>)}
|
|
153
|
+
<div class="paywall-story-frame bounded">
|
|
154
|
+
<Paywall {...props} />
|
|
155
|
+
</div>
|
|
156
|
+
{/snippet}
|
|
157
|
+
|
|
158
|
+
<Story
|
|
159
|
+
name="Scrolling Fit stack above a fixed box"
|
|
160
|
+
decorators={[viewportDecorator(375, 640, 0)]}
|
|
161
|
+
play={async ({ canvasElement }) => {
|
|
162
|
+
await waitFor(() => {
|
|
163
|
+
expect(scrollers(canvasElement)).toHaveLength(1);
|
|
164
|
+
expectOfferInView(canvasElement);
|
|
165
|
+
});
|
|
166
|
+
}}
|
|
167
|
+
args={{ paywallData: STACKED_FIT_SCROLL_PAYWALL }}
|
|
168
|
+
template={boundedFrame}
|
|
169
|
+
/>
|
|
170
|
+
|
|
171
|
+
<Story
|
|
172
|
+
name="Scrolling Fit stacks side by side"
|
|
173
|
+
decorators={[viewportDecorator(800, 600, 0)]}
|
|
174
|
+
play={async ({ canvasElement }) => {
|
|
175
|
+
await waitFor(() => expect(scrollers(canvasElement)).toHaveLength(2));
|
|
176
|
+
}}
|
|
177
|
+
args={{ paywallData: SIDE_BY_SIDE_SCROLL_PAYWALL }}
|
|
178
|
+
template={boundedFrame}
|
|
179
|
+
/>
|
|
180
|
+
|
|
181
|
+
<Story
|
|
182
|
+
name="Scrolling Fit stacks in a centered row"
|
|
183
|
+
decorators={[viewportDecorator(800, 600, 0)]}
|
|
184
|
+
play={async ({ canvasElement }) => {
|
|
185
|
+
const box = (id: string) =>
|
|
186
|
+
[...canvasElement.querySelectorAll(".stack")].find(
|
|
187
|
+
(el) =>
|
|
188
|
+
getComputedStyle(el).backgroundColor === id &&
|
|
189
|
+
el.closest(".rc-stack-scroll"),
|
|
190
|
+
)!;
|
|
191
|
+
await waitFor(() => {
|
|
192
|
+
// Only the tall column scrolls.
|
|
193
|
+
expect(scrollers(canvasElement)).toHaveLength(1);
|
|
194
|
+
const paywall = canvasElement
|
|
195
|
+
.querySelector(".paywall-content-scroll")!
|
|
196
|
+
.getBoundingClientRect();
|
|
197
|
+
const short = box("rgb(17, 212, 131)")
|
|
198
|
+
.closest(".rc-stack-scroll")!
|
|
199
|
+
.parentElement!.getBoundingClientRect();
|
|
200
|
+
// The short column stays centered in the row.
|
|
201
|
+
const paywallMiddle = paywall.top + paywall.height / 2;
|
|
202
|
+
expect(
|
|
203
|
+
Math.abs(short.top + short.height / 2 - paywallMiddle),
|
|
204
|
+
).toBeLessThan(1);
|
|
205
|
+
});
|
|
206
|
+
}}
|
|
207
|
+
args={{ paywallData: CENTERED_ROW_SCROLL_PAYWALL }}
|
|
208
|
+
template={boundedFrame}
|
|
209
|
+
/>
|
|
210
|
+
|
|
211
|
+
<Story
|
|
212
|
+
name="Horizontal scrolling stack"
|
|
213
|
+
decorators={[viewportDecorator(375, 640, 0)]}
|
|
214
|
+
play={async ({ canvasElement }) => {
|
|
215
|
+
await waitFor(() => {
|
|
216
|
+
const [row, ...others] = scrollers(canvasElement);
|
|
217
|
+
expect(others).toHaveLength(0);
|
|
218
|
+
expect(row?.classList.contains("rc-stack-scroll")).toBe(true);
|
|
219
|
+
});
|
|
220
|
+
}}
|
|
221
|
+
args={{ paywallData: HORIZONTAL_SCROLL_PAYWALL }}
|
|
222
|
+
template={boundedFrame}
|
|
223
|
+
/>
|
|
224
|
+
|
|
225
|
+
<Story
|
|
226
|
+
name="Root scrolling off"
|
|
227
|
+
decorators={[viewportDecorator(375, 640, 0)]}
|
|
228
|
+
play={async ({ canvasElement }) => {
|
|
229
|
+
await waitFor(() => expect(scrollers(canvasElement)).toHaveLength(0));
|
|
230
|
+
}}
|
|
231
|
+
args={{ paywallData: ROOT_NO_SCROLL_PAYWALL }}
|
|
232
|
+
template={boundedFrame}
|
|
233
|
+
/>
|
|
234
|
+
|
|
235
|
+
<Story
|
|
236
|
+
name="Window size rule, narrow"
|
|
237
|
+
decorators={[viewportDecorator(375, 640, 0)]}
|
|
238
|
+
play={async ({ canvasElement }) => {
|
|
239
|
+
await waitFor(() =>
|
|
240
|
+
expect(
|
|
241
|
+
scrollers(canvasElement).map((el) =>
|
|
242
|
+
el.classList.contains("paywall-content-scroll"),
|
|
243
|
+
),
|
|
244
|
+
).toEqual([true]),
|
|
245
|
+
);
|
|
246
|
+
}}
|
|
247
|
+
args={{ paywallData: WINDOW_RULE_COLUMNS_PAYWALL }}
|
|
248
|
+
template={boundedFrame}
|
|
249
|
+
/>
|
|
250
|
+
|
|
251
|
+
<Story
|
|
252
|
+
name="Window size rule, wide"
|
|
253
|
+
decorators={[viewportDecorator(800, 600, 0)]}
|
|
254
|
+
play={async ({ canvasElement }) => {
|
|
255
|
+
await waitFor(() => expect(scrollers(canvasElement)).toHaveLength(2));
|
|
256
|
+
}}
|
|
257
|
+
args={{ paywallData: WINDOW_RULE_COLUMNS_PAYWALL }}
|
|
258
|
+
template={boundedFrame}
|
|
259
|
+
/>
|
|
260
|
+
|
|
261
|
+
<Story
|
|
262
|
+
name="Scrolling stacks aligned to the center and end"
|
|
263
|
+
decorators={[viewportDecorator(375, 640, 0)]}
|
|
264
|
+
play={async ({ canvasElement }) => {
|
|
265
|
+
await waitFor(() => expect(scrollers(canvasElement)).toHaveLength(2));
|
|
266
|
+
for (const viewport of scrollers(canvasElement)) {
|
|
267
|
+
const rows = viewport.firstElementChild!;
|
|
268
|
+
viewport.scrollTop = 0;
|
|
269
|
+
expect(rows.firstElementChild!.getBoundingClientRect().top).toBe(
|
|
270
|
+
viewport.getBoundingClientRect().top,
|
|
271
|
+
);
|
|
272
|
+
viewport.scrollTop = viewport.scrollHeight;
|
|
273
|
+
expect(rows.lastElementChild!.getBoundingClientRect().bottom).toBe(
|
|
274
|
+
viewport.getBoundingClientRect().bottom,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}}
|
|
278
|
+
args={{ paywallData: ALIGNED_SCROLL_PAYWALL }}
|
|
279
|
+
template={boundedFrame}
|
|
280
|
+
/>
|
|
281
|
+
|
|
282
|
+
<Story
|
|
283
|
+
name="Scrolling stacks keep their border, badge and video"
|
|
284
|
+
decorators={[viewportDecorator(375, 640, 0)]}
|
|
285
|
+
play={async ({ canvasElement }) => {
|
|
286
|
+
await waitFor(() => expect(scrollers(canvasElement)).toHaveLength(2));
|
|
287
|
+
const [bordered, video] = scrollers(canvasElement);
|
|
288
|
+
const stack = bordered!.parentElement!;
|
|
289
|
+
expect(getComputedStyle(stack).borderTopWidth).toBe("10px");
|
|
290
|
+
expect(getComputedStyle(stack).overflowY).toBe("visible");
|
|
291
|
+
const badge = [...stack.children].find((el) => el !== bordered)!;
|
|
292
|
+
const badgeTop = badge.getBoundingClientRect().top;
|
|
293
|
+
bordered!.scrollTop = 150;
|
|
294
|
+
expect(badge.getBoundingClientRect().top).toBe(badgeTop);
|
|
295
|
+
video!.scrollTop = 150;
|
|
296
|
+
expect(video!.scrollTop).toBeGreaterThan(0);
|
|
297
|
+
}}
|
|
298
|
+
args={{ paywallData: DECORATED_SCROLL_PAYWALL }}
|
|
299
|
+
template={boundedFrame}
|
|
300
|
+
/>
|
|
301
|
+
|
|
302
|
+
<Story
|
|
303
|
+
name="Fit stack around a scrolling Fit stack"
|
|
304
|
+
decorators={[viewportDecorator(375, 640, 0)]}
|
|
305
|
+
play={async ({ canvasElement }) => {
|
|
306
|
+
await waitFor(() => {
|
|
307
|
+
expect(scrollers(canvasElement)).toHaveLength(1);
|
|
308
|
+
expectOfferInView(canvasElement);
|
|
309
|
+
});
|
|
310
|
+
}}
|
|
311
|
+
args={{ paywallData: fitAroundFitScrollPaywall(false) }}
|
|
312
|
+
template={boundedFrame}
|
|
313
|
+
/>
|
|
314
|
+
|
|
315
|
+
<Story
|
|
316
|
+
name="Fit stack around a scrolling package"
|
|
317
|
+
decorators={[viewportDecorator(375, 640, 0)]}
|
|
318
|
+
play={async ({ canvasElement }) => {
|
|
319
|
+
await waitFor(() => {
|
|
320
|
+
expect(scrollers(canvasElement)).toHaveLength(1);
|
|
321
|
+
expectOfferInView(canvasElement);
|
|
322
|
+
});
|
|
323
|
+
}}
|
|
324
|
+
args={{ paywallData: fitAroundFitScrollPaywall(true) }}
|
|
325
|
+
template={boundedFrame}
|
|
326
|
+
/>
|
|
327
|
+
|
|
328
|
+
<Story
|
|
329
|
+
name="Window size rule hides a package, narrow"
|
|
330
|
+
decorators={[viewportDecorator(375, 640, 0)]}
|
|
331
|
+
play={async ({ args, canvasElement }) => {
|
|
332
|
+
const canvas = within(canvasElement);
|
|
333
|
+
await canvas.findByText(/^Annual/);
|
|
334
|
+
await userEvent.click(canvas.getByText("Purchase"));
|
|
335
|
+
await waitFor(() =>
|
|
336
|
+
expect(args.onPurchaseClicked).toHaveBeenLastCalledWith("$rc_annual", ""),
|
|
337
|
+
);
|
|
338
|
+
}}
|
|
339
|
+
args={{ paywallData: WINDOW_RULE_PACKAGES_PAYWALL, onPurchaseClicked: fn() }}
|
|
340
|
+
template={boundedFrame}
|
|
341
|
+
/>
|
|
342
|
+
|
|
343
|
+
<Story
|
|
344
|
+
name="Window size rule hides a package, wide"
|
|
345
|
+
decorators={[viewportDecorator(800, 600, 0)]}
|
|
346
|
+
play={async ({ args, canvasElement }) => {
|
|
347
|
+
const canvas = within(canvasElement);
|
|
348
|
+
await canvas.findByText(/^Monthly/);
|
|
349
|
+
expect(canvas.queryByText(/^Annual/)).toBeNull();
|
|
350
|
+
await userEvent.click(canvas.getByText("Purchase"));
|
|
351
|
+
await waitFor(() =>
|
|
352
|
+
expect(args.onPurchaseClicked).toHaveBeenLastCalledWith(
|
|
353
|
+
"$rc_monthly",
|
|
354
|
+
"",
|
|
355
|
+
),
|
|
356
|
+
);
|
|
357
|
+
}}
|
|
358
|
+
args={{ paywallData: WINDOW_RULE_PACKAGES_PAYWALL, onPurchaseClicked: fn() }}
|
|
359
|
+
template={boundedFrame}
|
|
360
|
+
/>
|
|
361
|
+
|
|
362
|
+
<Story
|
|
363
|
+
name="Scrolling Fit sheet"
|
|
364
|
+
decorators={[viewportDecorator(375, 640, 0)]}
|
|
365
|
+
play={async ({ canvasElement }) => {
|
|
366
|
+
await userEvent.click(await within(canvasElement).findByText("Open sheet"));
|
|
367
|
+
await waitFor(() => {
|
|
368
|
+
const sheet = canvasElement.querySelector(".sheet")!;
|
|
369
|
+
const [contents, ...others] = scrollers(sheet.parentElement!);
|
|
370
|
+
expect(others).toHaveLength(0);
|
|
371
|
+
expect(contents).not.toBe(sheet);
|
|
372
|
+
expect(sheet.contains(contents!)).toBe(true);
|
|
373
|
+
expect(contents!.clientHeight).toBe(sheet.parentElement!.clientHeight);
|
|
374
|
+
});
|
|
375
|
+
}}
|
|
376
|
+
args={{ paywallData: SCROLLING_FIT_SHEET_PAYWALL }}
|
|
377
|
+
template={boundedFrame}
|
|
378
|
+
/>
|
|
379
|
+
|
|
380
|
+
<Story
|
|
381
|
+
name="Scrolling Fill contents in a Fit sheet"
|
|
382
|
+
decorators={[viewportDecorator(375, 640, 0)]}
|
|
383
|
+
play={async ({ canvasElement }) => {
|
|
384
|
+
await userEvent.click(await within(canvasElement).findByText("Open sheet"));
|
|
385
|
+
await waitFor(() => {
|
|
386
|
+
const sheet = canvasElement.querySelector(".sheet")!;
|
|
387
|
+
// The sheet takes the screen's height and its contents scroll inside.
|
|
388
|
+
expect(sheet.clientHeight).toBe(sheet.parentElement!.clientHeight);
|
|
389
|
+
expect(scrollers(sheet)).toHaveLength(1);
|
|
390
|
+
});
|
|
391
|
+
}}
|
|
392
|
+
args={{ paywallData: SCROLLING_FILL_IN_FIT_SHEET_PAYWALL }}
|
|
393
|
+
template={boundedFrame}
|
|
394
|
+
/>
|
|
395
|
+
|
|
396
|
+
<Story
|
|
397
|
+
name="Scrolling Fill sheet with margins"
|
|
398
|
+
decorators={[viewportDecorator(375, 640, 0)]}
|
|
399
|
+
play={async ({ canvasElement }) => {
|
|
400
|
+
await userEvent.click(await within(canvasElement).findByText("Open sheet"));
|
|
401
|
+
await waitFor(() => {
|
|
402
|
+
const sheet = canvasElement.querySelector(".sheet")!;
|
|
403
|
+
expect(scrollers(sheet)).toHaveLength(1);
|
|
404
|
+
// The contents stop 40px (their bottom margin) above the sheet's bottom.
|
|
405
|
+
const contents = sheet.querySelector(".stack")!;
|
|
406
|
+
expect(contents.getBoundingClientRect().bottom + 40).toBeLessThanOrEqual(
|
|
407
|
+
sheet.getBoundingClientRect().bottom + 0.5,
|
|
408
|
+
);
|
|
409
|
+
});
|
|
410
|
+
}}
|
|
411
|
+
args={{ paywallData: SCROLLING_FILL_SHEET_PAYWALL }}
|
|
412
|
+
template={boundedFrame}
|
|
413
|
+
/>
|
|
414
|
+
|
|
108
415
|
<!--
|
|
109
416
|
Regression for FUN-2262: fill-height Stack + fill-height WebView must fill
|
|
110
417
|
the space between the fixed header/footer bars. Collapse shows as the bars
|
|
@@ -125,6 +432,29 @@
|
|
|
125
432
|
}}
|
|
126
433
|
/>
|
|
127
434
|
|
|
435
|
+
<!-- Resize the canvas across 600px wide, or past square, to see the rules switch. -->
|
|
436
|
+
<Story
|
|
437
|
+
name="Window size rules, narrow"
|
|
438
|
+
decorators={[viewportDecorator(375, 640, 0)]}
|
|
439
|
+
play={async ({ canvasElement }) => {
|
|
440
|
+
const canvas = within(canvasElement);
|
|
441
|
+
await expect(await canvas.findByText("Width under 600px")).toBeVisible();
|
|
442
|
+
await expect(canvas.getByText("Portrait or square")).toBeVisible();
|
|
443
|
+
}}
|
|
444
|
+
args={{ paywallData: WINDOW_SIZE_PAYWALL }}
|
|
445
|
+
/>
|
|
446
|
+
|
|
447
|
+
<Story
|
|
448
|
+
name="Window size rules, wide"
|
|
449
|
+
decorators={[viewportDecorator(800, 600, 0)]}
|
|
450
|
+
play={async ({ canvasElement }) => {
|
|
451
|
+
const canvas = within(canvasElement);
|
|
452
|
+
await expect(await canvas.findByText("Width 600px or more")).toBeVisible();
|
|
453
|
+
await expect(canvas.getByText("Landscape")).toBeVisible();
|
|
454
|
+
}}
|
|
455
|
+
args={{ paywallData: WINDOW_SIZE_PAYWALL }}
|
|
456
|
+
/>
|
|
457
|
+
|
|
128
458
|
<Story
|
|
129
459
|
name="State-driven - tab state"
|
|
130
460
|
decorators={[viewportDecorator(375, 640, 0)]}
|
|
@@ -817,6 +1147,10 @@
|
|
|
817
1147
|
/>
|
|
818
1148
|
|
|
819
1149
|
<style>
|
|
1150
|
+
.paywall-story-frame.bounded {
|
|
1151
|
+
min-height: 0;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
820
1154
|
.paywall-story-frame {
|
|
821
1155
|
flex: 1 1 auto;
|
|
822
1156
|
display: flex;
|
|
@@ -50,10 +50,22 @@
|
|
|
50
50
|
} from "../../stores/relativeDiscount";
|
|
51
51
|
import {
|
|
52
52
|
collectPackageIdsInDisplayOrder,
|
|
53
|
+
collectPackagePlacements,
|
|
53
54
|
findSelectedPackageId,
|
|
54
55
|
} from "../../utils/style-utils";
|
|
56
|
+
import { reconciledSelection } from "../../utils/package-selection";
|
|
55
57
|
import { onMount } from "svelte";
|
|
56
|
-
import {
|
|
58
|
+
import { SvelteMap } from "svelte/reactivity";
|
|
59
|
+
import { derived, readable, toStore, writable } from "svelte/store";
|
|
60
|
+
import { setWindowSizeContext } from "../../stores/window-size";
|
|
61
|
+
import {
|
|
62
|
+
setRenderedPackagesContext,
|
|
63
|
+
type RenderedPackages,
|
|
64
|
+
} from "../../stores/rendered-packages";
|
|
65
|
+
import {
|
|
66
|
+
openDefaultSelection,
|
|
67
|
+
setDefaultSelectionContext,
|
|
68
|
+
} from "../../stores/default-selection";
|
|
57
69
|
import Stack from "../stack/Stack.svelte";
|
|
58
70
|
import {
|
|
59
71
|
firstContentIsHeroMedia,
|
|
@@ -106,6 +118,7 @@
|
|
|
106
118
|
/** Called when a `close_workflow` button is pressed to dismiss the paywall/workflow. */
|
|
107
119
|
onClose?: () => void;
|
|
108
120
|
onVisitCustomerCenterClicked?: () => void;
|
|
121
|
+
onCookieSettingsClicked?: () => void;
|
|
109
122
|
onRestorePurchasesClicked?: () => void;
|
|
110
123
|
onNavigateToUrlClicked?: (url: string) => void;
|
|
111
124
|
/**
|
|
@@ -180,6 +193,7 @@
|
|
|
180
193
|
onBackClicked,
|
|
181
194
|
onClose,
|
|
182
195
|
onVisitCustomerCenterClicked,
|
|
196
|
+
onCookieSettingsClicked,
|
|
183
197
|
onRestorePurchasesClicked,
|
|
184
198
|
onNavigateToUrlClicked,
|
|
185
199
|
onCompleteWorkflowNavigate,
|
|
@@ -322,6 +336,9 @@
|
|
|
322
336
|
case "screen_redirect":
|
|
323
337
|
onActionTriggered?.("screen_redirect");
|
|
324
338
|
return;
|
|
339
|
+
case "cookie_settings":
|
|
340
|
+
onCookieSettingsClicked?.();
|
|
341
|
+
return;
|
|
325
342
|
case "privacy_policy":
|
|
326
343
|
case "terms":
|
|
327
344
|
case "url":
|
|
@@ -464,6 +481,76 @@
|
|
|
464
481
|
|
|
465
482
|
setPackageInfoContext(packageInfo);
|
|
466
483
|
|
|
484
|
+
let paywallWidth = $state<number>();
|
|
485
|
+
let paywallHeight = $state<number>();
|
|
486
|
+
// Window size rules evaluate against the paywall's own box, as the native
|
|
487
|
+
// SDKs measure the paywall's bounds rather than the screen.
|
|
488
|
+
setWindowSizeContext(
|
|
489
|
+
toStore(() =>
|
|
490
|
+
paywallWidth == null || paywallHeight == null
|
|
491
|
+
? null
|
|
492
|
+
: { width: paywallWidth, height: paywallHeight },
|
|
493
|
+
),
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
// Without ResizeObserver the size stays unknown, so window size rules don't
|
|
497
|
+
// match instead of the paywall failing to render.
|
|
498
|
+
function measurePaywall(node: HTMLElement) {
|
|
499
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
500
|
+
// A hidden paywall measures 0 by 0, which says nothing about its size.
|
|
501
|
+
const isHidden = (width: number, height: number) =>
|
|
502
|
+
width === 0 && height === 0;
|
|
503
|
+
// Measure once before the first paint, so a wide paywall doesn't flash its
|
|
504
|
+
// narrow layout. The offset size is in whole px; ResizeObserver then
|
|
505
|
+
// reports the fractional box.
|
|
506
|
+
if (!isHidden(node.offsetWidth, node.offsetHeight)) {
|
|
507
|
+
paywallWidth = node.offsetWidth;
|
|
508
|
+
paywallHeight = node.offsetHeight;
|
|
509
|
+
}
|
|
510
|
+
let frame = 0;
|
|
511
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
512
|
+
const box = entry?.borderBoxSize?.[0];
|
|
513
|
+
const width = box?.inlineSize ?? entry?.contentRect.width ?? 0;
|
|
514
|
+
const height = box?.blockSize ?? entry?.contentRect.height ?? 0;
|
|
515
|
+
if (isHidden(width, height)) return;
|
|
516
|
+
// Next frame, so a rule that resizes the paywall doesn't set off
|
|
517
|
+
// ResizeObserver's loop error.
|
|
518
|
+
cancelAnimationFrame(frame);
|
|
519
|
+
frame = requestAnimationFrame(() => {
|
|
520
|
+
paywallWidth = width;
|
|
521
|
+
paywallHeight = height;
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
observer.observe(node);
|
|
525
|
+
return {
|
|
526
|
+
destroy: () => {
|
|
527
|
+
observer.disconnect();
|
|
528
|
+
cancelAnimationFrame(frame);
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
setDefaultSelectionContext(openDefaultSelection());
|
|
534
|
+
const renderedPackages: RenderedPackages = new SvelteMap();
|
|
535
|
+
setRenderedPackagesContext(renderedPackages);
|
|
536
|
+
const packagePlacements = collectPackagePlacements(base);
|
|
537
|
+
|
|
538
|
+
// Moves the selection off a package a rule hides, so it can't stay selected
|
|
539
|
+
// and purchasable. Waits for the first measurement when there will be one,
|
|
540
|
+
// since window size rules can't be evaluated before it.
|
|
541
|
+
const willMeasure = typeof ResizeObserver !== "undefined";
|
|
542
|
+
$effect(() => {
|
|
543
|
+
if (willMeasure && (paywallWidth == null || paywallHeight == null)) return;
|
|
544
|
+
const replacement = reconciledSelection(
|
|
545
|
+
$selectedPackageId,
|
|
546
|
+
renderedPackages.values(),
|
|
547
|
+
packagePlacements,
|
|
548
|
+
);
|
|
549
|
+
if (replacement !== undefined) {
|
|
550
|
+
selectedPackageId.set(replacement);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
467
554
|
onMount(() => {
|
|
468
555
|
registerFonts(uiConfig);
|
|
469
556
|
});
|
|
@@ -491,7 +578,7 @@
|
|
|
491
578
|
<svelte:boundary onerror={onError}>
|
|
492
579
|
<!-- Outside the root: filter/transform on `.blur` would clip the fixed backdrop to the paywall box. -->
|
|
493
580
|
<ViewportBackdrop model={viewportBackdropModel} blur={!!sheet} />
|
|
494
|
-
<div class={paywallClass} style={paywallStyle}>
|
|
581
|
+
<div class={paywallClass} style={paywallStyle} use:measurePaywall>
|
|
495
582
|
{#if header}
|
|
496
583
|
<div
|
|
497
584
|
class="header-wrapper"
|
|
@@ -510,6 +597,7 @@
|
|
|
510
597
|
bottom: (stack.padding?.bottom ?? 0) + footerHeight,
|
|
511
598
|
}
|
|
512
599
|
: stack.padding}
|
|
600
|
+
rootStack
|
|
513
601
|
class="paywall-content-scroll"
|
|
514
602
|
style={mapPaywallContentStackStyle(
|
|
515
603
|
stack.size.height,
|
|
@@ -547,7 +635,10 @@
|
|
|
547
635
|
</div>
|
|
548
636
|
|
|
549
637
|
{#if sheet}
|
|
550
|
-
|
|
638
|
+
<!-- Keyed, so opening another sheet from a sheet starts it fresh. -->
|
|
639
|
+
{#key sheet.props.id}
|
|
640
|
+
<Sheet {...sheet.props} />
|
|
641
|
+
{/key}
|
|
551
642
|
{/if}
|
|
552
643
|
</svelte:boundary>
|
|
553
644
|
|
|
@@ -600,10 +691,10 @@
|
|
|
600
691
|
flex-direction: column;
|
|
601
692
|
min-height: 0;
|
|
602
693
|
|
|
694
|
+
/* The root stack sets its own overflow: rootScrollStyle (stack-scroll.ts). */
|
|
603
695
|
& > :global(.paywall-content-scroll) {
|
|
604
696
|
flex: 1 1 auto;
|
|
605
697
|
min-height: 0;
|
|
606
|
-
overflow-y: auto;
|
|
607
698
|
}
|
|
608
699
|
}
|
|
609
700
|
|
|
@@ -33,6 +33,7 @@ interface Props {
|
|
|
33
33
|
/** Called when a `close_workflow` button is pressed to dismiss the paywall/workflow. */
|
|
34
34
|
onClose?: () => void;
|
|
35
35
|
onVisitCustomerCenterClicked?: () => void;
|
|
36
|
+
onCookieSettingsClicked?: () => void;
|
|
36
37
|
onRestorePurchasesClicked?: () => void;
|
|
37
38
|
onNavigateToUrlClicked?: (url: string) => void;
|
|
38
39
|
/**
|