@revenuecat/purchases-ui-js 4.8.5 → 4.8.7
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/footer/Footer.svelte +1 -4
- package/dist/components/paywall/Node.svelte +2 -0
- package/dist/components/paywall/Paywall.stories.svelte +27 -0
- package/dist/components/paywall/Paywall.svelte +33 -13
- package/dist/components/paywall/fixtures/transparent-footer-paywall.d.ts +7 -0
- package/dist/components/paywall/fixtures/transparent-footer-paywall.js +59 -0
- package/dist/components/web-view/WebView.stories.svelte +105 -0
- package/dist/components/web-view/WebView.stories.svelte.d.ts +19 -0
- package/dist/components/web-view/WebView.svelte +203 -0
- package/dist/components/web-view/WebView.svelte.d.ts +4 -0
- package/dist/components/web-view/WebViewTestWrapper.svelte +27 -0
- package/dist/components/web-view/WebViewTestWrapper.svelte.d.ts +4 -0
- package/dist/components/web-view/web-view-sdk.d.ts +49 -0
- package/dist/components/web-view/web-view-sdk.js +61 -0
- package/dist/components/web-view/web-view-utils.d.ts +24 -0
- package/dist/components/web-view/web-view-utils.js +52 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types/component.d.ts +2 -1
- package/dist/types/components/web-view.d.ts +15 -0
- package/dist/types/components/web-view.js +1 -0
- package/dist/types.d.ts +9 -3
- package/package.json +2 -1
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Stack from "../stack/Stack.svelte";
|
|
3
3
|
import type { FooterProps } from "../../types/components/footer";
|
|
4
|
-
import { STICKY_OVERLAY_Z_INDEX } from "../../utils/constants";
|
|
5
4
|
|
|
6
5
|
const { stack }: FooterProps = $props();
|
|
7
6
|
</script>
|
|
8
7
|
|
|
9
|
-
<div
|
|
8
|
+
<div>
|
|
10
9
|
<Stack {...stack} />
|
|
11
10
|
</div>
|
|
12
11
|
|
|
13
12
|
<style>
|
|
14
13
|
div {
|
|
15
|
-
position: sticky;
|
|
16
|
-
bottom: 0;
|
|
17
14
|
width: 100%;
|
|
18
15
|
|
|
19
16
|
display: flex;
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
Stack,
|
|
9
9
|
Timeline,
|
|
10
10
|
Video,
|
|
11
|
+
WebView,
|
|
11
12
|
} from "../..";
|
|
12
13
|
import ButtonNode from "../button/ButtonNode.svelte";
|
|
13
14
|
import Countdown from "../countdown/Countdown.svelte";
|
|
@@ -64,6 +65,7 @@
|
|
|
64
65
|
timeline: Timeline,
|
|
65
66
|
video: Video,
|
|
66
67
|
wallet_button: WalletButton,
|
|
68
|
+
web_view: WebView,
|
|
67
69
|
} satisfies {
|
|
68
70
|
[key in RenderableComponent["type"]]: SvelteComponent<
|
|
69
71
|
Extract<RenderableComponent, { type: key }>
|
|
@@ -39,6 +39,11 @@
|
|
|
39
39
|
DUELINGUE_PAYWALL,
|
|
40
40
|
} from "./fixtures/express-purchase-button-paywall";
|
|
41
41
|
import { CustomVariableValue } from "../../types/variables";
|
|
42
|
+
import {
|
|
43
|
+
paywallWithTransparentFooter,
|
|
44
|
+
paywallWithCenteredBodyAndFooter,
|
|
45
|
+
paywallWithHeaderAndFooter,
|
|
46
|
+
} from "./fixtures/transparent-footer-paywall";
|
|
42
47
|
|
|
43
48
|
const { Story } = defineMeta({
|
|
44
49
|
title: "Example/Paywall",
|
|
@@ -480,6 +485,28 @@
|
|
|
480
485
|
}}
|
|
481
486
|
/>
|
|
482
487
|
|
|
488
|
+
<Story
|
|
489
|
+
name="Sticky Footer - transparent (overlaps content)"
|
|
490
|
+
args={{
|
|
491
|
+
paywallData: paywallWithTransparentFooter,
|
|
492
|
+
}}
|
|
493
|
+
/>
|
|
494
|
+
|
|
495
|
+
<Story
|
|
496
|
+
name="Sticky Footer - centered body above footer"
|
|
497
|
+
decorators={[viewportDecorator(500, 500, 0)]}
|
|
498
|
+
args={{
|
|
499
|
+
paywallData: paywallWithCenteredBodyAndFooter,
|
|
500
|
+
}}
|
|
501
|
+
/>
|
|
502
|
+
|
|
503
|
+
<Story
|
|
504
|
+
name="Sticky Footer - with header"
|
|
505
|
+
args={{
|
|
506
|
+
paywallData: paywallWithHeaderAndFooter,
|
|
507
|
+
}}
|
|
508
|
+
/>
|
|
509
|
+
|
|
483
510
|
<Story
|
|
484
511
|
name="Header"
|
|
485
512
|
args={{
|
|
@@ -352,6 +352,13 @@
|
|
|
352
352
|
const pullContentUnderHeader = !!header && firstComponentIsFullWidthImage;
|
|
353
353
|
|
|
354
354
|
let headerHeight = $state(0);
|
|
355
|
+
let footerHeight = $state(0);
|
|
356
|
+
|
|
357
|
+
const contentColumnStyle = $derived(
|
|
358
|
+
maxContentWidth
|
|
359
|
+
? `max-width: ${maxContentWidth}; margin-inline: auto;`
|
|
360
|
+
: "",
|
|
361
|
+
);
|
|
355
362
|
</script>
|
|
356
363
|
|
|
357
364
|
<svelte:boundary onerror={onError}>
|
|
@@ -366,19 +373,16 @@
|
|
|
366
373
|
<Header {...header} />
|
|
367
374
|
</div>
|
|
368
375
|
{/if}
|
|
369
|
-
<div
|
|
370
|
-
class="paywall-content"
|
|
371
|
-
style={[
|
|
372
|
-
maxContentWidth
|
|
373
|
-
? `max-width: ${maxContentWidth}; margin-inline: auto;`
|
|
374
|
-
: "",
|
|
375
|
-
]
|
|
376
|
-
.filter(Boolean)
|
|
377
|
-
.join(" ")}
|
|
378
|
-
>
|
|
376
|
+
<div class="paywall-content" style={contentColumnStyle}>
|
|
379
377
|
<Stack
|
|
380
378
|
{...stack}
|
|
381
379
|
class="paywall-content-scroll"
|
|
380
|
+
padding={sticky_footer
|
|
381
|
+
? {
|
|
382
|
+
...stack.padding,
|
|
383
|
+
bottom: (stack.padding?.bottom ?? 0) + footerHeight,
|
|
384
|
+
}
|
|
385
|
+
: stack.padding}
|
|
382
386
|
style={{
|
|
383
387
|
height: "auto",
|
|
384
388
|
...(pullContentUnderHeader
|
|
@@ -386,10 +390,19 @@
|
|
|
386
390
|
: {}),
|
|
387
391
|
}}
|
|
388
392
|
/>
|
|
389
|
-
{#if sticky_footer}
|
|
390
|
-
<Footer {...sticky_footer} />
|
|
391
|
-
{/if}
|
|
392
393
|
</div>
|
|
394
|
+
{#if sticky_footer}
|
|
395
|
+
<!-- Overlaps the content so translucent footers show it; sticky rather than
|
|
396
|
+
absolute so it stays visible when an ancestor scrollport is shorter than
|
|
397
|
+
the paywall. -->
|
|
398
|
+
<div
|
|
399
|
+
class="footer-wrapper"
|
|
400
|
+
style="z-index: {STICKY_OVERLAY_Z_INDEX}; margin-top: -{footerHeight}px; {contentColumnStyle}"
|
|
401
|
+
bind:clientHeight={footerHeight}
|
|
402
|
+
>
|
|
403
|
+
<Footer {...sticky_footer} />
|
|
404
|
+
</div>
|
|
405
|
+
{/if}
|
|
393
406
|
</div>
|
|
394
407
|
|
|
395
408
|
{#if sheet}
|
|
@@ -422,6 +435,13 @@
|
|
|
422
435
|
width: 100%;
|
|
423
436
|
}
|
|
424
437
|
|
|
438
|
+
.footer-wrapper {
|
|
439
|
+
position: sticky;
|
|
440
|
+
bottom: 0;
|
|
441
|
+
width: 100%;
|
|
442
|
+
flex-shrink: 0;
|
|
443
|
+
}
|
|
444
|
+
|
|
425
445
|
.paywall-content {
|
|
426
446
|
z-index: 2;
|
|
427
447
|
width: 100%;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PaywallData } from "../../../types/paywall";
|
|
2
|
+
/** Translucent sticky footer over long scrollable content. */
|
|
3
|
+
export declare const paywallWithTransparentFooter: PaywallData;
|
|
4
|
+
/** Small body that must center in the space above the footer, not the whole screen. */
|
|
5
|
+
export declare const paywallWithCenteredBodyAndFooter: PaywallData;
|
|
6
|
+
/** Header and sticky footer together: the footer overlay must not affect the header. */
|
|
7
|
+
export declare const paywallWithHeaderAndFooter: PaywallData;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { paywallWithFooter, paywallWithHeader } from "../../../stories/fixtures";
|
|
2
|
+
function footerStack(paywall) {
|
|
3
|
+
const footer = paywall.components_config.base.sticky_footer;
|
|
4
|
+
if (!footer) {
|
|
5
|
+
throw new Error("Fixture has no sticky footer");
|
|
6
|
+
}
|
|
7
|
+
return footer.stack;
|
|
8
|
+
}
|
|
9
|
+
function lastTextComponent(components) {
|
|
10
|
+
return components.findLast((component) => component.type === "text");
|
|
11
|
+
}
|
|
12
|
+
/** Duplicates the last text component so the content is tall enough to scroll. */
|
|
13
|
+
function appendScrollableContent(paywall, count) {
|
|
14
|
+
const components = paywall.components_config.base.stack.components;
|
|
15
|
+
const lastText = lastTextComponent(components);
|
|
16
|
+
if (!lastText) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
for (let index = 0; index < count; index++) {
|
|
20
|
+
const copy = structuredClone(lastText);
|
|
21
|
+
copy.id = `scroll-filler-${index}`;
|
|
22
|
+
components.push(copy);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function setFooterBackground(paywall, hex) {
|
|
26
|
+
footerStack(paywall).background_color = {
|
|
27
|
+
light: { type: "hex", value: hex },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Translucent sticky footer over long scrollable content. */
|
|
31
|
+
export const paywallWithTransparentFooter = (() => {
|
|
32
|
+
const paywall = structuredClone(paywallWithFooter);
|
|
33
|
+
appendScrollableContent(paywall, 25);
|
|
34
|
+
setFooterBackground(paywall, "#057C5BAA");
|
|
35
|
+
return paywall;
|
|
36
|
+
})();
|
|
37
|
+
/** Small body that must center in the space above the footer, not the whole screen. */
|
|
38
|
+
export const paywallWithCenteredBodyAndFooter = (() => {
|
|
39
|
+
const paywall = structuredClone(paywallWithFooter);
|
|
40
|
+
const stack = paywall.components_config.base.stack;
|
|
41
|
+
const lastText = lastTextComponent(stack.components);
|
|
42
|
+
stack.components = lastText ? [lastText] : [];
|
|
43
|
+
stack.dimension.distribution = "center";
|
|
44
|
+
setFooterBackground(paywall, "#057C5BAA");
|
|
45
|
+
return paywall;
|
|
46
|
+
})();
|
|
47
|
+
/** Header and sticky footer together: the footer overlay must not affect the header. */
|
|
48
|
+
export const paywallWithHeaderAndFooter = (() => {
|
|
49
|
+
const paywall = structuredClone(paywallWithFooter);
|
|
50
|
+
appendScrollableContent(paywall, 15);
|
|
51
|
+
const headerDonor = structuredClone(paywallWithHeader);
|
|
52
|
+
paywall.components_config.base.header =
|
|
53
|
+
headerDonor.components_config.base.header;
|
|
54
|
+
paywall.components_localizations.en_US = {
|
|
55
|
+
...paywall.components_localizations.en_US,
|
|
56
|
+
...headerDonor.components_localizations.en_US,
|
|
57
|
+
};
|
|
58
|
+
return paywall;
|
|
59
|
+
})();
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import WebView from "./WebView.svelte";
|
|
3
|
+
import { componentDecorator } from "../../stories/component-decorator";
|
|
4
|
+
import { defineMeta } from "@storybook/addon-svelte-csf";
|
|
5
|
+
|
|
6
|
+
const { Story } = defineMeta({
|
|
7
|
+
title: "Components/WebView",
|
|
8
|
+
component: WebView,
|
|
9
|
+
decorators: [componentDecorator()],
|
|
10
|
+
parameters: {
|
|
11
|
+
// The iframe loads a real external URL, so screenshots are not stable.
|
|
12
|
+
chromatic: { disableSnapshot: true },
|
|
13
|
+
},
|
|
14
|
+
args: {
|
|
15
|
+
type: "web_view",
|
|
16
|
+
id: "web-view",
|
|
17
|
+
name: "Web View",
|
|
18
|
+
protocol_version: 1,
|
|
19
|
+
// Live RC bundle on a content-hash subdomain (Paywalls team). If this 404s,
|
|
20
|
+
// re-point at a current upload or replace with a committed static fixture.
|
|
21
|
+
url: "https://zoro7mkmsiy3biuznkoabsdaabga3v6kmn3753jlt5dgszc55woa.components.revenuecat-static.com/index.html",
|
|
22
|
+
size: {
|
|
23
|
+
width: { type: "fill" },
|
|
24
|
+
height: { type: "fixed", value: 400 },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<Story name="Default" />
|
|
31
|
+
|
|
32
|
+
<Story
|
|
33
|
+
name="Fill (full bleed)"
|
|
34
|
+
args={{
|
|
35
|
+
size: {
|
|
36
|
+
width: { type: "fill" },
|
|
37
|
+
height: { type: "fill" },
|
|
38
|
+
},
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
<Story
|
|
43
|
+
name="Fixed Size"
|
|
44
|
+
args={{
|
|
45
|
+
size: {
|
|
46
|
+
width: { type: "fixed", value: 320 },
|
|
47
|
+
height: { type: "fixed", value: 480 },
|
|
48
|
+
},
|
|
49
|
+
}}
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
<Story
|
|
53
|
+
name="Relative Size (50% width)"
|
|
54
|
+
args={{
|
|
55
|
+
size: {
|
|
56
|
+
width: { type: "relative", value: 0.5 },
|
|
57
|
+
height: { type: "fixed", value: 400 },
|
|
58
|
+
},
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
|
|
62
|
+
<Story
|
|
63
|
+
name="Fit Height (placeholder min-height)"
|
|
64
|
+
args={{
|
|
65
|
+
size: {
|
|
66
|
+
width: { type: "fill" },
|
|
67
|
+
height: { type: "fit" },
|
|
68
|
+
},
|
|
69
|
+
}}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
<Story
|
|
73
|
+
name="Fit Width (placeholder min-width)"
|
|
74
|
+
args={{
|
|
75
|
+
size: {
|
|
76
|
+
width: { type: "fit" },
|
|
77
|
+
height: { type: "fixed", value: 400 },
|
|
78
|
+
},
|
|
79
|
+
}}
|
|
80
|
+
/>
|
|
81
|
+
|
|
82
|
+
<!--
|
|
83
|
+
An https URL pointing at a site that disallows framing (X-Frame-Options /
|
|
84
|
+
CSP frame-ancestors) renders a blank frame: the browser refuses to display it
|
|
85
|
+
and, being cross-origin, the failure does not fire `onerror`. This illustrates
|
|
86
|
+
the render-only limitation - reliable load-failure detection needs the
|
|
87
|
+
protocol_version postMessage handshake.
|
|
88
|
+
-->
|
|
89
|
+
<Story
|
|
90
|
+
name="Framing blocked (X-Frame-Options)"
|
|
91
|
+
args={{
|
|
92
|
+
url: "https://www.google.com",
|
|
93
|
+
}}
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<!--
|
|
97
|
+
A rejected URL renders nothing (the empty canvas is expected). Any non-https
|
|
98
|
+
scheme is rejected by the https-only guard, not just http.
|
|
99
|
+
-->
|
|
100
|
+
<Story
|
|
101
|
+
name="Blocked URL (renders nothing)"
|
|
102
|
+
args={{
|
|
103
|
+
url: "http://insecure.example.com",
|
|
104
|
+
}}
|
|
105
|
+
/>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import WebView from "./WebView.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 WebView: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
16
|
+
[evt: string]: CustomEvent<any>;
|
|
17
|
+
}, {}, {}, string>;
|
|
18
|
+
type WebView = InstanceType<typeof WebView>;
|
|
19
|
+
export default WebView;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from "svelte";
|
|
3
|
+
import { getOptionalPackageInfoContext } from "../../stores/packageInfo";
|
|
4
|
+
import { getPaywallContext } from "../../stores/paywall";
|
|
5
|
+
import { getOptionalVariablesContext } from "../../stores/variables";
|
|
6
|
+
import type { WebViewProps } from "../../types/components/web-view";
|
|
7
|
+
import { css, mapSize, px } from "../../utils/base-utils";
|
|
8
|
+
import { evaluateVisibilityConditions } from "../../utils/style-utils";
|
|
9
|
+
import { isSameOriginAsHost, sanitizeWebViewUrl } from "./web-view-utils";
|
|
10
|
+
import {
|
|
11
|
+
FIT_MESSAGE,
|
|
12
|
+
loadCreateHostBridge,
|
|
13
|
+
RESIZE_MESSAGE,
|
|
14
|
+
} from "./web-view-sdk";
|
|
15
|
+
import type { ResizePayload, WebViewHostBridge } from "./web-view-sdk";
|
|
16
|
+
|
|
17
|
+
// Placeholder for a `fit` axis until the content reports its size — a cross-origin
|
|
18
|
+
// iframe exposes no intrinsic size, so `fit` would otherwise collapse to nothing.
|
|
19
|
+
const FIT_FALLBACK_SIZE_PX = 300;
|
|
20
|
+
|
|
21
|
+
// Cap a content-reported dimension so a buggy/hostile bundle can't blow up layout.
|
|
22
|
+
const MAX_FIT_SIZE_PX = 10_000;
|
|
23
|
+
|
|
24
|
+
const clampFitSize = (value: number): number =>
|
|
25
|
+
Math.max(1, Math.min(MAX_FIT_SIZE_PX, Math.round(value)));
|
|
26
|
+
|
|
27
|
+
const isPositiveFinite = (value: unknown): value is number =>
|
|
28
|
+
typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
29
|
+
|
|
30
|
+
const props: WebViewProps = $props();
|
|
31
|
+
|
|
32
|
+
const { selectedPackageId } = getPaywallContext();
|
|
33
|
+
const packageInfo = getOptionalPackageInfoContext();
|
|
34
|
+
const variables = getOptionalVariablesContext();
|
|
35
|
+
|
|
36
|
+
// Gate the origin guard on a post-mount flag (not `typeof window`) so SSR and the
|
|
37
|
+
// first client render match; otherwise a cross-origin URL mismatches on hydration.
|
|
38
|
+
let mounted = $state(false);
|
|
39
|
+
onMount(() => {
|
|
40
|
+
mounted = true;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const safeUrl = $derived(sanitizeWebViewUrl(props.url));
|
|
44
|
+
|
|
45
|
+
// Only grant `allow-same-origin` once we confirm the bundle is cross-origin: on a
|
|
46
|
+
// same-origin frame that combo could script the host. Best-effort on the initial
|
|
47
|
+
// URL only (post-redirect origins aren't observable); bundle URLs are restricted
|
|
48
|
+
// to the RC asset origin by the backend (khepri #21861).
|
|
49
|
+
const canEmbed = $derived(
|
|
50
|
+
mounted &&
|
|
51
|
+
safeUrl !== null &&
|
|
52
|
+
!isSameOriginAsHost(safeUrl, window.location.origin),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Content-reported sizes for `fit` axes (set by the resize handler below).
|
|
56
|
+
let fitWidth = $state<number | undefined>(undefined);
|
|
57
|
+
let fitHeight = $state<number | undefined>(undefined);
|
|
58
|
+
|
|
59
|
+
const fitAxisFallback = (axis: typeof props.size.width): number =>
|
|
60
|
+
axis.type === "fit" && typeof axis.default === "number"
|
|
61
|
+
? axis.default
|
|
62
|
+
: FIT_FALLBACK_SIZE_PX;
|
|
63
|
+
|
|
64
|
+
const style = $derived.by(() => {
|
|
65
|
+
const { width, height } = props.size;
|
|
66
|
+
const widthFit = width.type === "fit";
|
|
67
|
+
const heightFit = height.type === "fit";
|
|
68
|
+
// Pin a `fit` axis with an explicit size. `flex-shrink: 0` keeps flex parents
|
|
69
|
+
// from collapsing the box; avoid mirroring the size in `min-*` or it beats `max-*`.
|
|
70
|
+
// Prefer author-declared `default` when present; else the hard 300px fallback.
|
|
71
|
+
const fitWidthPx = widthFit
|
|
72
|
+
? clampFitSize(fitWidth ?? fitAxisFallback(width))
|
|
73
|
+
: null;
|
|
74
|
+
const fitHeightPx = heightFit
|
|
75
|
+
? clampFitSize(fitHeight ?? fitAxisFallback(height))
|
|
76
|
+
: null;
|
|
77
|
+
return css({
|
|
78
|
+
width: fitWidthPx != null ? px(fitWidthPx) : mapSize(width),
|
|
79
|
+
height: fitHeightPx != null ? px(fitHeightPx) : mapSize(height),
|
|
80
|
+
// Cap a fit axis to the parent. Do not mirror the reported size in `min-*`:
|
|
81
|
+
// that defeats `max-*` and clips content when the report exceeds the viewport.
|
|
82
|
+
...(fitWidthPx != null ? { "max-width": "100%" } : {}),
|
|
83
|
+
...(fitHeightPx != null ? { "max-height": "100%" } : {}),
|
|
84
|
+
"flex-shrink": width.type === "fixed" || widthFit || heightFit ? 0 : 1,
|
|
85
|
+
"line-height": 0,
|
|
86
|
+
// No transition: the iframe is height:100% of this box, so animating the
|
|
87
|
+
// resize would flash the iframe's own scrollbar mid-change.
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const isVisible = $derived(
|
|
92
|
+
evaluateVisibilityConditions(
|
|
93
|
+
{
|
|
94
|
+
selectedPackageId: $selectedPackageId,
|
|
95
|
+
packageInfo: $packageInfo,
|
|
96
|
+
variables: $variables ?? {},
|
|
97
|
+
},
|
|
98
|
+
undefined,
|
|
99
|
+
props.visible,
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Host<->content bridge (client-only): the host build is loaded from the CDN by
|
|
104
|
+
// protocol version and attached to the live iframe. The content reports its size
|
|
105
|
+
// (`resize`), driving `fit` axes; a CDN import failure degrades to the plain iframe.
|
|
106
|
+
let iframeEl = $state<HTMLIFrameElement | undefined>(undefined);
|
|
107
|
+
let previousSafeUrl: string | null | undefined;
|
|
108
|
+
|
|
109
|
+
$effect(() => {
|
|
110
|
+
const url = safeUrl;
|
|
111
|
+
if (url !== previousSafeUrl) {
|
|
112
|
+
fitWidth = undefined;
|
|
113
|
+
fitHeight = undefined;
|
|
114
|
+
previousSafeUrl = url;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const iframe = iframeEl;
|
|
118
|
+
// Read every prop the bridge is configured with synchronously: Svelte only
|
|
119
|
+
// tracks reads in the effect body, not inside the async `.then` below, so
|
|
120
|
+
// capturing them here re-runs the effect (rebuilding the bridge) when the
|
|
121
|
+
// component id or `fit` axes change in place.
|
|
122
|
+
const componentId = props.id;
|
|
123
|
+
const protocolVersion = props.protocol_version;
|
|
124
|
+
const widthFit = props.size.width.type === "fit";
|
|
125
|
+
const heightFit = props.size.height.type === "fit";
|
|
126
|
+
if (!iframe || !url) return;
|
|
127
|
+
|
|
128
|
+
let disposed = false;
|
|
129
|
+
let bridge: WebViewHostBridge | undefined;
|
|
130
|
+
|
|
131
|
+
loadCreateHostBridge(protocolVersion)
|
|
132
|
+
.then((createHostBridge) => {
|
|
133
|
+
if (disposed) return;
|
|
134
|
+
try {
|
|
135
|
+
bridge = createHostBridge({
|
|
136
|
+
iframe,
|
|
137
|
+
allowedOrigin: new URL(url).origin,
|
|
138
|
+
componentId,
|
|
139
|
+
protocolVersion,
|
|
140
|
+
});
|
|
141
|
+
// Tell the content which axes are `fit` so it hides its own scrollbar there
|
|
142
|
+
// (it would otherwise flash one while we catch up to a new size).
|
|
143
|
+
if (widthFit || heightFit) {
|
|
144
|
+
bridge.onReady(() =>
|
|
145
|
+
bridge?.send(FIT_MESSAGE, { width: widthFit, height: heightFit }),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
bridge.on(RESIZE_MESSAGE, (payload: unknown) => {
|
|
149
|
+
const { width, height } = (payload ?? {}) as ResizePayload;
|
|
150
|
+
if (widthFit && isPositiveFinite(width)) {
|
|
151
|
+
fitWidth = width;
|
|
152
|
+
}
|
|
153
|
+
if (heightFit && isPositiveFinite(height)) {
|
|
154
|
+
fitHeight = height;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.warn("web_view: failed to wire host bridge", error);
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
.catch(() => {
|
|
162
|
+
// Degrade to the plain iframe if the CDN import fails (e.g. CSP).
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return () => {
|
|
166
|
+
disposed = true;
|
|
167
|
+
bridge?.destroy();
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
</script>
|
|
171
|
+
|
|
172
|
+
{#if isVisible && safeUrl && canEmbed}
|
|
173
|
+
<div class="web-view" {style}>
|
|
174
|
+
<!--
|
|
175
|
+
`allow-scripts allow-same-origin` runs the bundle under its own origin (for
|
|
176
|
+
same-origin fetch/XHR); restricting it to that origin relies on the bundle's
|
|
177
|
+
CSP. `referrerpolicy`/`allow` and the omitted sandbox flags limit leakage and
|
|
178
|
+
block top navigation.
|
|
179
|
+
-->
|
|
180
|
+
<iframe
|
|
181
|
+
bind:this={iframeEl}
|
|
182
|
+
title={props.name ?? "Web view"}
|
|
183
|
+
src={safeUrl}
|
|
184
|
+
sandbox="allow-scripts allow-same-origin"
|
|
185
|
+
referrerpolicy="no-referrer"
|
|
186
|
+
allow=""
|
|
187
|
+
></iframe>
|
|
188
|
+
</div>
|
|
189
|
+
{/if}
|
|
190
|
+
|
|
191
|
+
<style>
|
|
192
|
+
.web-view {
|
|
193
|
+
position: relative;
|
|
194
|
+
overflow: hidden;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
iframe {
|
|
198
|
+
display: block;
|
|
199
|
+
width: 100%;
|
|
200
|
+
height: 100%;
|
|
201
|
+
border: 0;
|
|
202
|
+
}
|
|
203
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { setPackageInfoContext } from "../../stores/packageInfo";
|
|
3
|
+
import { setPaywallContext } from "../../stores/paywall";
|
|
4
|
+
import { setVariablesContext } from "../../stores/variables";
|
|
5
|
+
import type { WebViewProps } from "../../types/components/web-view";
|
|
6
|
+
import { readable, writable } from "svelte/store";
|
|
7
|
+
import WebView from "./WebView.svelte";
|
|
8
|
+
|
|
9
|
+
const props: WebViewProps = $props();
|
|
10
|
+
|
|
11
|
+
setPaywallContext({
|
|
12
|
+
defaultPackageId: undefined,
|
|
13
|
+
selectedPackageId: writable(undefined),
|
|
14
|
+
variablesPerPackage: readable(undefined),
|
|
15
|
+
baseVariables: readable(undefined),
|
|
16
|
+
infoPerPackage: readable(undefined),
|
|
17
|
+
onPurchase: () => {},
|
|
18
|
+
emitComponentInteraction: () => {},
|
|
19
|
+
onButtonAction: () => {},
|
|
20
|
+
uiConfig: {} as never,
|
|
21
|
+
hideBackButtons: false,
|
|
22
|
+
});
|
|
23
|
+
setPackageInfoContext(readable(undefined));
|
|
24
|
+
setVariablesContext(readable({}));
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<WebView {...props} />
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime loader for the RevenueCat web-components HOST bridge.
|
|
3
|
+
*
|
|
4
|
+
* The host bridge is deliberately NOT bundled into purchases-ui-js. It is
|
|
5
|
+
* dynamic-imported from the CDN by protocol major, so a `web_view` with
|
|
6
|
+
* `protocol_version: N` always gets the host build that matches the content build
|
|
7
|
+
* injected into its iframe. Both are served from a stable, rolling per-major path:
|
|
8
|
+
*
|
|
9
|
+
* https://sdk.revenuecat-static.com/v<major>/rc-host.js (ESM, exports createHostBridge)
|
|
10
|
+
*
|
|
11
|
+
* Shipping a new SDK patch just republishes that path, so neither this code nor any
|
|
12
|
+
* stored bundle HTML needs to change. Embedding apps must allow the SDK origin in
|
|
13
|
+
* their CSP `script-src` for the module import to succeed; if it fails, the caller
|
|
14
|
+
* keeps the plain (render-only) iframe.
|
|
15
|
+
*
|
|
16
|
+
* Message names, payload types, and the bridge/config shapes come from
|
|
17
|
+
* `@revenuecat/workflow-web-components-sdk` (the source of truth) rather than being
|
|
18
|
+
* mirrored here; only the CDN loader is local.
|
|
19
|
+
*
|
|
20
|
+
* Operational note: compile-time types come from the pinned npm package, while the
|
|
21
|
+
* runtime host build is loaded from the rolling per-major CDN path (`/v<major>/rc-host.js`).
|
|
22
|
+
* Dynamic `import()` cannot use SRI; a failed import is handled by the caller.
|
|
23
|
+
*/
|
|
24
|
+
import type { Bridge, HostBridgeConfig } from "@revenuecat/workflow-web-components-sdk";
|
|
25
|
+
export { FIT_MESSAGE, RESIZE_MESSAGE, } from "@revenuecat/workflow-web-components-sdk";
|
|
26
|
+
export type { ResizePayload } from "@revenuecat/workflow-web-components-sdk";
|
|
27
|
+
/** Base origin the versioned SDK builds are served from. */
|
|
28
|
+
export declare const SDK_BASE_URL = "https://sdk.revenuecat-static.com";
|
|
29
|
+
/**
|
|
30
|
+
* The subset of the SDK bridge purchases-ui-js uses: subscribe to a message, run
|
|
31
|
+
* a callback once the channel opens, fire-and-forget, and tear down.
|
|
32
|
+
*/
|
|
33
|
+
export type WebViewHostBridge = Pick<Bridge, "on" | "onReady" | "send" | "destroy">;
|
|
34
|
+
export type CreateHostBridge = (config: HostBridgeConfig) => Bridge;
|
|
35
|
+
/** Stable, rolling per-major URL of the host bridge ESM build. */
|
|
36
|
+
export declare function hostSdkUrl(protocolVersion: number): string;
|
|
37
|
+
type ModuleImporter = (url: string) => Promise<unknown>;
|
|
38
|
+
/**
|
|
39
|
+
* Resolve `createHostBridge` for a protocol major from the CDN. Rejects if the
|
|
40
|
+
* module can't be loaded or doesn't export the factory, so callers can degrade to
|
|
41
|
+
* a render-only iframe.
|
|
42
|
+
*/
|
|
43
|
+
export declare function loadCreateHostBridge(protocolVersion: number): Promise<CreateHostBridge>;
|
|
44
|
+
/**
|
|
45
|
+
* Override the module importer (and clear the cache). Intended for tests, which
|
|
46
|
+
* inject a fake so no real network/module resolution happens. Pass `null` to
|
|
47
|
+
* restore the default dynamic import.
|
|
48
|
+
*/
|
|
49
|
+
export declare function __setHostModuleImporterForTests(fn: ModuleImporter | null): void;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export { FIT_MESSAGE, RESIZE_MESSAGE, } from "@revenuecat/workflow-web-components-sdk";
|
|
2
|
+
/** Base origin the versioned SDK builds are served from. */
|
|
3
|
+
export const SDK_BASE_URL = "https://sdk.revenuecat-static.com";
|
|
4
|
+
/** Stable, rolling per-major URL of the host bridge ESM build. */
|
|
5
|
+
export function hostSdkUrl(protocolVersion) {
|
|
6
|
+
return `${SDK_BASE_URL}/v${protocolVersion}/rc-host.js`;
|
|
7
|
+
}
|
|
8
|
+
// The bare dynamic import. `@vite-ignore` / `webpackIgnore` keep bundlers from
|
|
9
|
+
// resolving the absolute CDN URL at build time; the browser fetches it at runtime.
|
|
10
|
+
const defaultImporter = (url) => import(/* @vite-ignore */ /* webpackIgnore: true */ url);
|
|
11
|
+
let importer = defaultImporter;
|
|
12
|
+
// Memoize per protocol major: the module is stable for the page's lifetime and
|
|
13
|
+
// multiple web views sharing a `protocol_version` should import it once.
|
|
14
|
+
const moduleCache = new Map();
|
|
15
|
+
function assertValidProtocolVersion(protocolVersion) {
|
|
16
|
+
if (!Number.isInteger(protocolVersion) || protocolVersion < 1) {
|
|
17
|
+
throw new Error(`invalid web_view protocol_version: ${protocolVersion}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function importHostModule(protocolVersion) {
|
|
21
|
+
const cached = moduleCache.get(protocolVersion);
|
|
22
|
+
if (cached)
|
|
23
|
+
return cached;
|
|
24
|
+
const url = hostSdkUrl(protocolVersion);
|
|
25
|
+
const promise = importer(url).then((module) => {
|
|
26
|
+
const createHostBridge = module
|
|
27
|
+
.createHostBridge;
|
|
28
|
+
if (typeof createHostBridge !== "function") {
|
|
29
|
+
throw new Error(`rc-host.js at ${url} does not export createHostBridge`);
|
|
30
|
+
}
|
|
31
|
+
return { createHostBridge };
|
|
32
|
+
});
|
|
33
|
+
// Don't cache a rejected import so a later mount can retry (e.g. after a
|
|
34
|
+
// transient network failure).
|
|
35
|
+
promise.catch(() => {
|
|
36
|
+
if (moduleCache.get(protocolVersion) === promise) {
|
|
37
|
+
moduleCache.delete(protocolVersion);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
moduleCache.set(protocolVersion, promise);
|
|
41
|
+
return promise;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Resolve `createHostBridge` for a protocol major from the CDN. Rejects if the
|
|
45
|
+
* module can't be loaded or doesn't export the factory, so callers can degrade to
|
|
46
|
+
* a render-only iframe.
|
|
47
|
+
*/
|
|
48
|
+
export async function loadCreateHostBridge(protocolVersion) {
|
|
49
|
+
assertValidProtocolVersion(protocolVersion);
|
|
50
|
+
const { createHostBridge } = await importHostModule(protocolVersion);
|
|
51
|
+
return createHostBridge;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Override the module importer (and clear the cache). Intended for tests, which
|
|
55
|
+
* inject a fake so no real network/module resolution happens. Pass `null` to
|
|
56
|
+
* restore the default dynamic import.
|
|
57
|
+
*/
|
|
58
|
+
export function __setHostModuleImporterForTests(fn) {
|
|
59
|
+
importer = fn ?? defaultImporter;
|
|
60
|
+
moduleCache.clear();
|
|
61
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a sanitized URL if it is a well-formed absolute `https:` URL,
|
|
3
|
+
* otherwise `null`.
|
|
4
|
+
*
|
|
5
|
+
* The web view embeds remote, third-party bundles, so only `https` is allowed:
|
|
6
|
+
* `http`, `data:`, `blob:`, `javascript:`, protocol-relative (`//host`) and
|
|
7
|
+
* relative URLs are all rejected, as are URLs carrying embedded credentials
|
|
8
|
+
* (`https://user:pass@host`). Whitespace and control characters (code points
|
|
9
|
+
* <= 0x20) are stripped before validation so obfuscated payloads such as
|
|
10
|
+
* "https\t://" or "jAvAsCrIpT:" cannot slip through. The normalized, parsed
|
|
11
|
+
* `href` is returned (not the raw input) so no control characters reach the
|
|
12
|
+
* iframe `src`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function sanitizeWebViewUrl(url: string): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Returns whether `url` resolves to the same origin as `hostOrigin` (typically
|
|
17
|
+
* `window.location.origin`). Used as a guard before applying the
|
|
18
|
+
* `allow-scripts allow-same-origin` sandbox: that combination is only safe when
|
|
19
|
+
* the framed bundle is on a different origin than the host page, otherwise it
|
|
20
|
+
* could script and un-sandbox the host. Callers should pass an already
|
|
21
|
+
* sanitized URL; an unparseable URL conservatively reports `true` (treat as
|
|
22
|
+
* same-origin -> block).
|
|
23
|
+
*/
|
|
24
|
+
export declare function isSameOriginAsHost(url: string, hostOrigin: string): boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const SAFE_WEB_VIEW_PROTOCOL = "https:";
|
|
2
|
+
/**
|
|
3
|
+
* Returns a sanitized URL if it is a well-formed absolute `https:` URL,
|
|
4
|
+
* otherwise `null`.
|
|
5
|
+
*
|
|
6
|
+
* The web view embeds remote, third-party bundles, so only `https` is allowed:
|
|
7
|
+
* `http`, `data:`, `blob:`, `javascript:`, protocol-relative (`//host`) and
|
|
8
|
+
* relative URLs are all rejected, as are URLs carrying embedded credentials
|
|
9
|
+
* (`https://user:pass@host`). Whitespace and control characters (code points
|
|
10
|
+
* <= 0x20) are stripped before validation so obfuscated payloads such as
|
|
11
|
+
* "https\t://" or "jAvAsCrIpT:" cannot slip through. The normalized, parsed
|
|
12
|
+
* `href` is returned (not the raw input) so no control characters reach the
|
|
13
|
+
* iframe `src`.
|
|
14
|
+
*/
|
|
15
|
+
export function sanitizeWebViewUrl(url) {
|
|
16
|
+
const normalized = Array.from(url)
|
|
17
|
+
.filter((character) => character.charCodeAt(0) > 0x20)
|
|
18
|
+
.join("");
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = new URL(normalized);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (parsed.protocol.toLowerCase() !== SAFE_WEB_VIEW_PROTOCOL) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
// Reject embedded credentials: they have no place in an asset URL and can be
|
|
30
|
+
// used to obscure the real host (e.g. `https://trusted.com@evil.com`).
|
|
31
|
+
if (parsed.username !== "" || parsed.password !== "") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return parsed.href;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Returns whether `url` resolves to the same origin as `hostOrigin` (typically
|
|
38
|
+
* `window.location.origin`). Used as a guard before applying the
|
|
39
|
+
* `allow-scripts allow-same-origin` sandbox: that combination is only safe when
|
|
40
|
+
* the framed bundle is on a different origin than the host page, otherwise it
|
|
41
|
+
* could script and un-sandbox the host. Callers should pass an already
|
|
42
|
+
* sanitized URL; an unparseable URL conservatively reports `true` (treat as
|
|
43
|
+
* same-origin -> block).
|
|
44
|
+
*/
|
|
45
|
+
export function isSameOriginAsHost(url, hostOrigin) {
|
|
46
|
+
try {
|
|
47
|
+
return new URL(url).origin === hostOrigin;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { default as Timeline } from "./components/timeline/Timeline.svelte";
|
|
|
15
15
|
export { default as Screen } from "./components/workflows/Screen.svelte";
|
|
16
16
|
export { default as Workflow } from "./components/workflows/Workflow.svelte";
|
|
17
17
|
export { default as Video } from "./components/video/Video.svelte";
|
|
18
|
+
export { default as WebView } from "./components/web-view/WebView.svelte";
|
|
18
19
|
export * from "./types";
|
|
19
20
|
export { type PaywallData } from "./types/paywall";
|
|
20
21
|
export { type WorkflowNavData, type WorkflowStepChangeEvent, workflowDataToNavData, } from "./types/workflow-nav";
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export { default as Timeline } from "./components/timeline/Timeline.svelte";
|
|
|
16
16
|
export { default as Screen } from "./components/workflows/Screen.svelte";
|
|
17
17
|
export { default as Workflow } from "./components/workflows/Workflow.svelte";
|
|
18
18
|
export { default as Video } from "./components/video/Video.svelte";
|
|
19
|
+
export { default as WebView } from "./components/web-view/WebView.svelte";
|
|
19
20
|
export * from "./types";
|
|
20
21
|
export {} from "./types/paywall";
|
|
21
22
|
export { workflowDataToNavData, } from "./types/workflow-nav";
|
|
@@ -16,7 +16,8 @@ import type { TextNodeProps } from "./components/text";
|
|
|
16
16
|
import type { TimelineProps } from "./components/timeline";
|
|
17
17
|
import type { VideoProps } from "./components/video";
|
|
18
18
|
import type { WalletButtonProps } from "./components/wallet-button";
|
|
19
|
+
import type { WebViewProps } from "./components/web-view";
|
|
19
20
|
import type { SkeletonLoaderProps } from "./components/skeleton-loader-props";
|
|
20
21
|
import type { ExpressPurchaseButtonProps } from "./components/express-purchase-button-props";
|
|
21
22
|
import type { HeaderProps } from "./components/header";
|
|
22
|
-
export type Component = ButtonProps | CarouselProps | CountdownProps | ExpressPurchaseButtonProps | FallbackHeaderProps | FooterProps | HeaderProps | IconProps | ImageProps | InputMultipleChoiceProps | InputOptionProps | InputSingleChoiceProps | InputTextProps | PackageProps | PurchaseButtonProps | RedemptionButtonProps | SkeletonLoaderProps | StackProps | TabControlButtonProps | TabControlToggleProps | TabControlProps | TabProps | TabsProps | TextNodeProps | TimelineProps | WalletButtonProps | VideoProps;
|
|
23
|
+
export type Component = ButtonProps | CarouselProps | CountdownProps | ExpressPurchaseButtonProps | FallbackHeaderProps | FooterProps | HeaderProps | IconProps | ImageProps | InputMultipleChoiceProps | InputOptionProps | InputSingleChoiceProps | InputTextProps | PackageProps | PurchaseButtonProps | RedemptionButtonProps | SkeletonLoaderProps | StackProps | TabControlButtonProps | TabControlToggleProps | TabControlProps | TabProps | TabsProps | TextNodeProps | TimelineProps | WalletButtonProps | VideoProps | WebViewProps;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SizeType } from "..";
|
|
2
|
+
import type { BaseComponent } from "../base";
|
|
3
|
+
export interface WebViewProps extends BaseComponent {
|
|
4
|
+
type: "web_view";
|
|
5
|
+
visible?: boolean | null;
|
|
6
|
+
/**
|
|
7
|
+
* Version of the host <-> web-component postMessage bridge protocol. Only `1`
|
|
8
|
+
* exists today. Selects the CDN host-bridge build loaded at runtime (see
|
|
9
|
+
* `web-view-sdk.ts`).
|
|
10
|
+
*/
|
|
11
|
+
protocol_version: 1;
|
|
12
|
+
/** External, `https`-only asset URL the iframe is pointed at. */
|
|
13
|
+
url: string;
|
|
14
|
+
size: SizeType;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types.d.ts
CHANGED
|
@@ -48,11 +48,17 @@ type RelativeSize = {
|
|
|
48
48
|
type: "relative";
|
|
49
49
|
value: number;
|
|
50
50
|
};
|
|
51
|
-
type
|
|
52
|
-
type: "fit"
|
|
51
|
+
type FitSize = {
|
|
52
|
+
type: "fit";
|
|
53
53
|
value?: null;
|
|
54
|
+
/** Placeholder px until the content reports its measured size. */
|
|
55
|
+
default?: number;
|
|
54
56
|
};
|
|
55
|
-
|
|
57
|
+
type FillSize = {
|
|
58
|
+
type: "fill";
|
|
59
|
+
value?: null;
|
|
60
|
+
};
|
|
61
|
+
export type Size = FixedSize | RelativeSize | FitSize | FillSize;
|
|
56
62
|
export type SizeType = {
|
|
57
63
|
width: Size;
|
|
58
64
|
height: Size;
|
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.
|
|
5
|
+
"version": "4.8.7",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "RevenueCat, Inc."
|
|
8
8
|
},
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
},
|
|
77
77
|
"packageManager": "npm@11.7.0+sha512.c22099a6fff8d5b2286c2a09df5352b4858a7c0c716320f58989d60ad8b29ecf2ce6fdfe97ccb41c23ffb1272e1fa079f868487dd6b81d02a2a9e199c095a117",
|
|
78
78
|
"dependencies": {
|
|
79
|
+
"@revenuecat/workflow-web-components-sdk": "^0.1.4",
|
|
79
80
|
"qrcode": "^1.5.4"
|
|
80
81
|
},
|
|
81
82
|
"peerDependencies": {
|