@revenuecat/purchases-ui-js 4.8.5 → 4.8.6

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.
@@ -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 }>
@@ -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,197 @@
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 style = $derived.by(() => {
60
+ const { width, height } = props.size;
61
+ const widthFit = width.type === "fit";
62
+ const heightFit = height.type === "fit";
63
+ // Pin a `fit` axis with an explicit size. `flex-shrink: 0` keeps flex parents
64
+ // from collapsing the box; avoid mirroring the size in `min-*` or it beats `max-*`.
65
+ const fitWidthPx = widthFit
66
+ ? clampFitSize(fitWidth ?? FIT_FALLBACK_SIZE_PX)
67
+ : null;
68
+ const fitHeightPx = heightFit
69
+ ? clampFitSize(fitHeight ?? FIT_FALLBACK_SIZE_PX)
70
+ : null;
71
+ return css({
72
+ width: fitWidthPx != null ? px(fitWidthPx) : mapSize(width),
73
+ height: fitHeightPx != null ? px(fitHeightPx) : mapSize(height),
74
+ // Cap a fit axis to the parent. Do not mirror the reported size in `min-*`:
75
+ // that defeats `max-*` and clips content when the report exceeds the viewport.
76
+ ...(fitWidthPx != null ? { "max-width": "100%" } : {}),
77
+ ...(fitHeightPx != null ? { "max-height": "100%" } : {}),
78
+ "flex-shrink": width.type === "fixed" || widthFit || heightFit ? 0 : 1,
79
+ "line-height": 0,
80
+ // No transition: the iframe is height:100% of this box, so animating the
81
+ // resize would flash the iframe's own scrollbar mid-change.
82
+ });
83
+ });
84
+
85
+ const isVisible = $derived(
86
+ evaluateVisibilityConditions(
87
+ {
88
+ selectedPackageId: $selectedPackageId,
89
+ packageInfo: $packageInfo,
90
+ variables: $variables ?? {},
91
+ },
92
+ undefined,
93
+ props.visible,
94
+ ),
95
+ );
96
+
97
+ // Host<->content bridge (client-only): the host build is loaded from the CDN by
98
+ // protocol version and attached to the live iframe. The content reports its size
99
+ // (`resize`), driving `fit` axes; a CDN import failure degrades to the plain iframe.
100
+ let iframeEl = $state<HTMLIFrameElement | undefined>(undefined);
101
+ let previousSafeUrl: string | null | undefined;
102
+
103
+ $effect(() => {
104
+ const url = safeUrl;
105
+ if (url !== previousSafeUrl) {
106
+ fitWidth = undefined;
107
+ fitHeight = undefined;
108
+ previousSafeUrl = url;
109
+ }
110
+
111
+ const iframe = iframeEl;
112
+ // Read every prop the bridge is configured with synchronously: Svelte only
113
+ // tracks reads in the effect body, not inside the async `.then` below, so
114
+ // capturing them here re-runs the effect (rebuilding the bridge) when the
115
+ // component id or `fit` axes change in place.
116
+ const componentId = props.id;
117
+ const protocolVersion = props.protocol_version;
118
+ const widthFit = props.size.width.type === "fit";
119
+ const heightFit = props.size.height.type === "fit";
120
+ if (!iframe || !url) return;
121
+
122
+ let disposed = false;
123
+ let bridge: WebViewHostBridge | undefined;
124
+
125
+ loadCreateHostBridge(protocolVersion)
126
+ .then((createHostBridge) => {
127
+ if (disposed) return;
128
+ try {
129
+ bridge = createHostBridge({
130
+ iframe,
131
+ allowedOrigin: new URL(url).origin,
132
+ componentId,
133
+ protocolVersion,
134
+ });
135
+ // Tell the content which axes are `fit` so it hides its own scrollbar there
136
+ // (it would otherwise flash one while we catch up to a new size).
137
+ if (widthFit || heightFit) {
138
+ bridge.onReady(() =>
139
+ bridge?.send(FIT_MESSAGE, { width: widthFit, height: heightFit }),
140
+ );
141
+ }
142
+ bridge.on(RESIZE_MESSAGE, (payload: unknown) => {
143
+ const { width, height } = (payload ?? {}) as ResizePayload;
144
+ if (widthFit && isPositiveFinite(width)) {
145
+ fitWidth = width;
146
+ }
147
+ if (heightFit && isPositiveFinite(height)) {
148
+ fitHeight = height;
149
+ }
150
+ });
151
+ } catch (error) {
152
+ console.warn("web_view: failed to wire host bridge", error);
153
+ }
154
+ })
155
+ .catch(() => {
156
+ // Degrade to the plain iframe if the CDN import fails (e.g. CSP).
157
+ });
158
+
159
+ return () => {
160
+ disposed = true;
161
+ bridge?.destroy();
162
+ };
163
+ });
164
+ </script>
165
+
166
+ {#if isVisible && safeUrl && canEmbed}
167
+ <div class="web-view" {style}>
168
+ <!--
169
+ `allow-scripts allow-same-origin` runs the bundle under its own origin (for
170
+ same-origin fetch/XHR); restricting it to that origin relies on the bundle's
171
+ CSP. `referrerpolicy`/`allow` and the omitted sandbox flags limit leakage and
172
+ block top navigation.
173
+ -->
174
+ <iframe
175
+ bind:this={iframeEl}
176
+ title={props.name ?? "Web view"}
177
+ src={safeUrl}
178
+ sandbox="allow-scripts allow-same-origin"
179
+ referrerpolicy="no-referrer"
180
+ allow=""
181
+ ></iframe>
182
+ </div>
183
+ {/if}
184
+
185
+ <style>
186
+ .web-view {
187
+ position: relative;
188
+ overflow: hidden;
189
+ }
190
+
191
+ iframe {
192
+ display: block;
193
+ width: 100%;
194
+ height: 100%;
195
+ border: 0;
196
+ }
197
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { WebViewProps } from "../../types/components/web-view";
2
+ declare const WebView: import("svelte").Component<WebViewProps, {}, "">;
3
+ type WebView = ReturnType<typeof WebView>;
4
+ export default WebView;
@@ -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,4 @@
1
+ import type { WebViewProps } from "../../types/components/web-view";
2
+ declare const WebViewTestWrapper: import("svelte").Component<WebViewProps, {}, "">;
3
+ type WebViewTestWrapper = ReturnType<typeof WebViewTestWrapper>;
4
+ export default WebViewTestWrapper;
@@ -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/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",
5
+ "version": "4.8.6",
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": {