@grizzlee/react 0.1.1 → 0.3.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/CHANGELOG.md +8 -0
- package/README.md +39 -4
- package/embed.cjs +28 -7
- package/embed.cjs.map +3 -3
- package/embed.js +28 -7
- package/embed.js.map +3 -3
- package/index.cjs +34 -7
- package/index.cjs.map +3 -3
- package/index.js +34 -7
- package/index.js.map +3 -3
- package/native.cjs +4715 -0
- package/native.cjs.map +7 -0
- package/native.js +4717 -0
- package/native.js.map +7 -0
- package/package.json +25 -3
- package/types/embed.d.cts +1 -1
- package/types/embed.d.ts +1 -1
- package/types/host.d.cts +5 -1
- package/types/host.d.ts +5 -1
- package/types/index.d.cts +1 -1
- package/types/index.d.ts +1 -1
- package/types/native.d.cts +18 -0
- package/types/native.d.ts +18 -0
- package/types/protocol.d.cts +7 -0
- package/types/protocol.d.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 0.3.0
|
|
2
|
+
|
|
3
|
+
- Add `GrizzleeNativeEmbed` at `@grizzlee/react/native`. It loads a server-minted start URL directly in a WebView, renews sessions through `onExpired` and opens React Native's share sheet unless `onShare` handles sharing. React Native and React Native WebView are optional peers. Web entry points do not import them.
|
|
4
|
+
|
|
5
|
+
## 0.2.0
|
|
6
|
+
|
|
7
|
+
- Add `onShare` to `GrizzleeEmbed` and `mountGrizzleeEmbed`, with the exported `GrizzleeShare` type. A callback opts in through a validated host message. It receives the program ID, referral URL and localized invitation when the partner taps Share. Hosts without a callback keep sharing inside the frame.
|
|
8
|
+
|
|
1
9
|
## 0.1.1
|
|
2
10
|
|
|
3
11
|
- Published from CI through npm trusted publishing. No code changes.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Grizzlee embed hosts
|
|
2
2
|
|
|
3
|
-
`@grizzlee/react` exports `GrizzleeEmbed`, `mountGrizzleeEmbed`, `GrizzleeEmbedProps` and `
|
|
3
|
+
`@grizzlee/react` exports `GrizzleeEmbed`, `mountGrizzleeEmbed`, `GrizzleeEmbedProps`, `GrizzleeEmbedOptions` and `GrizzleeShare`. React 18 or later is an optional peer dependency, so a host using only `@grizzlee/react/embed` does not need React. `@grizzlee/react/native` exports `GrizzleeNativeEmbed`, `GrizzleeNativeEmbedProps` and `GrizzleeShare`. React Native and React Native WebView are optional peers used only by the native entry point.
|
|
4
4
|
|
|
5
5
|
```tsx
|
|
6
6
|
import { GrizzleeEmbed } from "@grizzlee/react";
|
|
@@ -16,10 +16,21 @@ async function newSession() {
|
|
|
16
16
|
<GrizzleeEmbed url={startUrl} onSessionExpired={newSession} />;
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
`url` is an absolute HTTP or HTTPS URL minted by your server. Optional props are `onSessionExpired`, `onReady`, `title`, `className`, `style` and `minHeight`. `title` defaults to `"Partner dashboard"`. `minHeight` defaults to 320 CSS pixels and stays between 120 and 10,000. Resize messages must contain a positive safe integer. The iframe height stays between the minimum and 10,000 pixels. These bounds override `style.height` and `style.minHeight`.
|
|
19
|
+
`url` is an absolute HTTP or HTTPS URL minted by your server. Optional props are `onSessionExpired`, `onReady`, `onShare`, `title`, `className`, `style` and `minHeight`. `title` defaults to `"Partner dashboard"`. `minHeight` defaults to 320 CSS pixels and stays between 120 and 10,000. Resize messages must contain a positive safe integer. The iframe height stays between the minimum and 10,000 pixels. These bounds override `style.height` and `style.minHeight`.
|
|
20
20
|
|
|
21
21
|
`onReady` runs when the partner summary loads, including after renewal. `onSessionExpired` can return a new URL directly or through a promise, or return nothing if you handle recovery yourself. Catch request errors in that callback to display them in your app. A rejected promise leaves the expired dashboard visible. Only one renewal runs at a time. Changing `url` loads a new session, and unmounting removes the listener and ignores pending renewal results.
|
|
22
22
|
|
|
23
|
+
`onShare` receives `{ programId, url, text }` when the partner taps **Share**. Supplying it opts in through a `grizzlee:configure` message after the frame reports ready. `text` is an invitation in the session's language. The listener validates field types and accepts only HTTP or HTTPS URLs without credentials. Without a callback, sharing stays in the frame. React updates the opt-in when the callback changes. Both hosts configure each renewed session.
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
<GrizzleeEmbed
|
|
27
|
+
url={startUrl}
|
|
28
|
+
onShare={({ url, text }) => openShareSheet({ url, text })}
|
|
29
|
+
/>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`openShareSheet` is your app's callback. `mountGrizzleeEmbed` accepts the same `onShare` option. For React Native or Expo, use `GrizzleeNativeEmbed` below.
|
|
33
|
+
|
|
23
34
|
The component renders during server-side rendering without accessing `window` or `document`. It attaches its message listener after mounting. In Next.js App Router, render it from a client component when passing callbacks.
|
|
24
35
|
|
|
25
36
|
For a host without React:
|
|
@@ -38,7 +49,31 @@ dashboard.destroy();
|
|
|
38
49
|
|
|
39
50
|
The helper accepts the same options except `style`. It appends an iframe and returns `{ destroy() }`. Both hosts use `allow="clipboard-write"`, `referrerPolicy="no-referrer"` and `loading="lazy"`. They accept messages only from the current URL's origin and their own iframe window. Neither sets `sandbox`. If you add it to a plain iframe, include `allow-scripts allow-same-origin allow-downloads`. Without `allow-same-origin`, the exchange sends `Origin: null` and fails. Without `allow-downloads`, QR downloads fail.
|
|
40
51
|
|
|
41
|
-
|
|
52
|
+
## React Native
|
|
53
|
+
|
|
54
|
+
Install `react-native-webview` in your React Native app, then import the native subpath. It includes a URL parser for React Native releases whose built-in `URL` cannot read origins. No global URL polyfill setup is needed.
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
import { GrizzleeNativeEmbed } from "@grizzlee/react/native";
|
|
58
|
+
|
|
59
|
+
// Fetch the initial startUrl from your authenticated merchant API before rendering.
|
|
60
|
+
<GrizzleeNativeEmbed
|
|
61
|
+
startUrl={startUrl}
|
|
62
|
+
onExpired={fetchNewStartUrl}
|
|
63
|
+
onError={showError}
|
|
64
|
+
style={{ flex: 1 }}
|
|
65
|
+
/>;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`fetchNewStartUrl` calls your merchant API and returns a fresh session URL. The merchant server authenticates the app user and creates the session with `createEmbedSession`. Keep an allowed merchant origin in workspace settings and use it as `embedOrigin`; no page needs to exist at that origin. A native endpoint should authenticate the app's access token instead of requiring a browser `Origin` header. Never put the Grizzlee server key in the app.
|
|
69
|
+
|
|
70
|
+
`startUrl` must be an absolute HTTP or HTTPS `/embed/start` URL with a session ID and no credentials. Invalid initial URLs throw during rendering. The component loads the dashboard as the WebView's top document, with no iframe or merchant wrapper page. It accepts messages only from the current Grizzlee origin, path and session query, and blocks navigation to other documents. The native parent sets the WebView's size; dashboard resize messages do not change it. Put it in a parent with a height or `flex: 1`.
|
|
71
|
+
|
|
72
|
+
`onExpired` can return a new URL or a promise, or return nothing if your app updates `startUrl` itself. Only one renewal runs at a time. Changing `startUrl` opens a new session; pending results from the old session or an unmounted component are ignored. A rejected renewal leaves the expired dashboard visible and calls `onError`. The component also requests a fresh session if the WebView's rendering process ends, since the bearer lived in that process's memory.
|
|
73
|
+
|
|
74
|
+
`onReady` runs after each session's summary loads. **Share** opens React Native's share sheet by default, with the invitation and referral URL in the message. Optional `onShare` receives `{ programId, url, text }` and takes over sharing. It can return a promise. Both hosts use the same share field and URL validation. `onError` receives an `Error` for document loading, renewal or sharing failures. Display errors and any retry controls in your app.
|
|
75
|
+
|
|
76
|
+
Create sessions on your backend with [@grizzlee/sdk](https://www.npmjs.com/package/@grizzlee/sdk) and pass the returned URL to the browser or native host. Hosts never need the server key or schema dependencies. See [the embed guide](../../docs/embed.md#mobile-webviews) for an authenticated app example and other native WebViews.
|
|
42
77
|
|
|
43
78
|
## Install
|
|
44
79
|
|
|
@@ -48,7 +83,7 @@ pnpm add @grizzlee/react
|
|
|
48
83
|
yarn add @grizzlee/react
|
|
49
84
|
```
|
|
50
85
|
|
|
51
|
-
Requires Node >= 22.
|
|
86
|
+
Requires Node >= 22. All three entry points support ESM and CommonJS with source maps and matching TypeScript declarations. The native entry point needs React Native >= 0.78 and React Native WebView >= 13, with the React version required by your React Native release. The web entry points do not import either native peer.
|
|
52
87
|
|
|
53
88
|
## Versioning
|
|
54
89
|
|
package/embed.cjs
CHANGED
|
@@ -24,19 +24,31 @@ __export(embed_exports, {
|
|
|
24
24
|
});
|
|
25
25
|
module.exports = __toCommonJS(embed_exports);
|
|
26
26
|
|
|
27
|
-
// packages/react/src/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return Number.isFinite(height) ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height))) : 320;
|
|
31
|
-
}
|
|
32
|
-
function embedUrl(url) {
|
|
33
|
-
const parsed = new URL(url);
|
|
27
|
+
// packages/react/src/protocol.ts
|
|
28
|
+
function embedUrl(url, Url = URL) {
|
|
29
|
+
const parsed = new Url(url);
|
|
34
30
|
if (!["https:", "http:"].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
35
31
|
throw new Error(
|
|
36
32
|
"Use an absolute HTTP or HTTPS embed URL without credentials"
|
|
37
33
|
);
|
|
38
34
|
return parsed;
|
|
39
35
|
}
|
|
36
|
+
function parseShare(message, validateUrl = embedUrl) {
|
|
37
|
+
if (!("programId" in message) || typeof message.programId !== "string" || !message.programId || !("url" in message) || typeof message.url !== "string" || !("text" in message) || typeof message.text !== "string")
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
validateUrl(message.url);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return { programId: message.programId, url: message.url, text: message.text };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// packages/react/src/host.ts
|
|
48
|
+
var MAX_HEIGHT = 1e4;
|
|
49
|
+
function minimumHeight(height = 320) {
|
|
50
|
+
return Number.isFinite(height) ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height))) : 320;
|
|
51
|
+
}
|
|
40
52
|
function listenToEmbed(frame, options) {
|
|
41
53
|
const hostWindow = frame.ownerDocument.defaultView;
|
|
42
54
|
if (!hostWindow)
|
|
@@ -51,8 +63,17 @@ function listenToEmbed(frame, options) {
|
|
|
51
63
|
if (!message || typeof message !== "object" || !("type" in message)) return;
|
|
52
64
|
switch (message.type) {
|
|
53
65
|
case "grizzlee:ready":
|
|
66
|
+
frame.contentWindow.postMessage(
|
|
67
|
+
{ type: "grizzlee:configure", share: !!options().onShare },
|
|
68
|
+
origin
|
|
69
|
+
);
|
|
54
70
|
options().onReady?.();
|
|
55
71
|
break;
|
|
72
|
+
case "grizzlee:share": {
|
|
73
|
+
const share = parseShare(message);
|
|
74
|
+
if (share) options().onShare?.(share);
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
56
77
|
case "grizzlee:resize": {
|
|
57
78
|
if ("height" in message && typeof message.height === "number" && Number.isSafeInteger(message.height) && message.height > 0) {
|
|
58
79
|
const height = Math.min(
|
package/embed.cjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/embed.ts", "../src/host.ts"],
|
|
4
|
-
"sourcesContent": ["import {\n embedUrl,\n listenToEmbed,\n minimumHeight,\n type GrizzleeEmbedOptions,\n} from \"./host\";\n\nexport type { GrizzleeEmbedOptions } from \"./host\";\n\n/** Append a dashboard iframe. Destroy removes its listener and the iframe. */\nexport function mountGrizzleeEmbed(\n container: HTMLElement,\n options: GrizzleeEmbedOptions,\n): { destroy(): void } {\n const url = embedUrl(options.url);\n const frame = container.ownerDocument.createElement(\"iframe\");\n frame.title = options.title ?? \"Partner dashboard\";\n if (options.className) frame.className = options.className;\n frame.allow = \"clipboard-write\";\n frame.referrerPolicy = \"no-referrer\";\n frame.loading = \"lazy\";\n frame.style.width = \"100%\";\n frame.style.border = \"0\";\n frame.style.minHeight = `${minimumHeight(options.minHeight)}px`;\n frame.style.height = frame.style.minHeight;\n const stop = listenToEmbed(frame, () => options);\n frame.src = url.href;\n container.appendChild(frame);\n return {\n destroy() {\n stop();\n frame.remove();\n },\n };\n}\n", "export interface GrizzleeEmbedOptions {\n /** An absolute HTTP or HTTPS start URL minted by the merchant's server. */\n url: string;\n /** Return a newly minted URL to reopen the dashboard. Handle errors here. */\n onSessionExpired?: () => string | void | Promise<string | void>;\n onReady?: () => void;\n title?: string;\n className?: string;\n /** Minimum height in CSS pixels, clamped to 120 through 10,000. Defaults to 320. */\n minHeight?: number;\n}\n\nconst MAX_HEIGHT = 10_000;\n\nexport function minimumHeight(height = 320): number {\n return Number.isFinite(height)\n ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height)))\n : 320;\n}\n\
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;
|
|
3
|
+
"sources": ["../src/embed.ts", "../src/protocol.ts", "../src/host.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n embedUrl,\n listenToEmbed,\n minimumHeight,\n type GrizzleeEmbedOptions,\n} from \"./host\";\n\nexport type { GrizzleeEmbedOptions, GrizzleeShare } from \"./host\";\n\n/** Append a dashboard iframe. Destroy removes its listener and the iframe. */\nexport function mountGrizzleeEmbed(\n container: HTMLElement,\n options: GrizzleeEmbedOptions,\n): { destroy(): void } {\n const url = embedUrl(options.url);\n const frame = container.ownerDocument.createElement(\"iframe\");\n frame.title = options.title ?? \"Partner dashboard\";\n if (options.className) frame.className = options.className;\n frame.allow = \"clipboard-write\";\n frame.referrerPolicy = \"no-referrer\";\n frame.loading = \"lazy\";\n frame.style.width = \"100%\";\n frame.style.border = \"0\";\n frame.style.minHeight = `${minimumHeight(options.minHeight)}px`;\n frame.style.height = frame.style.minHeight;\n const stop = listenToEmbed(frame, () => options);\n frame.src = url.href;\n container.appendChild(frame);\n return {\n destroy() {\n stop();\n frame.remove();\n },\n };\n}\n", "export interface GrizzleeShare {\n programId: string;\n url: string;\n text: string;\n}\n\nexport function embedUrl(\n url: string,\n Url: new (value: string) => URL = URL,\n): URL {\n const parsed = new Url(url);\n if (\n ![\"https:\", \"http:\"].includes(parsed.protocol) ||\n parsed.username ||\n parsed.password\n )\n throw new Error(\n \"Use an absolute HTTP or HTTPS embed URL without credentials\",\n );\n return parsed;\n}\n\nexport function parseShare(\n message: object,\n validateUrl: (value: string) => unknown = embedUrl,\n): GrizzleeShare | null {\n if (\n !(\"programId\" in message) ||\n typeof message.programId !== \"string\" ||\n !message.programId ||\n !(\"url\" in message) ||\n typeof message.url !== \"string\" ||\n !(\"text\" in message) ||\n typeof message.text !== \"string\"\n )\n return null;\n try {\n validateUrl(message.url);\n } catch {\n return null;\n }\n return { programId: message.programId, url: message.url, text: message.text };\n}\n", "import { embedUrl, parseShare, type GrizzleeShare } from \"./protocol\";\n\nexport { embedUrl } from \"./protocol\";\nexport type { GrizzleeShare } from \"./protocol\";\n\nexport interface GrizzleeEmbedOptions {\n /** An absolute HTTP or HTTPS start URL minted by the merchant's server. */\n url: string;\n /** Return a newly minted URL to reopen the dashboard. Handle errors here. */\n onSessionExpired?: () => string | void | Promise<string | void>;\n onReady?: () => void;\n /** Supplying this callback asks the frame to send share actions to the host. */\n onShare?: (share: GrizzleeShare) => void;\n title?: string;\n className?: string;\n /** Minimum height in CSS pixels, clamped to 120 through 10,000. Defaults to 320. */\n minHeight?: number;\n}\n\nconst MAX_HEIGHT = 10_000;\n\nexport function minimumHeight(height = 320): number {\n return Number.isFinite(height)\n ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height)))\n : 320;\n}\n\n/** Both hosts use this listener so origin checks and renewal behave the same way. */\nexport function listenToEmbed(\n frame: HTMLIFrameElement,\n options: () => GrizzleeEmbedOptions,\n): () => void {\n const hostWindow = frame.ownerDocument.defaultView;\n if (!hostWindow)\n throw new Error(\"The iframe must belong to a browser document\");\n let origin = embedUrl(options().url).origin;\n let active = true;\n let renewing = false;\n\n const receive = (event: MessageEvent<unknown>) => {\n if (\n !active ||\n event.origin !== origin ||\n !frame.contentWindow ||\n event.source !== frame.contentWindow\n )\n return;\n const message = event.data;\n if (!message || typeof message !== \"object\" || !(\"type\" in message)) return;\n\n switch (message.type) {\n case \"grizzlee:ready\":\n frame.contentWindow.postMessage(\n { type: \"grizzlee:configure\", share: !!options().onShare },\n origin,\n );\n options().onReady?.();\n break;\n case \"grizzlee:share\": {\n const share = parseShare(message);\n if (share) options().onShare?.(share);\n break;\n }\n case \"grizzlee:resize\": {\n if (\n \"height\" in message &&\n typeof message.height === \"number\" &&\n Number.isSafeInteger(message.height) &&\n message.height > 0\n ) {\n const height = Math.min(\n MAX_HEIGHT,\n Math.max(minimumHeight(options().minHeight), message.height),\n );\n frame.style.height = `${height}px`;\n }\n break;\n }\n case \"grizzlee:session_expired\": {\n const renew = options().onSessionExpired;\n if (!renew || renewing) return;\n renewing = true;\n void (async () => {\n try {\n const url = await renew();\n if (!active || !url) return;\n const next = embedUrl(url);\n origin = next.origin;\n frame.style.height = `${minimumHeight(options().minHeight)}px`;\n frame.src = next.href;\n } catch {\n // Leave the expired dashboard in place. The merchant handles errors in its callback.\n } finally {\n renewing = false;\n }\n })();\n break;\n }\n }\n };\n\n hostWindow.addEventListener(\"message\", receive);\n return () => {\n active = false;\n hostWindow.removeEventListener(\"message\", receive);\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/embed.js
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
|
-
// packages/react/src/
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
return Number.isFinite(height) ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height))) : 320;
|
|
5
|
-
}
|
|
6
|
-
function embedUrl(url) {
|
|
7
|
-
const parsed = new URL(url);
|
|
1
|
+
// packages/react/src/protocol.ts
|
|
2
|
+
function embedUrl(url, Url = URL) {
|
|
3
|
+
const parsed = new Url(url);
|
|
8
4
|
if (!["https:", "http:"].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
9
5
|
throw new Error(
|
|
10
6
|
"Use an absolute HTTP or HTTPS embed URL without credentials"
|
|
11
7
|
);
|
|
12
8
|
return parsed;
|
|
13
9
|
}
|
|
10
|
+
function parseShare(message, validateUrl = embedUrl) {
|
|
11
|
+
if (!("programId" in message) || typeof message.programId !== "string" || !message.programId || !("url" in message) || typeof message.url !== "string" || !("text" in message) || typeof message.text !== "string")
|
|
12
|
+
return null;
|
|
13
|
+
try {
|
|
14
|
+
validateUrl(message.url);
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return { programId: message.programId, url: message.url, text: message.text };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// packages/react/src/host.ts
|
|
22
|
+
var MAX_HEIGHT = 1e4;
|
|
23
|
+
function minimumHeight(height = 320) {
|
|
24
|
+
return Number.isFinite(height) ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height))) : 320;
|
|
25
|
+
}
|
|
14
26
|
function listenToEmbed(frame, options) {
|
|
15
27
|
const hostWindow = frame.ownerDocument.defaultView;
|
|
16
28
|
if (!hostWindow)
|
|
@@ -25,8 +37,17 @@ function listenToEmbed(frame, options) {
|
|
|
25
37
|
if (!message || typeof message !== "object" || !("type" in message)) return;
|
|
26
38
|
switch (message.type) {
|
|
27
39
|
case "grizzlee:ready":
|
|
40
|
+
frame.contentWindow.postMessage(
|
|
41
|
+
{ type: "grizzlee:configure", share: !!options().onShare },
|
|
42
|
+
origin
|
|
43
|
+
);
|
|
28
44
|
options().onReady?.();
|
|
29
45
|
break;
|
|
46
|
+
case "grizzlee:share": {
|
|
47
|
+
const share = parseShare(message);
|
|
48
|
+
if (share) options().onShare?.(share);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
30
51
|
case "grizzlee:resize": {
|
|
31
52
|
if ("height" in message && typeof message.height === "number" && Number.isSafeInteger(message.height) && message.height > 0) {
|
|
32
53
|
const height = Math.min(
|
package/embed.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/host.ts", "../src/embed.ts"],
|
|
4
|
-
"sourcesContent": ["export interface GrizzleeEmbedOptions {\n /** An absolute HTTP or HTTPS start URL minted by the merchant's server. */\n url: string;\n /** Return a newly minted URL to reopen the dashboard. Handle errors here. */\n onSessionExpired?: () => string | void | Promise<string | void>;\n onReady?: () => void;\n title?: string;\n className?: string;\n /** Minimum height in CSS pixels, clamped to 120 through 10,000. Defaults to 320. */\n minHeight?: number;\n}\n\nconst MAX_HEIGHT = 10_000;\n\nexport function minimumHeight(height = 320): number {\n return Number.isFinite(height)\n ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height)))\n : 320;\n}\n\
|
|
5
|
-
"mappings": ";
|
|
3
|
+
"sources": ["../src/protocol.ts", "../src/host.ts", "../src/embed.ts"],
|
|
4
|
+
"sourcesContent": ["export interface GrizzleeShare {\n programId: string;\n url: string;\n text: string;\n}\n\nexport function embedUrl(\n url: string,\n Url: new (value: string) => URL = URL,\n): URL {\n const parsed = new Url(url);\n if (\n ![\"https:\", \"http:\"].includes(parsed.protocol) ||\n parsed.username ||\n parsed.password\n )\n throw new Error(\n \"Use an absolute HTTP or HTTPS embed URL without credentials\",\n );\n return parsed;\n}\n\nexport function parseShare(\n message: object,\n validateUrl: (value: string) => unknown = embedUrl,\n): GrizzleeShare | null {\n if (\n !(\"programId\" in message) ||\n typeof message.programId !== \"string\" ||\n !message.programId ||\n !(\"url\" in message) ||\n typeof message.url !== \"string\" ||\n !(\"text\" in message) ||\n typeof message.text !== \"string\"\n )\n return null;\n try {\n validateUrl(message.url);\n } catch {\n return null;\n }\n return { programId: message.programId, url: message.url, text: message.text };\n}\n", "import { embedUrl, parseShare, type GrizzleeShare } from \"./protocol\";\n\nexport { embedUrl } from \"./protocol\";\nexport type { GrizzleeShare } from \"./protocol\";\n\nexport interface GrizzleeEmbedOptions {\n /** An absolute HTTP or HTTPS start URL minted by the merchant's server. */\n url: string;\n /** Return a newly minted URL to reopen the dashboard. Handle errors here. */\n onSessionExpired?: () => string | void | Promise<string | void>;\n onReady?: () => void;\n /** Supplying this callback asks the frame to send share actions to the host. */\n onShare?: (share: GrizzleeShare) => void;\n title?: string;\n className?: string;\n /** Minimum height in CSS pixels, clamped to 120 through 10,000. Defaults to 320. */\n minHeight?: number;\n}\n\nconst MAX_HEIGHT = 10_000;\n\nexport function minimumHeight(height = 320): number {\n return Number.isFinite(height)\n ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height)))\n : 320;\n}\n\n/** Both hosts use this listener so origin checks and renewal behave the same way. */\nexport function listenToEmbed(\n frame: HTMLIFrameElement,\n options: () => GrizzleeEmbedOptions,\n): () => void {\n const hostWindow = frame.ownerDocument.defaultView;\n if (!hostWindow)\n throw new Error(\"The iframe must belong to a browser document\");\n let origin = embedUrl(options().url).origin;\n let active = true;\n let renewing = false;\n\n const receive = (event: MessageEvent<unknown>) => {\n if (\n !active ||\n event.origin !== origin ||\n !frame.contentWindow ||\n event.source !== frame.contentWindow\n )\n return;\n const message = event.data;\n if (!message || typeof message !== \"object\" || !(\"type\" in message)) return;\n\n switch (message.type) {\n case \"grizzlee:ready\":\n frame.contentWindow.postMessage(\n { type: \"grizzlee:configure\", share: !!options().onShare },\n origin,\n );\n options().onReady?.();\n break;\n case \"grizzlee:share\": {\n const share = parseShare(message);\n if (share) options().onShare?.(share);\n break;\n }\n case \"grizzlee:resize\": {\n if (\n \"height\" in message &&\n typeof message.height === \"number\" &&\n Number.isSafeInteger(message.height) &&\n message.height > 0\n ) {\n const height = Math.min(\n MAX_HEIGHT,\n Math.max(minimumHeight(options().minHeight), message.height),\n );\n frame.style.height = `${height}px`;\n }\n break;\n }\n case \"grizzlee:session_expired\": {\n const renew = options().onSessionExpired;\n if (!renew || renewing) return;\n renewing = true;\n void (async () => {\n try {\n const url = await renew();\n if (!active || !url) return;\n const next = embedUrl(url);\n origin = next.origin;\n frame.style.height = `${minimumHeight(options().minHeight)}px`;\n frame.src = next.href;\n } catch {\n // Leave the expired dashboard in place. The merchant handles errors in its callback.\n } finally {\n renewing = false;\n }\n })();\n break;\n }\n }\n };\n\n hostWindow.addEventListener(\"message\", receive);\n return () => {\n active = false;\n hostWindow.removeEventListener(\"message\", receive);\n };\n}\n", "import {\n embedUrl,\n listenToEmbed,\n minimumHeight,\n type GrizzleeEmbedOptions,\n} from \"./host\";\n\nexport type { GrizzleeEmbedOptions, GrizzleeShare } from \"./host\";\n\n/** Append a dashboard iframe. Destroy removes its listener and the iframe. */\nexport function mountGrizzleeEmbed(\n container: HTMLElement,\n options: GrizzleeEmbedOptions,\n): { destroy(): void } {\n const url = embedUrl(options.url);\n const frame = container.ownerDocument.createElement(\"iframe\");\n frame.title = options.title ?? \"Partner dashboard\";\n if (options.className) frame.className = options.className;\n frame.allow = \"clipboard-write\";\n frame.referrerPolicy = \"no-referrer\";\n frame.loading = \"lazy\";\n frame.style.width = \"100%\";\n frame.style.border = \"0\";\n frame.style.minHeight = `${minimumHeight(options.minHeight)}px`;\n frame.style.height = frame.style.minHeight;\n const stop = listenToEmbed(frame, () => options);\n frame.src = url.href;\n container.appendChild(frame);\n return {\n destroy() {\n stop();\n frame.remove();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/index.cjs
CHANGED
|
@@ -27,19 +27,31 @@ __export(index_exports, {
|
|
|
27
27
|
module.exports = __toCommonJS(index_exports);
|
|
28
28
|
var import_react = require("react");
|
|
29
29
|
|
|
30
|
-
// packages/react/src/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return Number.isFinite(height) ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height))) : 320;
|
|
34
|
-
}
|
|
35
|
-
function embedUrl(url) {
|
|
36
|
-
const parsed = new URL(url);
|
|
30
|
+
// packages/react/src/protocol.ts
|
|
31
|
+
function embedUrl(url, Url = URL) {
|
|
32
|
+
const parsed = new Url(url);
|
|
37
33
|
if (!["https:", "http:"].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
38
34
|
throw new Error(
|
|
39
35
|
"Use an absolute HTTP or HTTPS embed URL without credentials"
|
|
40
36
|
);
|
|
41
37
|
return parsed;
|
|
42
38
|
}
|
|
39
|
+
function parseShare(message, validateUrl = embedUrl) {
|
|
40
|
+
if (!("programId" in message) || typeof message.programId !== "string" || !message.programId || !("url" in message) || typeof message.url !== "string" || !("text" in message) || typeof message.text !== "string")
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
validateUrl(message.url);
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return { programId: message.programId, url: message.url, text: message.text };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// packages/react/src/host.ts
|
|
51
|
+
var MAX_HEIGHT = 1e4;
|
|
52
|
+
function minimumHeight(height = 320) {
|
|
53
|
+
return Number.isFinite(height) ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height))) : 320;
|
|
54
|
+
}
|
|
43
55
|
function listenToEmbed(frame, options) {
|
|
44
56
|
const hostWindow = frame.ownerDocument.defaultView;
|
|
45
57
|
if (!hostWindow)
|
|
@@ -54,8 +66,17 @@ function listenToEmbed(frame, options) {
|
|
|
54
66
|
if (!message || typeof message !== "object" || !("type" in message)) return;
|
|
55
67
|
switch (message.type) {
|
|
56
68
|
case "grizzlee:ready":
|
|
69
|
+
frame.contentWindow.postMessage(
|
|
70
|
+
{ type: "grizzlee:configure", share: !!options().onShare },
|
|
71
|
+
origin
|
|
72
|
+
);
|
|
57
73
|
options().onReady?.();
|
|
58
74
|
break;
|
|
75
|
+
case "grizzlee:share": {
|
|
76
|
+
const share = parseShare(message);
|
|
77
|
+
if (share) options().onShare?.(share);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
59
80
|
case "grizzlee:resize": {
|
|
60
81
|
if ("height" in message && typeof message.height === "number" && Number.isSafeInteger(message.height) && message.height > 0) {
|
|
61
82
|
const height = Math.min(
|
|
@@ -127,6 +148,12 @@ function GrizzleeEmbed(props) {
|
|
|
127
148
|
(0, import_react.useEffect)(() => {
|
|
128
149
|
current.current = props;
|
|
129
150
|
});
|
|
151
|
+
(0, import_react.useEffect)(() => {
|
|
152
|
+
frame.current?.contentWindow?.postMessage(
|
|
153
|
+
{ type: "grizzlee:configure", share: !!props.onShare },
|
|
154
|
+
embedUrl(url).origin
|
|
155
|
+
);
|
|
156
|
+
}, [url, props.onShare]);
|
|
130
157
|
(0, import_react.useEffect)(() => {
|
|
131
158
|
if (frame.current)
|
|
132
159
|
return listenToEmbed(frame.current, () => current.current);
|
package/index.cjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/index.ts", "../src/host.ts", "../src/embed.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\";\n\nimport {\n createElement,\n useEffect,\n useRef,\n type CSSProperties,\n type ReactElement,\n} from \"react\";\nimport {\n embedUrl,\n listenToEmbed,\n minimumHeight,\n type GrizzleeEmbedOptions,\n} from \"./host\";\n\nexport { mountGrizzleeEmbed } from \"./embed\";\nexport type { GrizzleeEmbedOptions } from \"./host\";\n\nexport interface GrizzleeEmbedProps extends GrizzleeEmbedOptions {\n style?: CSSProperties;\n}\n\n/** Render a dashboard without putting a server key in the browser. */\nexport function GrizzleeEmbed(props: GrizzleeEmbedProps): ReactElement {\n const frame = useRef<HTMLIFrameElement>(null);\n const current = useRef(props);\n const url = embedUrl(props.url).href;\n const height = minimumHeight(props.minHeight);\n\n useEffect(() => {\n current.current = props;\n });\n useEffect(() => {\n if (frame.current)\n return listenToEmbed(frame.current, () => current.current);\n }, [url]);\n\n return createElement(\"iframe\", {\n ref: frame,\n src: url,\n title: props.title ?? \"Partner dashboard\",\n className: props.className,\n style: {\n width: \"100%\",\n border: 0,\n ...props.style,\n height,\n minHeight: height,\n },\n allow: \"clipboard-write\",\n referrerPolicy: \"no-referrer\",\n loading: \"lazy\",\n });\n}\n", "export interface GrizzleeEmbedOptions {\n /** An absolute HTTP or HTTPS start URL minted by the merchant's server. */\n url: string;\n /** Return a newly minted URL to reopen the dashboard. Handle errors here. */\n onSessionExpired?: () => string | void | Promise<string | void>;\n onReady?: () => void;\n title?: string;\n className?: string;\n /** Minimum height in CSS pixels, clamped to 120 through 10,000. Defaults to 320. */\n minHeight?: number;\n}\n\nconst MAX_HEIGHT = 10_000;\n\nexport function minimumHeight(height = 320): number {\n return Number.isFinite(height)\n ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height)))\n : 320;\n}\n\
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,mBAMO;;;
|
|
3
|
+
"sources": ["../src/index.ts", "../src/protocol.ts", "../src/host.ts", "../src/embed.ts"],
|
|
4
|
+
"sourcesContent": ["\"use client\";\n\nimport {\n createElement,\n useEffect,\n useRef,\n type CSSProperties,\n type ReactElement,\n} from \"react\";\nimport {\n embedUrl,\n listenToEmbed,\n minimumHeight,\n type GrizzleeEmbedOptions,\n} from \"./host\";\n\nexport { mountGrizzleeEmbed } from \"./embed\";\nexport type { GrizzleeEmbedOptions, GrizzleeShare } from \"./host\";\n\nexport interface GrizzleeEmbedProps extends GrizzleeEmbedOptions {\n style?: CSSProperties;\n}\n\n/** Render a dashboard without putting a server key in the browser. */\nexport function GrizzleeEmbed(props: GrizzleeEmbedProps): ReactElement {\n const frame = useRef<HTMLIFrameElement>(null);\n const current = useRef(props);\n const url = embedUrl(props.url).href;\n const height = minimumHeight(props.minHeight);\n\n useEffect(() => {\n current.current = props;\n });\n useEffect(() => {\n frame.current?.contentWindow?.postMessage(\n { type: \"grizzlee:configure\", share: !!props.onShare },\n embedUrl(url).origin,\n );\n }, [url, props.onShare]);\n useEffect(() => {\n if (frame.current)\n return listenToEmbed(frame.current, () => current.current);\n }, [url]);\n\n return createElement(\"iframe\", {\n ref: frame,\n src: url,\n title: props.title ?? \"Partner dashboard\",\n className: props.className,\n style: {\n width: \"100%\",\n border: 0,\n ...props.style,\n height,\n minHeight: height,\n },\n allow: \"clipboard-write\",\n referrerPolicy: \"no-referrer\",\n loading: \"lazy\",\n });\n}\n", "export interface GrizzleeShare {\n programId: string;\n url: string;\n text: string;\n}\n\nexport function embedUrl(\n url: string,\n Url: new (value: string) => URL = URL,\n): URL {\n const parsed = new Url(url);\n if (\n ![\"https:\", \"http:\"].includes(parsed.protocol) ||\n parsed.username ||\n parsed.password\n )\n throw new Error(\n \"Use an absolute HTTP or HTTPS embed URL without credentials\",\n );\n return parsed;\n}\n\nexport function parseShare(\n message: object,\n validateUrl: (value: string) => unknown = embedUrl,\n): GrizzleeShare | null {\n if (\n !(\"programId\" in message) ||\n typeof message.programId !== \"string\" ||\n !message.programId ||\n !(\"url\" in message) ||\n typeof message.url !== \"string\" ||\n !(\"text\" in message) ||\n typeof message.text !== \"string\"\n )\n return null;\n try {\n validateUrl(message.url);\n } catch {\n return null;\n }\n return { programId: message.programId, url: message.url, text: message.text };\n}\n", "import { embedUrl, parseShare, type GrizzleeShare } from \"./protocol\";\n\nexport { embedUrl } from \"./protocol\";\nexport type { GrizzleeShare } from \"./protocol\";\n\nexport interface GrizzleeEmbedOptions {\n /** An absolute HTTP or HTTPS start URL minted by the merchant's server. */\n url: string;\n /** Return a newly minted URL to reopen the dashboard. Handle errors here. */\n onSessionExpired?: () => string | void | Promise<string | void>;\n onReady?: () => void;\n /** Supplying this callback asks the frame to send share actions to the host. */\n onShare?: (share: GrizzleeShare) => void;\n title?: string;\n className?: string;\n /** Minimum height in CSS pixels, clamped to 120 through 10,000. Defaults to 320. */\n minHeight?: number;\n}\n\nconst MAX_HEIGHT = 10_000;\n\nexport function minimumHeight(height = 320): number {\n return Number.isFinite(height)\n ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height)))\n : 320;\n}\n\n/** Both hosts use this listener so origin checks and renewal behave the same way. */\nexport function listenToEmbed(\n frame: HTMLIFrameElement,\n options: () => GrizzleeEmbedOptions,\n): () => void {\n const hostWindow = frame.ownerDocument.defaultView;\n if (!hostWindow)\n throw new Error(\"The iframe must belong to a browser document\");\n let origin = embedUrl(options().url).origin;\n let active = true;\n let renewing = false;\n\n const receive = (event: MessageEvent<unknown>) => {\n if (\n !active ||\n event.origin !== origin ||\n !frame.contentWindow ||\n event.source !== frame.contentWindow\n )\n return;\n const message = event.data;\n if (!message || typeof message !== \"object\" || !(\"type\" in message)) return;\n\n switch (message.type) {\n case \"grizzlee:ready\":\n frame.contentWindow.postMessage(\n { type: \"grizzlee:configure\", share: !!options().onShare },\n origin,\n );\n options().onReady?.();\n break;\n case \"grizzlee:share\": {\n const share = parseShare(message);\n if (share) options().onShare?.(share);\n break;\n }\n case \"grizzlee:resize\": {\n if (\n \"height\" in message &&\n typeof message.height === \"number\" &&\n Number.isSafeInteger(message.height) &&\n message.height > 0\n ) {\n const height = Math.min(\n MAX_HEIGHT,\n Math.max(minimumHeight(options().minHeight), message.height),\n );\n frame.style.height = `${height}px`;\n }\n break;\n }\n case \"grizzlee:session_expired\": {\n const renew = options().onSessionExpired;\n if (!renew || renewing) return;\n renewing = true;\n void (async () => {\n try {\n const url = await renew();\n if (!active || !url) return;\n const next = embedUrl(url);\n origin = next.origin;\n frame.style.height = `${minimumHeight(options().minHeight)}px`;\n frame.src = next.href;\n } catch {\n // Leave the expired dashboard in place. The merchant handles errors in its callback.\n } finally {\n renewing = false;\n }\n })();\n break;\n }\n }\n };\n\n hostWindow.addEventListener(\"message\", receive);\n return () => {\n active = false;\n hostWindow.removeEventListener(\"message\", receive);\n };\n}\n", "import {\n embedUrl,\n listenToEmbed,\n minimumHeight,\n type GrizzleeEmbedOptions,\n} from \"./host\";\n\nexport type { GrizzleeEmbedOptions, GrizzleeShare } from \"./host\";\n\n/** Append a dashboard iframe. Destroy removes its listener and the iframe. */\nexport function mountGrizzleeEmbed(\n container: HTMLElement,\n options: GrizzleeEmbedOptions,\n): { destroy(): void } {\n const url = embedUrl(options.url);\n const frame = container.ownerDocument.createElement(\"iframe\");\n frame.title = options.title ?? \"Partner dashboard\";\n if (options.className) frame.className = options.className;\n frame.allow = \"clipboard-write\";\n frame.referrerPolicy = \"no-referrer\";\n frame.loading = \"lazy\";\n frame.style.width = \"100%\";\n frame.style.border = \"0\";\n frame.style.minHeight = `${minimumHeight(options.minHeight)}px`;\n frame.style.height = frame.style.minHeight;\n const stop = listenToEmbed(frame, () => options);\n frame.src = url.href;\n container.appendChild(frame);\n return {\n destroy() {\n stop();\n frame.remove();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAEA,mBAMO;;;ACFA,SAAS,SACd,KACA,MAAkC,KAC7B;AACL,QAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,MACE,CAAC,CAAC,UAAU,OAAO,EAAE,SAAS,OAAO,QAAQ,KAC7C,OAAO,YACP,OAAO;AAEP,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AACF,SAAO;AACT;AAEO,SAAS,WACd,SACA,cAA0C,UACpB;AACtB,MACE,EAAE,eAAe,YACjB,OAAO,QAAQ,cAAc,YAC7B,CAAC,QAAQ,aACT,EAAE,SAAS,YACX,OAAO,QAAQ,QAAQ,YACvB,EAAE,UAAU,YACZ,OAAO,QAAQ,SAAS;AAExB,WAAO;AACT,MAAI;AACF,gBAAY,QAAQ,GAAG;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,SAAO,EAAE,WAAW,QAAQ,WAAW,KAAK,QAAQ,KAAK,MAAM,QAAQ,KAAK;AAC9E;;;ACvBA,IAAM,aAAa;AAEZ,SAAS,cAAc,SAAS,KAAa;AAClD,SAAO,OAAO,SAAS,MAAM,IACzB,KAAK,IAAI,YAAY,KAAK,IAAI,KAAK,KAAK,KAAK,MAAM,CAAC,CAAC,IACrD;AACN;AAGO,SAAS,cACd,OACA,SACY;AACZ,QAAM,aAAa,MAAM,cAAc;AACvC,MAAI,CAAC;AACH,UAAM,IAAI,MAAM,8CAA8C;AAChE,MAAI,SAAS,SAAS,QAAQ,EAAE,GAAG,EAAE;AACrC,MAAI,SAAS;AACb,MAAI,WAAW;AAEf,QAAM,UAAU,CAAC,UAAiC;AAChD,QACE,CAAC,UACD,MAAM,WAAW,UACjB,CAAC,MAAM,iBACP,MAAM,WAAW,MAAM;AAEvB;AACF,UAAM,UAAU,MAAM;AACtB,QAAI,CAAC,WAAW,OAAO,YAAY,YAAY,EAAE,UAAU,SAAU;AAErE,YAAQ,QAAQ,MAAM;AAAA,MACpB,KAAK;AACH,cAAM,cAAc;AAAA,UAClB,EAAE,MAAM,sBAAsB,OAAO,CAAC,CAAC,QAAQ,EAAE,QAAQ;AAAA,UACzD;AAAA,QACF;AACA,gBAAQ,EAAE,UAAU;AACpB;AAAA,MACF,KAAK,kBAAkB;AACrB,cAAM,QAAQ,WAAW,OAAO;AAChC,YAAI,MAAO,SAAQ,EAAE,UAAU,KAAK;AACpC;AAAA,MACF;AAAA,MACA,KAAK,mBAAmB;AACtB,YACE,YAAY,WACZ,OAAO,QAAQ,WAAW,YAC1B,OAAO,cAAc,QAAQ,MAAM,KACnC,QAAQ,SAAS,GACjB;AACA,gBAAM,SAAS,KAAK;AAAA,YAClB;AAAA,YACA,KAAK,IAAI,cAAc,QAAQ,EAAE,SAAS,GAAG,QAAQ,MAAM;AAAA,UAC7D;AACA,gBAAM,MAAM,SAAS,GAAG,MAAM;AAAA,QAChC;AACA;AAAA,MACF;AAAA,MACA,KAAK,4BAA4B;AAC/B,cAAM,QAAQ,QAAQ,EAAE;AACxB,YAAI,CAAC,SAAS,SAAU;AACxB,mBAAW;AACX,cAAM,YAAY;AAChB,cAAI;AACF,kBAAM,MAAM,MAAM,MAAM;AACxB,gBAAI,CAAC,UAAU,CAAC,IAAK;AACrB,kBAAM,OAAO,SAAS,GAAG;AACzB,qBAAS,KAAK;AACd,kBAAM,MAAM,SAAS,GAAG,cAAc,QAAQ,EAAE,SAAS,CAAC;AAC1D,kBAAM,MAAM,KAAK;AAAA,UACnB,QAAQ;AAAA,UAER,UAAE;AACA,uBAAW;AAAA,UACb;AAAA,QACF,GAAG;AACH;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,aAAW,iBAAiB,WAAW,OAAO;AAC9C,SAAO,MAAM;AACX,aAAS;AACT,eAAW,oBAAoB,WAAW,OAAO;AAAA,EACnD;AACF;;;AChGO,SAAS,mBACd,WACA,SACqB;AACrB,QAAM,MAAM,SAAS,QAAQ,GAAG;AAChC,QAAM,QAAQ,UAAU,cAAc,cAAc,QAAQ;AAC5D,QAAM,QAAQ,QAAQ,SAAS;AAC/B,MAAI,QAAQ,UAAW,OAAM,YAAY,QAAQ;AACjD,QAAM,QAAQ;AACd,QAAM,iBAAiB;AACvB,QAAM,UAAU;AAChB,QAAM,MAAM,QAAQ;AACpB,QAAM,MAAM,SAAS;AACrB,QAAM,MAAM,YAAY,GAAG,cAAc,QAAQ,SAAS,CAAC;AAC3D,QAAM,MAAM,SAAS,MAAM,MAAM;AACjC,QAAM,OAAO,cAAc,OAAO,MAAM,OAAO;AAC/C,QAAM,MAAM,IAAI;AAChB,YAAU,YAAY,KAAK;AAC3B,SAAO;AAAA,IACL,UAAU;AACR,WAAK;AACL,YAAM,OAAO;AAAA,IACf;AAAA,EACF;AACF;;;AHVO,SAAS,cAAc,OAAyC;AACrE,QAAM,YAAQ,qBAA0B,IAAI;AAC5C,QAAM,cAAU,qBAAO,KAAK;AAC5B,QAAM,MAAM,SAAS,MAAM,GAAG,EAAE;AAChC,QAAM,SAAS,cAAc,MAAM,SAAS;AAE5C,8BAAU,MAAM;AACd,YAAQ,UAAU;AAAA,EACpB,CAAC;AACD,8BAAU,MAAM;AACd,UAAM,SAAS,eAAe;AAAA,MAC5B,EAAE,MAAM,sBAAsB,OAAO,CAAC,CAAC,MAAM,QAAQ;AAAA,MACrD,SAAS,GAAG,EAAE;AAAA,IAChB;AAAA,EACF,GAAG,CAAC,KAAK,MAAM,OAAO,CAAC;AACvB,8BAAU,MAAM;AACd,QAAI,MAAM;AACR,aAAO,cAAc,MAAM,SAAS,MAAM,QAAQ,OAAO;AAAA,EAC7D,GAAG,CAAC,GAAG,CAAC;AAER,aAAO,4BAAc,UAAU;AAAA,IAC7B,KAAK;AAAA,IACL,KAAK;AAAA,IACL,OAAO,MAAM,SAAS;AAAA,IACtB,WAAW,MAAM;AAAA,IACjB,OAAO;AAAA,MACL,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,GAAG,MAAM;AAAA,MACT;AAAA,MACA,WAAW;AAAA,IACb;AAAA,IACA,OAAO;AAAA,IACP,gBAAgB;AAAA,IAChB,SAAS;AAAA,EACX,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/index.js
CHANGED
|
@@ -7,19 +7,31 @@ import {
|
|
|
7
7
|
useRef
|
|
8
8
|
} from "react";
|
|
9
9
|
|
|
10
|
-
// packages/react/src/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return Number.isFinite(height) ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height))) : 320;
|
|
14
|
-
}
|
|
15
|
-
function embedUrl(url) {
|
|
16
|
-
const parsed = new URL(url);
|
|
10
|
+
// packages/react/src/protocol.ts
|
|
11
|
+
function embedUrl(url, Url = URL) {
|
|
12
|
+
const parsed = new Url(url);
|
|
17
13
|
if (!["https:", "http:"].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
18
14
|
throw new Error(
|
|
19
15
|
"Use an absolute HTTP or HTTPS embed URL without credentials"
|
|
20
16
|
);
|
|
21
17
|
return parsed;
|
|
22
18
|
}
|
|
19
|
+
function parseShare(message, validateUrl = embedUrl) {
|
|
20
|
+
if (!("programId" in message) || typeof message.programId !== "string" || !message.programId || !("url" in message) || typeof message.url !== "string" || !("text" in message) || typeof message.text !== "string")
|
|
21
|
+
return null;
|
|
22
|
+
try {
|
|
23
|
+
validateUrl(message.url);
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return { programId: message.programId, url: message.url, text: message.text };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// packages/react/src/host.ts
|
|
31
|
+
var MAX_HEIGHT = 1e4;
|
|
32
|
+
function minimumHeight(height = 320) {
|
|
33
|
+
return Number.isFinite(height) ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height))) : 320;
|
|
34
|
+
}
|
|
23
35
|
function listenToEmbed(frame, options) {
|
|
24
36
|
const hostWindow = frame.ownerDocument.defaultView;
|
|
25
37
|
if (!hostWindow)
|
|
@@ -34,8 +46,17 @@ function listenToEmbed(frame, options) {
|
|
|
34
46
|
if (!message || typeof message !== "object" || !("type" in message)) return;
|
|
35
47
|
switch (message.type) {
|
|
36
48
|
case "grizzlee:ready":
|
|
49
|
+
frame.contentWindow.postMessage(
|
|
50
|
+
{ type: "grizzlee:configure", share: !!options().onShare },
|
|
51
|
+
origin
|
|
52
|
+
);
|
|
37
53
|
options().onReady?.();
|
|
38
54
|
break;
|
|
55
|
+
case "grizzlee:share": {
|
|
56
|
+
const share = parseShare(message);
|
|
57
|
+
if (share) options().onShare?.(share);
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
39
60
|
case "grizzlee:resize": {
|
|
40
61
|
if ("height" in message && typeof message.height === "number" && Number.isSafeInteger(message.height) && message.height > 0) {
|
|
41
62
|
const height = Math.min(
|
|
@@ -107,6 +128,12 @@ function GrizzleeEmbed(props) {
|
|
|
107
128
|
useEffect(() => {
|
|
108
129
|
current.current = props;
|
|
109
130
|
});
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
frame.current?.contentWindow?.postMessage(
|
|
133
|
+
{ type: "grizzlee:configure", share: !!props.onShare },
|
|
134
|
+
embedUrl(url).origin
|
|
135
|
+
);
|
|
136
|
+
}, [url, props.onShare]);
|
|
110
137
|
useEffect(() => {
|
|
111
138
|
if (frame.current)
|
|
112
139
|
return listenToEmbed(frame.current, () => current.current);
|
package/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/index.ts", "../src/host.ts", "../src/embed.ts"],
|
|
4
|
-
"sourcesContent": ["\"use client\";\n\nimport {\n createElement,\n useEffect,\n useRef,\n type CSSProperties,\n type ReactElement,\n} from \"react\";\nimport {\n embedUrl,\n listenToEmbed,\n minimumHeight,\n type GrizzleeEmbedOptions,\n} from \"./host\";\n\nexport { mountGrizzleeEmbed } from \"./embed\";\nexport type { GrizzleeEmbedOptions } from \"./host\";\n\nexport interface GrizzleeEmbedProps extends GrizzleeEmbedOptions {\n style?: CSSProperties;\n}\n\n/** Render a dashboard without putting a server key in the browser. */\nexport function GrizzleeEmbed(props: GrizzleeEmbedProps): ReactElement {\n const frame = useRef<HTMLIFrameElement>(null);\n const current = useRef(props);\n const url = embedUrl(props.url).href;\n const height = minimumHeight(props.minHeight);\n\n useEffect(() => {\n current.current = props;\n });\n useEffect(() => {\n if (frame.current)\n return listenToEmbed(frame.current, () => current.current);\n }, [url]);\n\n return createElement(\"iframe\", {\n ref: frame,\n src: url,\n title: props.title ?? \"Partner dashboard\",\n className: props.className,\n style: {\n width: \"100%\",\n border: 0,\n ...props.style,\n height,\n minHeight: height,\n },\n allow: \"clipboard-write\",\n referrerPolicy: \"no-referrer\",\n loading: \"lazy\",\n });\n}\n", "export interface GrizzleeEmbedOptions {\n /** An absolute HTTP or HTTPS start URL minted by the merchant's server. */\n url: string;\n /** Return a newly minted URL to reopen the dashboard. Handle errors here. */\n onSessionExpired?: () => string | void | Promise<string | void>;\n onReady?: () => void;\n title?: string;\n className?: string;\n /** Minimum height in CSS pixels, clamped to 120 through 10,000. Defaults to 320. */\n minHeight?: number;\n}\n\nconst MAX_HEIGHT = 10_000;\n\nexport function minimumHeight(height = 320): number {\n return Number.isFinite(height)\n ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height)))\n : 320;\n}\n\
|
|
5
|
-
"mappings": ";;;AAEA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;;;
|
|
3
|
+
"sources": ["../src/index.ts", "../src/protocol.ts", "../src/host.ts", "../src/embed.ts"],
|
|
4
|
+
"sourcesContent": ["\"use client\";\n\nimport {\n createElement,\n useEffect,\n useRef,\n type CSSProperties,\n type ReactElement,\n} from \"react\";\nimport {\n embedUrl,\n listenToEmbed,\n minimumHeight,\n type GrizzleeEmbedOptions,\n} from \"./host\";\n\nexport { mountGrizzleeEmbed } from \"./embed\";\nexport type { GrizzleeEmbedOptions, GrizzleeShare } from \"./host\";\n\nexport interface GrizzleeEmbedProps extends GrizzleeEmbedOptions {\n style?: CSSProperties;\n}\n\n/** Render a dashboard without putting a server key in the browser. */\nexport function GrizzleeEmbed(props: GrizzleeEmbedProps): ReactElement {\n const frame = useRef<HTMLIFrameElement>(null);\n const current = useRef(props);\n const url = embedUrl(props.url).href;\n const height = minimumHeight(props.minHeight);\n\n useEffect(() => {\n current.current = props;\n });\n useEffect(() => {\n frame.current?.contentWindow?.postMessage(\n { type: \"grizzlee:configure\", share: !!props.onShare },\n embedUrl(url).origin,\n );\n }, [url, props.onShare]);\n useEffect(() => {\n if (frame.current)\n return listenToEmbed(frame.current, () => current.current);\n }, [url]);\n\n return createElement(\"iframe\", {\n ref: frame,\n src: url,\n title: props.title ?? \"Partner dashboard\",\n className: props.className,\n style: {\n width: \"100%\",\n border: 0,\n ...props.style,\n height,\n minHeight: height,\n },\n allow: \"clipboard-write\",\n referrerPolicy: \"no-referrer\",\n loading: \"lazy\",\n });\n}\n", "export interface GrizzleeShare {\n programId: string;\n url: string;\n text: string;\n}\n\nexport function embedUrl(\n url: string,\n Url: new (value: string) => URL = URL,\n): URL {\n const parsed = new Url(url);\n if (\n ![\"https:\", \"http:\"].includes(parsed.protocol) ||\n parsed.username ||\n parsed.password\n )\n throw new Error(\n \"Use an absolute HTTP or HTTPS embed URL without credentials\",\n );\n return parsed;\n}\n\nexport function parseShare(\n message: object,\n validateUrl: (value: string) => unknown = embedUrl,\n): GrizzleeShare | null {\n if (\n !(\"programId\" in message) ||\n typeof message.programId !== \"string\" ||\n !message.programId ||\n !(\"url\" in message) ||\n typeof message.url !== \"string\" ||\n !(\"text\" in message) ||\n typeof message.text !== \"string\"\n )\n return null;\n try {\n validateUrl(message.url);\n } catch {\n return null;\n }\n return { programId: message.programId, url: message.url, text: message.text };\n}\n", "import { embedUrl, parseShare, type GrizzleeShare } from \"./protocol\";\n\nexport { embedUrl } from \"./protocol\";\nexport type { GrizzleeShare } from \"./protocol\";\n\nexport interface GrizzleeEmbedOptions {\n /** An absolute HTTP or HTTPS start URL minted by the merchant's server. */\n url: string;\n /** Return a newly minted URL to reopen the dashboard. Handle errors here. */\n onSessionExpired?: () => string | void | Promise<string | void>;\n onReady?: () => void;\n /** Supplying this callback asks the frame to send share actions to the host. */\n onShare?: (share: GrizzleeShare) => void;\n title?: string;\n className?: string;\n /** Minimum height in CSS pixels, clamped to 120 through 10,000. Defaults to 320. */\n minHeight?: number;\n}\n\nconst MAX_HEIGHT = 10_000;\n\nexport function minimumHeight(height = 320): number {\n return Number.isFinite(height)\n ? Math.min(MAX_HEIGHT, Math.max(120, Math.ceil(height)))\n : 320;\n}\n\n/** Both hosts use this listener so origin checks and renewal behave the same way. */\nexport function listenToEmbed(\n frame: HTMLIFrameElement,\n options: () => GrizzleeEmbedOptions,\n): () => void {\n const hostWindow = frame.ownerDocument.defaultView;\n if (!hostWindow)\n throw new Error(\"The iframe must belong to a browser document\");\n let origin = embedUrl(options().url).origin;\n let active = true;\n let renewing = false;\n\n const receive = (event: MessageEvent<unknown>) => {\n if (\n !active ||\n event.origin !== origin ||\n !frame.contentWindow ||\n event.source !== frame.contentWindow\n )\n return;\n const message = event.data;\n if (!message || typeof message !== \"object\" || !(\"type\" in message)) return;\n\n switch (message.type) {\n case \"grizzlee:ready\":\n frame.contentWindow.postMessage(\n { type: \"grizzlee:configure\", share: !!options().onShare },\n origin,\n );\n options().onReady?.();\n break;\n case \"grizzlee:share\": {\n const share = parseShare(message);\n if (share) options().onShare?.(share);\n break;\n }\n case \"grizzlee:resize\": {\n if (\n \"height\" in message &&\n typeof message.height === \"number\" &&\n Number.isSafeInteger(message.height) &&\n message.height > 0\n ) {\n const height = Math.min(\n MAX_HEIGHT,\n Math.max(minimumHeight(options().minHeight), message.height),\n );\n frame.style.height = `${height}px`;\n }\n break;\n }\n case \"grizzlee:session_expired\": {\n const renew = options().onSessionExpired;\n if (!renew || renewing) return;\n renewing = true;\n void (async () => {\n try {\n const url = await renew();\n if (!active || !url) return;\n const next = embedUrl(url);\n origin = next.origin;\n frame.style.height = `${minimumHeight(options().minHeight)}px`;\n frame.src = next.href;\n } catch {\n // Leave the expired dashboard in place. The merchant handles errors in its callback.\n } finally {\n renewing = false;\n }\n })();\n break;\n }\n }\n };\n\n hostWindow.addEventListener(\"message\", receive);\n return () => {\n active = false;\n hostWindow.removeEventListener(\"message\", receive);\n };\n}\n", "import {\n embedUrl,\n listenToEmbed,\n minimumHeight,\n type GrizzleeEmbedOptions,\n} from \"./host\";\n\nexport type { GrizzleeEmbedOptions, GrizzleeShare } from \"./host\";\n\n/** Append a dashboard iframe. Destroy removes its listener and the iframe. */\nexport function mountGrizzleeEmbed(\n container: HTMLElement,\n options: GrizzleeEmbedOptions,\n): { destroy(): void } {\n const url = embedUrl(options.url);\n const frame = container.ownerDocument.createElement(\"iframe\");\n frame.title = options.title ?? \"Partner dashboard\";\n if (options.className) frame.className = options.className;\n frame.allow = \"clipboard-write\";\n frame.referrerPolicy = \"no-referrer\";\n frame.loading = \"lazy\";\n frame.style.width = \"100%\";\n frame.style.border = \"0\";\n frame.style.minHeight = `${minimumHeight(options.minHeight)}px`;\n frame.style.height = frame.style.minHeight;\n const stop = listenToEmbed(frame, () => options);\n frame.src = url.href;\n container.appendChild(frame);\n return {\n destroy() {\n stop();\n frame.remove();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|