@grizzlee/react 0.2.0 → 0.3.1
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 +28 -4
- package/embed.cjs +21 -21
- package/embed.cjs.map +3 -3
- package/embed.js +21 -21
- package/embed.js.map +3 -3
- package/index.cjs +21 -21
- package/index.cjs.map +3 -3
- package/index.js +21 -21
- package/index.js.map +3 -3
- package/native.cjs +193 -0
- package/native.cjs.map +7 -0
- package/native.js +177 -0
- package/native.js.map +7 -0
- package/package.json +28 -3
- package/types/host.d.cts +3 -6
- package/types/host.d.ts +3 -6
- 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.1
|
|
2
|
+
|
|
3
|
+
- Fix `GrizzleeNativeEmbed` crashing on its first URL with `Cannot read property 'decode' of undefined`. The package bundled `react-native-url-polyfill` with the ES module build of `punycode`, which has no `ucs2` export. The polyfill is now a dependency that the app's bundler resolves.
|
|
4
|
+
|
|
5
|
+
## 0.3.0
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
|
|
1
9
|
## 0.2.0
|
|
2
10
|
|
|
3
11
|
- 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.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Grizzlee embed hosts
|
|
2
2
|
|
|
3
|
-
`@grizzlee/react` exports `GrizzleeEmbed`, `mountGrizzleeEmbed`, `GrizzleeEmbedProps`, `GrizzleeEmbedOptions` and `GrizzleeShare`. React 18 or later is
|
|
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";
|
|
@@ -29,7 +29,7 @@ async function newSession() {
|
|
|
29
29
|
/>
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
`openShareSheet` is your app's callback. `mountGrizzleeEmbed` accepts the same `onShare` option. For React Native or Expo, use
|
|
32
|
+
`openShareSheet` is your app's callback. `mountGrizzleeEmbed` accepts the same `onShare` option. For React Native or Expo, use `GrizzleeNativeEmbed` below.
|
|
33
33
|
|
|
34
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.
|
|
35
35
|
|
|
@@ -49,7 +49,31 @@ dashboard.destroy();
|
|
|
49
49
|
|
|
50
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.
|
|
51
51
|
|
|
52
|
-
|
|
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.
|
|
53
77
|
|
|
54
78
|
## Install
|
|
55
79
|
|
|
@@ -59,7 +83,7 @@ pnpm add @grizzlee/react
|
|
|
59
83
|
yarn add @grizzlee/react
|
|
60
84
|
```
|
|
61
85
|
|
|
62
|
-
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.
|
|
63
87
|
|
|
64
88
|
## Versioning
|
|
65
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)
|
|
@@ -58,20 +70,8 @@ function listenToEmbed(frame, options) {
|
|
|
58
70
|
options().onReady?.();
|
|
59
71
|
break;
|
|
60
72
|
case "grizzlee:share": {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const url = new URL(message.url);
|
|
65
|
-
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password)
|
|
66
|
-
return;
|
|
67
|
-
} catch {
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
options().onShare?.({
|
|
71
|
-
programId: message.programId,
|
|
72
|
-
url: message.url,
|
|
73
|
-
text: message.text
|
|
74
|
-
});
|
|
73
|
+
const share = parseShare(message);
|
|
74
|
+
if (share) options().onShare?.(share);
|
|
75
75
|
break;
|
|
76
76
|
}
|
|
77
77
|
case "grizzlee:resize": {
|
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, 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 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\
|
|
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)
|
|
@@ -32,20 +44,8 @@ function listenToEmbed(frame, options) {
|
|
|
32
44
|
options().onReady?.();
|
|
33
45
|
break;
|
|
34
46
|
case "grizzlee:share": {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const url = new URL(message.url);
|
|
39
|
-
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password)
|
|
40
|
-
return;
|
|
41
|
-
} catch {
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
options().onShare?.({
|
|
45
|
-
programId: message.programId,
|
|
46
|
-
url: message.url,
|
|
47
|
-
text: message.text
|
|
48
|
-
});
|
|
47
|
+
const share = parseShare(message);
|
|
48
|
+
if (share) options().onShare?.(share);
|
|
49
49
|
break;
|
|
50
50
|
}
|
|
51
51
|
case "grizzlee:resize": {
|
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 GrizzleeShare {\n programId: string;\n url: string;\n text: string;\n}\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\
|
|
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)
|
|
@@ -61,20 +73,8 @@ function listenToEmbed(frame, options) {
|
|
|
61
73
|
options().onReady?.();
|
|
62
74
|
break;
|
|
63
75
|
case "grizzlee:share": {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const url = new URL(message.url);
|
|
68
|
-
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password)
|
|
69
|
-
return;
|
|
70
|
-
} catch {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
options().onShare?.({
|
|
74
|
-
programId: message.programId,
|
|
75
|
-
url: message.url,
|
|
76
|
-
text: message.text
|
|
77
|
-
});
|
|
76
|
+
const share = parseShare(message);
|
|
77
|
+
if (share) options().onShare?.(share);
|
|
78
78
|
break;
|
|
79
79
|
}
|
|
80
80
|
case "grizzlee:resize": {
|
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, 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 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\
|
|
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)
|
|
@@ -41,20 +53,8 @@ function listenToEmbed(frame, options) {
|
|
|
41
53
|
options().onReady?.();
|
|
42
54
|
break;
|
|
43
55
|
case "grizzlee:share": {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
const url = new URL(message.url);
|
|
48
|
-
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password)
|
|
49
|
-
return;
|
|
50
|
-
} catch {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
options().onShare?.({
|
|
54
|
-
programId: message.programId,
|
|
55
|
-
url: message.url,
|
|
56
|
-
text: message.text
|
|
57
|
-
});
|
|
56
|
+
const share = parseShare(message);
|
|
57
|
+
if (share) options().onShare?.(share);
|
|
58
58
|
break;
|
|
59
59
|
}
|
|
60
60
|
case "grizzlee:resize": {
|
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, 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 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\
|
|
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
|
}
|
package/native.cjs
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// packages/react/src/native.ts
|
|
21
|
+
var native_exports = {};
|
|
22
|
+
__export(native_exports, {
|
|
23
|
+
GrizzleeNativeEmbed: () => GrizzleeNativeEmbed
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(native_exports);
|
|
26
|
+
var import_react = require("react");
|
|
27
|
+
var import_react_native = require("react-native");
|
|
28
|
+
var import_react_native_webview = require("react-native-webview");
|
|
29
|
+
var import_react_native_url_polyfill = require("react-native-url-polyfill");
|
|
30
|
+
|
|
31
|
+
// packages/react/src/protocol.ts
|
|
32
|
+
function embedUrl(url, Url = URL) {
|
|
33
|
+
const parsed = new Url(url);
|
|
34
|
+
if (!["https:", "http:"].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
35
|
+
throw new Error(
|
|
36
|
+
"Use an absolute HTTP or HTTPS embed URL without credentials"
|
|
37
|
+
);
|
|
38
|
+
return parsed;
|
|
39
|
+
}
|
|
40
|
+
function parseShare(message, validateUrl = embedUrl) {
|
|
41
|
+
if (!("programId" in message) || typeof message.programId !== "string" || !message.programId || !("url" in message) || typeof message.url !== "string" || !("text" in message) || typeof message.text !== "string")
|
|
42
|
+
return null;
|
|
43
|
+
try {
|
|
44
|
+
validateUrl(message.url);
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return { programId: message.programId, url: message.url, text: message.text };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// packages/react/src/native.ts
|
|
52
|
+
var nativeUrl = (value) => embedUrl(value, import_react_native_url_polyfill.URL);
|
|
53
|
+
function startUrl(value) {
|
|
54
|
+
const url = nativeUrl(value);
|
|
55
|
+
if (url.pathname !== "/embed/start" || !url.searchParams.get("session"))
|
|
56
|
+
throw new Error("Use the /embed/start URL returned by createEmbedSession");
|
|
57
|
+
return url;
|
|
58
|
+
}
|
|
59
|
+
function GrizzleeNativeEmbed(props) {
|
|
60
|
+
const url = startUrl(props.startUrl).href;
|
|
61
|
+
return (0, import_react.createElement)(NativeSession, { ...props, startUrl: url, key: url });
|
|
62
|
+
}
|
|
63
|
+
function NativeSession(props) {
|
|
64
|
+
const frame = (0, import_react.useRef)(null);
|
|
65
|
+
const current = (0, import_react.useRef)(props);
|
|
66
|
+
const active = (0, import_react.useRef)(true);
|
|
67
|
+
const renewing = (0, import_react.useRef)(false);
|
|
68
|
+
const [url, setUrl] = (0, import_react.useState)(props.startUrl);
|
|
69
|
+
const currentUrl = (0, import_react.useRef)(url);
|
|
70
|
+
const source = startUrl(url);
|
|
71
|
+
(0, import_react.useEffect)(() => {
|
|
72
|
+
current.current = props;
|
|
73
|
+
});
|
|
74
|
+
(0, import_react.useEffect)(() => {
|
|
75
|
+
active.current = true;
|
|
76
|
+
return () => {
|
|
77
|
+
active.current = false;
|
|
78
|
+
};
|
|
79
|
+
}, []);
|
|
80
|
+
function isCurrentOrigin(value) {
|
|
81
|
+
if (!active.current || url !== currentUrl.current) return false;
|
|
82
|
+
try {
|
|
83
|
+
return nativeUrl(value).origin === source.origin;
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function isCurrentDocument(value) {
|
|
89
|
+
if (!isCurrentOrigin(value)) return false;
|
|
90
|
+
const sender = nativeUrl(value);
|
|
91
|
+
return sender.pathname === source.pathname && sender.search === source.search;
|
|
92
|
+
}
|
|
93
|
+
function reportError(error) {
|
|
94
|
+
if (active.current && url === currentUrl.current)
|
|
95
|
+
current.current.onError?.(
|
|
96
|
+
error instanceof Error ? error : new Error("Partner dashboard failed")
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
async function renew() {
|
|
100
|
+
if (!active.current || url !== currentUrl.current || renewing.current)
|
|
101
|
+
return;
|
|
102
|
+
const callback = current.current.onExpired;
|
|
103
|
+
if (!callback) return;
|
|
104
|
+
renewing.current = true;
|
|
105
|
+
try {
|
|
106
|
+
const value = await callback();
|
|
107
|
+
if (!active.current || url !== currentUrl.current || !value) return;
|
|
108
|
+
const next = startUrl(value);
|
|
109
|
+
if (next.href === url)
|
|
110
|
+
throw new Error("Session renewal must return a fresh start URL");
|
|
111
|
+
currentUrl.current = next.href;
|
|
112
|
+
setUrl(next.href);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
reportError(error);
|
|
115
|
+
} finally {
|
|
116
|
+
renewing.current = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function share(value) {
|
|
120
|
+
try {
|
|
121
|
+
if (current.current.onShare) await current.current.onShare(value);
|
|
122
|
+
else await import_react_native.Share.share({ message: `${value.text} ${value.url}` });
|
|
123
|
+
} catch (error) {
|
|
124
|
+
reportError(error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function receive(event) {
|
|
128
|
+
if (!isCurrentOrigin(event.nativeEvent.url)) return;
|
|
129
|
+
let message;
|
|
130
|
+
try {
|
|
131
|
+
message = JSON.parse(event.nativeEvent.data);
|
|
132
|
+
} catch {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!message || typeof message !== "object" || !("type" in message)) return;
|
|
136
|
+
switch (message.type) {
|
|
137
|
+
case "grizzlee:ready":
|
|
138
|
+
frame.current?.injectJavaScript(
|
|
139
|
+
`window.postMessage({ type: "grizzlee:configure", share: true }, ${JSON.stringify(source.origin)}); true;`
|
|
140
|
+
);
|
|
141
|
+
current.current.onReady?.();
|
|
142
|
+
break;
|
|
143
|
+
case "grizzlee:share": {
|
|
144
|
+
const value = parseShare(message, nativeUrl);
|
|
145
|
+
if (value) void share(value);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
case "grizzlee:session_expired":
|
|
149
|
+
void renew();
|
|
150
|
+
break;
|
|
151
|
+
// The WebView scrolls the document and keeps the size set by its native parent.
|
|
152
|
+
case "grizzlee:resize":
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return (0, import_react.createElement)(import_react_native_webview.WebView, {
|
|
157
|
+
ref: frame,
|
|
158
|
+
key: url,
|
|
159
|
+
source: { uri: url },
|
|
160
|
+
style: [{ flex: 1 }, props.style],
|
|
161
|
+
// Route every navigation through our check instead of WebView's automatic OS handoff.
|
|
162
|
+
originWhitelist: ["*"],
|
|
163
|
+
onShouldStartLoadWithRequest: (request) => isCurrentDocument(request.url),
|
|
164
|
+
onOpenWindow: () => {
|
|
165
|
+
},
|
|
166
|
+
javaScriptEnabled: true,
|
|
167
|
+
javaScriptCanOpenWindowsAutomatically: false,
|
|
168
|
+
incognito: true,
|
|
169
|
+
cacheEnabled: false,
|
|
170
|
+
sharedCookiesEnabled: false,
|
|
171
|
+
thirdPartyCookiesEnabled: false,
|
|
172
|
+
onMessage: receive,
|
|
173
|
+
onError: (event) => {
|
|
174
|
+
if (isCurrentDocument(event.nativeEvent.url))
|
|
175
|
+
reportError(new Error("Partner dashboard could not load"));
|
|
176
|
+
},
|
|
177
|
+
onHttpError: (event) => {
|
|
178
|
+
if (isCurrentDocument(event.nativeEvent.url))
|
|
179
|
+
reportError(
|
|
180
|
+
new Error(
|
|
181
|
+
`Partner dashboard returned HTTP ${event.nativeEvent.statusCode}`
|
|
182
|
+
)
|
|
183
|
+
);
|
|
184
|
+
},
|
|
185
|
+
onContentProcessDidTerminate: () => {
|
|
186
|
+
void renew();
|
|
187
|
+
},
|
|
188
|
+
onRenderProcessGone: () => {
|
|
189
|
+
void renew();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=native.cjs.map
|
package/native.cjs.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/native.ts", "../src/protocol.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n createElement,\n useEffect,\n useRef,\n useState,\n type ReactElement,\n} from \"react\";\nimport { Share, type StyleProp, type ViewStyle } from \"react-native\";\nimport { WebView, type WebViewMessageEvent } from \"react-native-webview\";\nimport { URL as NativeURL } from \"react-native-url-polyfill\";\nimport { embedUrl, parseShare, type GrizzleeShare } from \"./protocol\";\n\nexport type { GrizzleeShare } from \"./protocol\";\n\nexport interface GrizzleeNativeEmbedProps {\n /** A one-use /embed/start URL minted by the merchant's server. */\n startUrl: string;\n /** Return a fresh start URL, or update startUrl in your app. */\n onExpired?: () => string | void | Promise<string | void>;\n onReady?: () => void;\n /** Without this callback, the component opens React Native's share sheet. */\n onShare?: (share: GrizzleeShare) => void | Promise<void>;\n /** Loading, renewal and sharing errors. */\n onError?: (error: Error) => void;\n style?: StyleProp<ViewStyle>;\n}\n\n// Older React Native releases do not implement URL.origin or URL.protocol.\nconst nativeUrl = (value: string) => embedUrl(value, NativeURL);\n\nfunction startUrl(value: string): URL {\n const url = nativeUrl(value);\n if (url.pathname !== \"/embed/start\" || !url.searchParams.get(\"session\"))\n throw new Error(\"Use the /embed/start URL returned by createEmbedSession\");\n return url;\n}\n\n/** Render the dashboard directly in a WebView. No merchant wrapper page is needed. */\nexport function GrizzleeNativeEmbed(\n props: GrizzleeNativeEmbedProps,\n): ReactElement {\n const url = startUrl(props.startUrl).href;\n return createElement(NativeSession, { ...props, startUrl: url, key: url });\n}\n\nfunction NativeSession(props: GrizzleeNativeEmbedProps): ReactElement {\n const frame = useRef<WebView>(null);\n const current = useRef(props);\n const active = useRef(true);\n const renewing = useRef(false);\n const [url, setUrl] = useState(props.startUrl);\n const currentUrl = useRef(url);\n const source = startUrl(url);\n\n useEffect(() => {\n current.current = props;\n });\n useEffect(() => {\n active.current = true;\n return () => {\n active.current = false;\n };\n }, []);\n\n function isCurrentOrigin(value: string): boolean {\n if (!active.current || url !== currentUrl.current) return false;\n try {\n return nativeUrl(value).origin === source.origin;\n } catch {\n return false;\n }\n }\n\n function isCurrentDocument(value: string): boolean {\n if (!isCurrentOrigin(value)) return false;\n const sender = nativeUrl(value);\n return (\n sender.pathname === source.pathname && sender.search === source.search\n );\n }\n\n function reportError(error: unknown) {\n if (active.current && url === currentUrl.current)\n current.current.onError?.(\n error instanceof Error ? error : new Error(\"Partner dashboard failed\"),\n );\n }\n\n async function renew() {\n if (!active.current || url !== currentUrl.current || renewing.current)\n return;\n const callback = current.current.onExpired;\n if (!callback) return;\n renewing.current = true;\n try {\n const value = await callback();\n if (!active.current || url !== currentUrl.current || !value) return;\n const next = startUrl(value);\n if (next.href === url)\n throw new Error(\"Session renewal must return a fresh start URL\");\n currentUrl.current = next.href;\n setUrl(next.href);\n } catch (error) {\n reportError(error);\n } finally {\n renewing.current = false;\n }\n }\n\n async function share(value: GrizzleeShare) {\n try {\n if (current.current.onShare) await current.current.onShare(value);\n else await Share.share({ message: `${value.text} ${value.url}` });\n } catch (error) {\n reportError(error);\n }\n }\n\n function receive(event: WebViewMessageEvent) {\n if (!isCurrentOrigin(event.nativeEvent.url)) return;\n let message: unknown;\n try {\n message = JSON.parse(event.nativeEvent.data);\n } catch {\n return;\n }\n if (!message || typeof message !== \"object\" || !(\"type\" in message)) return;\n switch (message.type) {\n case \"grizzlee:ready\":\n frame.current?.injectJavaScript(\n `window.postMessage({ type: \"grizzlee:configure\", share: true }, ${JSON.stringify(source.origin)}); true;`,\n );\n current.current.onReady?.();\n break;\n case \"grizzlee:share\": {\n const value = parseShare(message, nativeUrl);\n if (value) void share(value);\n break;\n }\n case \"grizzlee:session_expired\":\n void renew();\n break;\n // The WebView scrolls the document and keeps the size set by its native parent.\n case \"grizzlee:resize\":\n break;\n }\n }\n\n return createElement(WebView, {\n ref: frame,\n key: url,\n source: { uri: url },\n style: [{ flex: 1 }, props.style],\n // Route every navigation through our check instead of WebView's automatic OS handoff.\n originWhitelist: [\"*\"],\n onShouldStartLoadWithRequest: (request) => isCurrentDocument(request.url),\n onOpenWindow: () => {},\n javaScriptEnabled: true,\n javaScriptCanOpenWindowsAutomatically: false,\n incognito: true,\n cacheEnabled: false,\n sharedCookiesEnabled: false,\n thirdPartyCookiesEnabled: false,\n onMessage: receive,\n onError: (event) => {\n if (isCurrentDocument(event.nativeEvent.url))\n reportError(new Error(\"Partner dashboard could not load\"));\n },\n onHttpError: (event) => {\n if (isCurrentDocument(event.nativeEvent.url))\n reportError(\n new Error(\n `Partner dashboard returned HTTP ${event.nativeEvent.statusCode}`,\n ),\n );\n },\n onContentProcessDidTerminate: () => {\n void renew();\n },\n onRenderProcessGone: () => {\n void renew();\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"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["NativeURL"]
|
|
7
|
+
}
|
package/native.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// packages/react/src/native.ts
|
|
2
|
+
import {
|
|
3
|
+
createElement,
|
|
4
|
+
useEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useState
|
|
7
|
+
} from "react";
|
|
8
|
+
import { Share } from "react-native";
|
|
9
|
+
import { WebView } from "react-native-webview";
|
|
10
|
+
import { URL as NativeURL } from "react-native-url-polyfill";
|
|
11
|
+
|
|
12
|
+
// packages/react/src/protocol.ts
|
|
13
|
+
function embedUrl(url, Url = URL) {
|
|
14
|
+
const parsed = new Url(url);
|
|
15
|
+
if (!["https:", "http:"].includes(parsed.protocol) || parsed.username || parsed.password)
|
|
16
|
+
throw new Error(
|
|
17
|
+
"Use an absolute HTTP or HTTPS embed URL without credentials"
|
|
18
|
+
);
|
|
19
|
+
return parsed;
|
|
20
|
+
}
|
|
21
|
+
function parseShare(message, validateUrl = embedUrl) {
|
|
22
|
+
if (!("programId" in message) || typeof message.programId !== "string" || !message.programId || !("url" in message) || typeof message.url !== "string" || !("text" in message) || typeof message.text !== "string")
|
|
23
|
+
return null;
|
|
24
|
+
try {
|
|
25
|
+
validateUrl(message.url);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return { programId: message.programId, url: message.url, text: message.text };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// packages/react/src/native.ts
|
|
33
|
+
var nativeUrl = (value) => embedUrl(value, NativeURL);
|
|
34
|
+
function startUrl(value) {
|
|
35
|
+
const url = nativeUrl(value);
|
|
36
|
+
if (url.pathname !== "/embed/start" || !url.searchParams.get("session"))
|
|
37
|
+
throw new Error("Use the /embed/start URL returned by createEmbedSession");
|
|
38
|
+
return url;
|
|
39
|
+
}
|
|
40
|
+
function GrizzleeNativeEmbed(props) {
|
|
41
|
+
const url = startUrl(props.startUrl).href;
|
|
42
|
+
return createElement(NativeSession, { ...props, startUrl: url, key: url });
|
|
43
|
+
}
|
|
44
|
+
function NativeSession(props) {
|
|
45
|
+
const frame = useRef(null);
|
|
46
|
+
const current = useRef(props);
|
|
47
|
+
const active = useRef(true);
|
|
48
|
+
const renewing = useRef(false);
|
|
49
|
+
const [url, setUrl] = useState(props.startUrl);
|
|
50
|
+
const currentUrl = useRef(url);
|
|
51
|
+
const source = startUrl(url);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
current.current = props;
|
|
54
|
+
});
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
active.current = true;
|
|
57
|
+
return () => {
|
|
58
|
+
active.current = false;
|
|
59
|
+
};
|
|
60
|
+
}, []);
|
|
61
|
+
function isCurrentOrigin(value) {
|
|
62
|
+
if (!active.current || url !== currentUrl.current) return false;
|
|
63
|
+
try {
|
|
64
|
+
return nativeUrl(value).origin === source.origin;
|
|
65
|
+
} catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function isCurrentDocument(value) {
|
|
70
|
+
if (!isCurrentOrigin(value)) return false;
|
|
71
|
+
const sender = nativeUrl(value);
|
|
72
|
+
return sender.pathname === source.pathname && sender.search === source.search;
|
|
73
|
+
}
|
|
74
|
+
function reportError(error) {
|
|
75
|
+
if (active.current && url === currentUrl.current)
|
|
76
|
+
current.current.onError?.(
|
|
77
|
+
error instanceof Error ? error : new Error("Partner dashboard failed")
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
async function renew() {
|
|
81
|
+
if (!active.current || url !== currentUrl.current || renewing.current)
|
|
82
|
+
return;
|
|
83
|
+
const callback = current.current.onExpired;
|
|
84
|
+
if (!callback) return;
|
|
85
|
+
renewing.current = true;
|
|
86
|
+
try {
|
|
87
|
+
const value = await callback();
|
|
88
|
+
if (!active.current || url !== currentUrl.current || !value) return;
|
|
89
|
+
const next = startUrl(value);
|
|
90
|
+
if (next.href === url)
|
|
91
|
+
throw new Error("Session renewal must return a fresh start URL");
|
|
92
|
+
currentUrl.current = next.href;
|
|
93
|
+
setUrl(next.href);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
reportError(error);
|
|
96
|
+
} finally {
|
|
97
|
+
renewing.current = false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function share(value) {
|
|
101
|
+
try {
|
|
102
|
+
if (current.current.onShare) await current.current.onShare(value);
|
|
103
|
+
else await Share.share({ message: `${value.text} ${value.url}` });
|
|
104
|
+
} catch (error) {
|
|
105
|
+
reportError(error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function receive(event) {
|
|
109
|
+
if (!isCurrentOrigin(event.nativeEvent.url)) return;
|
|
110
|
+
let message;
|
|
111
|
+
try {
|
|
112
|
+
message = JSON.parse(event.nativeEvent.data);
|
|
113
|
+
} catch {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (!message || typeof message !== "object" || !("type" in message)) return;
|
|
117
|
+
switch (message.type) {
|
|
118
|
+
case "grizzlee:ready":
|
|
119
|
+
frame.current?.injectJavaScript(
|
|
120
|
+
`window.postMessage({ type: "grizzlee:configure", share: true }, ${JSON.stringify(source.origin)}); true;`
|
|
121
|
+
);
|
|
122
|
+
current.current.onReady?.();
|
|
123
|
+
break;
|
|
124
|
+
case "grizzlee:share": {
|
|
125
|
+
const value = parseShare(message, nativeUrl);
|
|
126
|
+
if (value) void share(value);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case "grizzlee:session_expired":
|
|
130
|
+
void renew();
|
|
131
|
+
break;
|
|
132
|
+
// The WebView scrolls the document and keeps the size set by its native parent.
|
|
133
|
+
case "grizzlee:resize":
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return createElement(WebView, {
|
|
138
|
+
ref: frame,
|
|
139
|
+
key: url,
|
|
140
|
+
source: { uri: url },
|
|
141
|
+
style: [{ flex: 1 }, props.style],
|
|
142
|
+
// Route every navigation through our check instead of WebView's automatic OS handoff.
|
|
143
|
+
originWhitelist: ["*"],
|
|
144
|
+
onShouldStartLoadWithRequest: (request) => isCurrentDocument(request.url),
|
|
145
|
+
onOpenWindow: () => {
|
|
146
|
+
},
|
|
147
|
+
javaScriptEnabled: true,
|
|
148
|
+
javaScriptCanOpenWindowsAutomatically: false,
|
|
149
|
+
incognito: true,
|
|
150
|
+
cacheEnabled: false,
|
|
151
|
+
sharedCookiesEnabled: false,
|
|
152
|
+
thirdPartyCookiesEnabled: false,
|
|
153
|
+
onMessage: receive,
|
|
154
|
+
onError: (event) => {
|
|
155
|
+
if (isCurrentDocument(event.nativeEvent.url))
|
|
156
|
+
reportError(new Error("Partner dashboard could not load"));
|
|
157
|
+
},
|
|
158
|
+
onHttpError: (event) => {
|
|
159
|
+
if (isCurrentDocument(event.nativeEvent.url))
|
|
160
|
+
reportError(
|
|
161
|
+
new Error(
|
|
162
|
+
`Partner dashboard returned HTTP ${event.nativeEvent.statusCode}`
|
|
163
|
+
)
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
onContentProcessDidTerminate: () => {
|
|
167
|
+
void renew();
|
|
168
|
+
},
|
|
169
|
+
onRenderProcessGone: () => {
|
|
170
|
+
void renew();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
export {
|
|
175
|
+
GrizzleeNativeEmbed
|
|
176
|
+
};
|
|
177
|
+
//# sourceMappingURL=native.js.map
|
package/native.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/native.ts", "../src/protocol.ts"],
|
|
4
|
+
"sourcesContent": ["import {\n createElement,\n useEffect,\n useRef,\n useState,\n type ReactElement,\n} from \"react\";\nimport { Share, type StyleProp, type ViewStyle } from \"react-native\";\nimport { WebView, type WebViewMessageEvent } from \"react-native-webview\";\nimport { URL as NativeURL } from \"react-native-url-polyfill\";\nimport { embedUrl, parseShare, type GrizzleeShare } from \"./protocol\";\n\nexport type { GrizzleeShare } from \"./protocol\";\n\nexport interface GrizzleeNativeEmbedProps {\n /** A one-use /embed/start URL minted by the merchant's server. */\n startUrl: string;\n /** Return a fresh start URL, or update startUrl in your app. */\n onExpired?: () => string | void | Promise<string | void>;\n onReady?: () => void;\n /** Without this callback, the component opens React Native's share sheet. */\n onShare?: (share: GrizzleeShare) => void | Promise<void>;\n /** Loading, renewal and sharing errors. */\n onError?: (error: Error) => void;\n style?: StyleProp<ViewStyle>;\n}\n\n// Older React Native releases do not implement URL.origin or URL.protocol.\nconst nativeUrl = (value: string) => embedUrl(value, NativeURL);\n\nfunction startUrl(value: string): URL {\n const url = nativeUrl(value);\n if (url.pathname !== \"/embed/start\" || !url.searchParams.get(\"session\"))\n throw new Error(\"Use the /embed/start URL returned by createEmbedSession\");\n return url;\n}\n\n/** Render the dashboard directly in a WebView. No merchant wrapper page is needed. */\nexport function GrizzleeNativeEmbed(\n props: GrizzleeNativeEmbedProps,\n): ReactElement {\n const url = startUrl(props.startUrl).href;\n return createElement(NativeSession, { ...props, startUrl: url, key: url });\n}\n\nfunction NativeSession(props: GrizzleeNativeEmbedProps): ReactElement {\n const frame = useRef<WebView>(null);\n const current = useRef(props);\n const active = useRef(true);\n const renewing = useRef(false);\n const [url, setUrl] = useState(props.startUrl);\n const currentUrl = useRef(url);\n const source = startUrl(url);\n\n useEffect(() => {\n current.current = props;\n });\n useEffect(() => {\n active.current = true;\n return () => {\n active.current = false;\n };\n }, []);\n\n function isCurrentOrigin(value: string): boolean {\n if (!active.current || url !== currentUrl.current) return false;\n try {\n return nativeUrl(value).origin === source.origin;\n } catch {\n return false;\n }\n }\n\n function isCurrentDocument(value: string): boolean {\n if (!isCurrentOrigin(value)) return false;\n const sender = nativeUrl(value);\n return (\n sender.pathname === source.pathname && sender.search === source.search\n );\n }\n\n function reportError(error: unknown) {\n if (active.current && url === currentUrl.current)\n current.current.onError?.(\n error instanceof Error ? error : new Error(\"Partner dashboard failed\"),\n );\n }\n\n async function renew() {\n if (!active.current || url !== currentUrl.current || renewing.current)\n return;\n const callback = current.current.onExpired;\n if (!callback) return;\n renewing.current = true;\n try {\n const value = await callback();\n if (!active.current || url !== currentUrl.current || !value) return;\n const next = startUrl(value);\n if (next.href === url)\n throw new Error(\"Session renewal must return a fresh start URL\");\n currentUrl.current = next.href;\n setUrl(next.href);\n } catch (error) {\n reportError(error);\n } finally {\n renewing.current = false;\n }\n }\n\n async function share(value: GrizzleeShare) {\n try {\n if (current.current.onShare) await current.current.onShare(value);\n else await Share.share({ message: `${value.text} ${value.url}` });\n } catch (error) {\n reportError(error);\n }\n }\n\n function receive(event: WebViewMessageEvent) {\n if (!isCurrentOrigin(event.nativeEvent.url)) return;\n let message: unknown;\n try {\n message = JSON.parse(event.nativeEvent.data);\n } catch {\n return;\n }\n if (!message || typeof message !== \"object\" || !(\"type\" in message)) return;\n switch (message.type) {\n case \"grizzlee:ready\":\n frame.current?.injectJavaScript(\n `window.postMessage({ type: \"grizzlee:configure\", share: true }, ${JSON.stringify(source.origin)}); true;`,\n );\n current.current.onReady?.();\n break;\n case \"grizzlee:share\": {\n const value = parseShare(message, nativeUrl);\n if (value) void share(value);\n break;\n }\n case \"grizzlee:session_expired\":\n void renew();\n break;\n // The WebView scrolls the document and keeps the size set by its native parent.\n case \"grizzlee:resize\":\n break;\n }\n }\n\n return createElement(WebView, {\n ref: frame,\n key: url,\n source: { uri: url },\n style: [{ flex: 1 }, props.style],\n // Route every navigation through our check instead of WebView's automatic OS handoff.\n originWhitelist: [\"*\"],\n onShouldStartLoadWithRequest: (request) => isCurrentDocument(request.url),\n onOpenWindow: () => {},\n javaScriptEnabled: true,\n javaScriptCanOpenWindowsAutomatically: false,\n incognito: true,\n cacheEnabled: false,\n sharedCookiesEnabled: false,\n thirdPartyCookiesEnabled: false,\n onMessage: receive,\n onError: (event) => {\n if (isCurrentDocument(event.nativeEvent.url))\n reportError(new Error(\"Partner dashboard could not load\"));\n },\n onHttpError: (event) => {\n if (isCurrentDocument(event.nativeEvent.url))\n reportError(\n new Error(\n `Partner dashboard returned HTTP ${event.nativeEvent.statusCode}`,\n ),\n );\n },\n onContentProcessDidTerminate: () => {\n void renew();\n },\n onRenderProcessGone: () => {\n void renew();\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"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grizzlee/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
8
|
"homepage": "https://grizzlee.ca",
|
|
9
|
-
"description": "Embed Grizzlee partner dashboards in React or plain JavaScript apps.",
|
|
9
|
+
"description": "Embed Grizzlee partner dashboards in React, React Native or plain JavaScript apps.",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"sideEffects": false,
|
|
12
12
|
"engines": {
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
"*": {
|
|
20
20
|
"embed": [
|
|
21
21
|
"./types/embed.d.cts"
|
|
22
|
+
],
|
|
23
|
+
"native": [
|
|
24
|
+
"./types/native.d.cts"
|
|
22
25
|
]
|
|
23
26
|
}
|
|
24
27
|
},
|
|
@@ -44,6 +47,17 @@
|
|
|
44
47
|
"default": "./embed.cjs"
|
|
45
48
|
},
|
|
46
49
|
"default": "./embed.js"
|
|
50
|
+
},
|
|
51
|
+
"./native": {
|
|
52
|
+
"import": {
|
|
53
|
+
"types": "./types/native.d.ts",
|
|
54
|
+
"default": "./native.js"
|
|
55
|
+
},
|
|
56
|
+
"require": {
|
|
57
|
+
"types": "./types/native.d.cts",
|
|
58
|
+
"default": "./native.cjs"
|
|
59
|
+
},
|
|
60
|
+
"default": "./native.js"
|
|
47
61
|
}
|
|
48
62
|
},
|
|
49
63
|
"files": [
|
|
@@ -55,12 +69,23 @@
|
|
|
55
69
|
"README.md",
|
|
56
70
|
"CHANGELOG.md"
|
|
57
71
|
],
|
|
72
|
+
"dependencies": {
|
|
73
|
+
"react-native-url-polyfill": "2.0.0"
|
|
74
|
+
},
|
|
58
75
|
"peerDependencies": {
|
|
59
|
-
"react": ">=18"
|
|
76
|
+
"react": ">=18",
|
|
77
|
+
"react-native": ">=0.78",
|
|
78
|
+
"react-native-webview": ">=13"
|
|
60
79
|
},
|
|
61
80
|
"peerDependenciesMeta": {
|
|
62
81
|
"react": {
|
|
63
82
|
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"react-native": {
|
|
85
|
+
"optional": true
|
|
86
|
+
},
|
|
87
|
+
"react-native-webview": {
|
|
88
|
+
"optional": true
|
|
64
89
|
}
|
|
65
90
|
}
|
|
66
91
|
}
|
package/types/host.d.cts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
text: string;
|
|
5
|
-
}
|
|
1
|
+
import { type GrizzleeShare } from "./protocol.cjs";
|
|
2
|
+
export { embedUrl } from "./protocol.cjs";
|
|
3
|
+
export type { GrizzleeShare } from "./protocol.cjs";
|
|
6
4
|
export interface GrizzleeEmbedOptions {
|
|
7
5
|
/** An absolute HTTP or HTTPS start URL minted by the merchant's server. */
|
|
8
6
|
url: string;
|
|
@@ -17,6 +15,5 @@ export interface GrizzleeEmbedOptions {
|
|
|
17
15
|
minHeight?: number;
|
|
18
16
|
}
|
|
19
17
|
export declare function minimumHeight(height?: number): number;
|
|
20
|
-
export declare function embedUrl(url: string): URL;
|
|
21
18
|
/** Both hosts use this listener so origin checks and renewal behave the same way. */
|
|
22
19
|
export declare function listenToEmbed(frame: HTMLIFrameElement, options: () => GrizzleeEmbedOptions): () => void;
|
package/types/host.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
text: string;
|
|
5
|
-
}
|
|
1
|
+
import { type GrizzleeShare } from "./protocol.js";
|
|
2
|
+
export { embedUrl } from "./protocol.js";
|
|
3
|
+
export type { GrizzleeShare } from "./protocol.js";
|
|
6
4
|
export interface GrizzleeEmbedOptions {
|
|
7
5
|
/** An absolute HTTP or HTTPS start URL minted by the merchant's server. */
|
|
8
6
|
url: string;
|
|
@@ -17,6 +15,5 @@ export interface GrizzleeEmbedOptions {
|
|
|
17
15
|
minHeight?: number;
|
|
18
16
|
}
|
|
19
17
|
export declare function minimumHeight(height?: number): number;
|
|
20
|
-
export declare function embedUrl(url: string): URL;
|
|
21
18
|
/** Both hosts use this listener so origin checks and renewal behave the same way. */
|
|
22
19
|
export declare function listenToEmbed(frame: HTMLIFrameElement, options: () => GrizzleeEmbedOptions): () => void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
import { type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
import { type GrizzleeShare } from "./protocol.cjs";
|
|
4
|
+
export type { GrizzleeShare } from "./protocol.cjs";
|
|
5
|
+
export interface GrizzleeNativeEmbedProps {
|
|
6
|
+
/** A one-use /embed/start URL minted by the merchant's server. */
|
|
7
|
+
startUrl: string;
|
|
8
|
+
/** Return a fresh start URL, or update startUrl in your app. */
|
|
9
|
+
onExpired?: () => string | void | Promise<string | void>;
|
|
10
|
+
onReady?: () => void;
|
|
11
|
+
/** Without this callback, the component opens React Native's share sheet. */
|
|
12
|
+
onShare?: (share: GrizzleeShare) => void | Promise<void>;
|
|
13
|
+
/** Loading, renewal and sharing errors. */
|
|
14
|
+
onError?: (error: Error) => void;
|
|
15
|
+
style?: StyleProp<ViewStyle>;
|
|
16
|
+
}
|
|
17
|
+
/** Render the dashboard directly in a WebView. No merchant wrapper page is needed. */
|
|
18
|
+
export declare function GrizzleeNativeEmbed(props: GrizzleeNativeEmbedProps): ReactElement;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
import { type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
import { type GrizzleeShare } from "./protocol.js";
|
|
4
|
+
export type { GrizzleeShare } from "./protocol.js";
|
|
5
|
+
export interface GrizzleeNativeEmbedProps {
|
|
6
|
+
/** A one-use /embed/start URL minted by the merchant's server. */
|
|
7
|
+
startUrl: string;
|
|
8
|
+
/** Return a fresh start URL, or update startUrl in your app. */
|
|
9
|
+
onExpired?: () => string | void | Promise<string | void>;
|
|
10
|
+
onReady?: () => void;
|
|
11
|
+
/** Without this callback, the component opens React Native's share sheet. */
|
|
12
|
+
onShare?: (share: GrizzleeShare) => void | Promise<void>;
|
|
13
|
+
/** Loading, renewal and sharing errors. */
|
|
14
|
+
onError?: (error: Error) => void;
|
|
15
|
+
style?: StyleProp<ViewStyle>;
|
|
16
|
+
}
|
|
17
|
+
/** Render the dashboard directly in a WebView. No merchant wrapper page is needed. */
|
|
18
|
+
export declare function GrizzleeNativeEmbed(props: GrizzleeNativeEmbedProps): ReactElement;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface GrizzleeShare {
|
|
2
|
+
programId: string;
|
|
3
|
+
url: string;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function embedUrl(url: string, Url?: new (value: string) => URL): URL;
|
|
7
|
+
export declare function parseShare(message: object, validateUrl?: (value: string) => unknown): GrizzleeShare | null;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface GrizzleeShare {
|
|
2
|
+
programId: string;
|
|
3
|
+
url: string;
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function embedUrl(url: string, Url?: new (value: string) => URL): URL;
|
|
7
|
+
export declare function parseShare(message: object, validateUrl?: (value: string) => unknown): GrizzleeShare | null;
|