@usethrottle/checkout-react 0.1.0 → 0.2.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/README.md +114 -13
- package/dist/index.cjs +140 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +111 -20
- package/dist/index.d.ts +111 -20
- package/dist/index.js +135 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
# @usethrottle/checkout-react
|
|
2
2
|
|
|
3
|
-
React
|
|
3
|
+
React components for embedding Throttle checkout in your site. Two embed
|
|
4
|
+
products + one helper hook:
|
|
5
|
+
|
|
6
|
+
- **`<PaymentEmbed/>`** — payment-only iframe. Drops a Gr4vy-backed
|
|
7
|
+
card form into your existing checkout page.
|
|
8
|
+
- **`<CheckoutEmbed/>`** — full-checkout iframe. Address + shipping +
|
|
9
|
+
payment. Single iframe, single drop-in.
|
|
10
|
+
- **`useThrottleEvents()`** — hook for parents who build their own
|
|
11
|
+
iframe wrapper but want Throttle's strict postMessage validation.
|
|
4
12
|
|
|
5
13
|
## Install
|
|
6
14
|
|
|
@@ -8,34 +16,127 @@ React component for embedding Throttle checkout in your app.
|
|
|
8
16
|
npm install @usethrottle/checkout-react
|
|
9
17
|
```
|
|
10
18
|
|
|
11
|
-
##
|
|
19
|
+
## One-time setup: allow your site's origin
|
|
20
|
+
|
|
21
|
+
Each embed posts events to your page over `postMessage` with a strict
|
|
22
|
+
`targetOrigin`. Your origin must be on the merchant's allowlist:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
throttle embed-config set --origins https://shop.example.com
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or via API:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
fetch('https://throttle-api-gff1.onrender.com/api/v1/embed-config', {
|
|
32
|
+
method: 'PUT',
|
|
33
|
+
headers: { 'x-api-key': 'sk_...', 'content-type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ allowed_origins: ['https://shop.example.com'] }),
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## `<PaymentEmbed/>` — payment-only
|
|
12
39
|
|
|
13
40
|
```tsx
|
|
14
|
-
import {
|
|
41
|
+
import { PaymentEmbed } from '@usethrottle/checkout-react';
|
|
15
42
|
|
|
16
43
|
export function CheckoutPage({ sessionId }: { sessionId: string }) {
|
|
17
44
|
return (
|
|
18
|
-
<
|
|
45
|
+
<PaymentEmbed
|
|
19
46
|
sessionId={sessionId}
|
|
20
|
-
|
|
47
|
+
parentOrigin="https://shop.example.com"
|
|
48
|
+
primary="#ff6b00"
|
|
49
|
+
onReady={() => console.log('ready')}
|
|
50
|
+
onProcessing={() => console.log('paying...')}
|
|
51
|
+
onSucceeded={({ orderId, paymentId }) => {
|
|
21
52
|
// navigate to thank-you page, clear cart, etc.
|
|
53
|
+
window.location.href = `/orders/${orderId}`;
|
|
22
54
|
}}
|
|
23
|
-
|
|
55
|
+
onFailed={({ code, message }) => {
|
|
24
56
|
// surface to user
|
|
25
57
|
}}
|
|
58
|
+
onCanceled={() => {
|
|
59
|
+
// user dismissed
|
|
60
|
+
}}
|
|
26
61
|
/>
|
|
27
62
|
);
|
|
28
63
|
}
|
|
29
64
|
```
|
|
30
65
|
|
|
31
|
-
|
|
66
|
+
## `<CheckoutEmbed/>` — full checkout
|
|
67
|
+
|
|
68
|
+
```tsx
|
|
69
|
+
import { CheckoutEmbed } from '@usethrottle/checkout-react';
|
|
70
|
+
|
|
71
|
+
export function CheckoutPage({ sessionId }: { sessionId: string }) {
|
|
72
|
+
return (
|
|
73
|
+
<CheckoutEmbed
|
|
74
|
+
sessionId={sessionId}
|
|
75
|
+
parentOrigin="https://shop.example.com"
|
|
76
|
+
primary="#ff6b00"
|
|
77
|
+
logo="https://shop.example.com/logo.png"
|
|
78
|
+
onSucceeded={({ orderId, paymentId }) => { /* ... */ }}
|
|
79
|
+
onStepChanged={({ step }) => {
|
|
80
|
+
// step: 'cart' | 'address' | 'shipping' | 'payment'
|
|
81
|
+
}}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## `useThrottleEvents` — DIY iframe wrapper
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
import { useThrottleEvents, type ThrottleMessage } from '@usethrottle/checkout-react';
|
|
91
|
+
import { useRef } from 'react';
|
|
92
|
+
|
|
93
|
+
export function MyEmbed({ sessionId }: { sessionId: string }) {
|
|
94
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
95
|
+
const baseUrl = 'https://throttle-checkout.vercel.app';
|
|
96
|
+
|
|
97
|
+
useThrottleEvents({
|
|
98
|
+
iframeRef,
|
|
99
|
+
expectedOrigin: new URL(baseUrl).origin,
|
|
100
|
+
onMessage: (msg: ThrottleMessage) => {
|
|
101
|
+
if (msg.type === 'throttle.completed') console.log('paid', msg.orderId);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const src = `${baseUrl}/c/${sessionId}?embed=1&mode=payment-only&parentOrigin=${window.location.origin}`;
|
|
106
|
+
return <iframe ref={iframeRef} src={src} allow="payment *" />;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
32
109
|
|
|
33
110
|
## Events
|
|
34
111
|
|
|
35
|
-
|
|
112
|
+
Both `<PaymentEmbed/>` and `<CheckoutEmbed/>` fire these events to the
|
|
113
|
+
parent page over `postMessage`. They're complementary to Throttle's
|
|
114
|
+
outbound webhooks (which fire from Throttle's servers to your backend
|
|
115
|
+
over signed HTTP).
|
|
116
|
+
|
|
117
|
+
| Event | When | Callback |
|
|
36
118
|
|---|---|---|
|
|
37
|
-
| `throttle.ready` | `onReady` |
|
|
38
|
-
| `throttle.
|
|
39
|
-
| `throttle.completed` |
|
|
40
|
-
| `throttle.
|
|
41
|
-
| `throttle.
|
|
119
|
+
| `throttle.ready` | iframe mounted | `onReady()` |
|
|
120
|
+
| `throttle.processing` | user clicked Pay; awaiting auth | `onProcessing()` |
|
|
121
|
+
| `throttle.completed` | payment captured | `onSucceeded({orderId, paymentId})` |
|
|
122
|
+
| `throttle.error` | card declined / network failure | `onFailed({code, message})` |
|
|
123
|
+
| `throttle.cancelled` | user dismissed | `onCanceled()` |
|
|
124
|
+
| `throttle.step.changed` | (CheckoutEmbed) step transition | `onStepChanged({step})` |
|
|
125
|
+
| `throttle.resize` | iframe content resized | (auto: updates iframe height) |
|
|
126
|
+
|
|
127
|
+
Every message is wrapped in
|
|
128
|
+
`{ source: 'throttle', version: 1, ...event }` so your page can
|
|
129
|
+
disambiguate Throttle messages from other senders. The hook validates
|
|
130
|
+
origin, source window, and envelope before invoking your callback.
|
|
131
|
+
|
|
132
|
+
## Backwards-compat alias: `<ThrottleCheckout/>`
|
|
133
|
+
|
|
134
|
+
`ThrottleCheckout` is a v0.1 alias that resolves to `CheckoutEmbed`.
|
|
135
|
+
The callback shape changed in v0.2: `onCompleted(orderId, paymentId)`
|
|
136
|
+
is now `onSucceeded({orderId, paymentId})` and `onError(code, message)`
|
|
137
|
+
is now `onFailed({code, message})`. Update call sites accordingly.
|
|
138
|
+
|
|
139
|
+
## See also
|
|
140
|
+
|
|
141
|
+
- [CLI: `throttle embed-config`](https://www.npmjs.com/package/@usethrottle/cli)
|
|
142
|
+
- [API client: `@usethrottle/api-client`](https://www.npmjs.com/package/@usethrottle/api-client)
|
package/dist/index.cjs
CHANGED
|
@@ -20,43 +20,89 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.tsx
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
|
|
23
|
+
CheckoutEmbed: () => CheckoutEmbed,
|
|
24
|
+
PaymentEmbed: () => PaymentEmbed,
|
|
25
|
+
ThrottleCheckout: () => CheckoutEmbed,
|
|
26
|
+
useThrottleEvents: () => useThrottleEvents
|
|
24
27
|
});
|
|
25
28
|
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/CheckoutEmbed.tsx
|
|
31
|
+
var import_react2 = require("react");
|
|
32
|
+
|
|
33
|
+
// src/useThrottleEvents.ts
|
|
26
34
|
var import_react = require("react");
|
|
35
|
+
function useThrottleEvents({ iframeRef, expectedOrigin, onMessage }) {
|
|
36
|
+
(0, import_react.useEffect)(() => {
|
|
37
|
+
function handle(ev) {
|
|
38
|
+
if (ev.origin !== expectedOrigin) return;
|
|
39
|
+
if (iframeRef.current && ev.source !== iframeRef.current.contentWindow) return;
|
|
40
|
+
const data = ev.data;
|
|
41
|
+
if (!data || typeof data !== "object") return;
|
|
42
|
+
if (data.source !== "throttle" || data.version !== 1) return;
|
|
43
|
+
if (typeof data.type !== "string" || !data.type.startsWith("throttle.")) return;
|
|
44
|
+
onMessage(data);
|
|
45
|
+
}
|
|
46
|
+
window.addEventListener("message", handle);
|
|
47
|
+
return () => window.removeEventListener("message", handle);
|
|
48
|
+
}, [iframeRef, expectedOrigin, onMessage]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/CheckoutEmbed.tsx
|
|
27
52
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
28
|
-
var DEFAULT_BASE_URL = "https://throttle-checkout
|
|
29
|
-
function
|
|
53
|
+
var DEFAULT_BASE_URL = "https://throttle-checkout.vercel.app";
|
|
54
|
+
function CheckoutEmbed({
|
|
30
55
|
sessionId,
|
|
56
|
+
parentOrigin,
|
|
31
57
|
baseUrl = DEFAULT_BASE_URL,
|
|
32
|
-
|
|
58
|
+
primary,
|
|
59
|
+
logo,
|
|
60
|
+
initialHeight = 600,
|
|
33
61
|
className,
|
|
34
62
|
style,
|
|
35
63
|
onReady,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
64
|
+
onProcessing,
|
|
65
|
+
onSucceeded,
|
|
66
|
+
onFailed,
|
|
67
|
+
onCanceled,
|
|
68
|
+
onStepChanged
|
|
39
69
|
}) {
|
|
40
|
-
const iframeRef = (0,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
70
|
+
const iframeRef = (0, import_react2.useRef)(null);
|
|
71
|
+
const expectedOrigin = new URL(baseUrl).origin;
|
|
72
|
+
const dispatch = (0, import_react2.useCallback)(
|
|
73
|
+
(msg) => {
|
|
74
|
+
switch (msg.type) {
|
|
75
|
+
case "throttle.ready":
|
|
76
|
+
onReady?.();
|
|
77
|
+
break;
|
|
78
|
+
case "throttle.processing":
|
|
79
|
+
onProcessing?.();
|
|
80
|
+
break;
|
|
81
|
+
case "throttle.completed":
|
|
82
|
+
onSucceeded?.({ orderId: msg.orderId, paymentId: msg.paymentId });
|
|
83
|
+
break;
|
|
84
|
+
case "throttle.error":
|
|
85
|
+
onFailed?.({ code: msg.code, message: msg.message });
|
|
86
|
+
break;
|
|
87
|
+
case "throttle.cancelled":
|
|
88
|
+
onCanceled?.();
|
|
89
|
+
break;
|
|
90
|
+
case "throttle.step.changed":
|
|
91
|
+
onStepChanged?.({ step: msg.step });
|
|
92
|
+
break;
|
|
93
|
+
case "throttle.resize":
|
|
94
|
+
if (iframeRef.current) iframeRef.current.style.height = `${msg.height}px`;
|
|
95
|
+
break;
|
|
49
96
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}, [onReady, onCompleted, onCancelled, onError]);
|
|
97
|
+
},
|
|
98
|
+
[onReady, onProcessing, onSucceeded, onFailed, onCanceled, onStepChanged]
|
|
99
|
+
);
|
|
100
|
+
useThrottleEvents({ iframeRef, expectedOrigin, onMessage: dispatch });
|
|
101
|
+
const params = new URLSearchParams({ embed: "1", mode: "checkout-full", parentOrigin });
|
|
102
|
+
if (primary) params.set("primary", primary);
|
|
103
|
+
if (logo) params.set("logo", logo);
|
|
58
104
|
const cleanBase = baseUrl.replace(/\/$/, "");
|
|
59
|
-
const src = `${cleanBase}/c/${encodeURIComponent(sessionId)}
|
|
105
|
+
const src = `${cleanBase}/c/${encodeURIComponent(sessionId)}?${params.toString()}`;
|
|
60
106
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
61
107
|
"iframe",
|
|
62
108
|
{
|
|
@@ -69,8 +115,76 @@ function ThrottleCheckout({
|
|
|
69
115
|
}
|
|
70
116
|
);
|
|
71
117
|
}
|
|
118
|
+
|
|
119
|
+
// src/PaymentEmbed.tsx
|
|
120
|
+
var import_react3 = require("react");
|
|
121
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
122
|
+
var DEFAULT_BASE_URL2 = "https://throttle-checkout.vercel.app";
|
|
123
|
+
function PaymentEmbed({
|
|
124
|
+
sessionId,
|
|
125
|
+
parentOrigin,
|
|
126
|
+
baseUrl = DEFAULT_BASE_URL2,
|
|
127
|
+
primary,
|
|
128
|
+
logo,
|
|
129
|
+
initialHeight = 480,
|
|
130
|
+
className,
|
|
131
|
+
style,
|
|
132
|
+
onReady,
|
|
133
|
+
onProcessing,
|
|
134
|
+
onSucceeded,
|
|
135
|
+
onFailed,
|
|
136
|
+
onCanceled
|
|
137
|
+
}) {
|
|
138
|
+
const iframeRef = (0, import_react3.useRef)(null);
|
|
139
|
+
const expectedOrigin = new URL(baseUrl).origin;
|
|
140
|
+
const dispatch = (0, import_react3.useCallback)(
|
|
141
|
+
(msg) => {
|
|
142
|
+
switch (msg.type) {
|
|
143
|
+
case "throttle.ready":
|
|
144
|
+
onReady?.();
|
|
145
|
+
break;
|
|
146
|
+
case "throttle.processing":
|
|
147
|
+
onProcessing?.();
|
|
148
|
+
break;
|
|
149
|
+
case "throttle.completed":
|
|
150
|
+
onSucceeded?.({ orderId: msg.orderId, paymentId: msg.paymentId });
|
|
151
|
+
break;
|
|
152
|
+
case "throttle.error":
|
|
153
|
+
onFailed?.({ code: msg.code, message: msg.message });
|
|
154
|
+
break;
|
|
155
|
+
case "throttle.cancelled":
|
|
156
|
+
onCanceled?.();
|
|
157
|
+
break;
|
|
158
|
+
case "throttle.resize":
|
|
159
|
+
if (iframeRef.current) iframeRef.current.style.height = `${msg.height}px`;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
[onReady, onProcessing, onSucceeded, onFailed, onCanceled]
|
|
164
|
+
);
|
|
165
|
+
useThrottleEvents({ iframeRef, expectedOrigin, onMessage: dispatch });
|
|
166
|
+
const params = new URLSearchParams({ embed: "1", mode: "payment-only", parentOrigin });
|
|
167
|
+
if (primary) params.set("primary", primary);
|
|
168
|
+
if (logo) params.set("logo", logo);
|
|
169
|
+
const cleanBase = baseUrl.replace(/\/$/, "");
|
|
170
|
+
const src = `${cleanBase}/c/${encodeURIComponent(sessionId)}?${params.toString()}`;
|
|
171
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
172
|
+
"iframe",
|
|
173
|
+
{
|
|
174
|
+
ref: iframeRef,
|
|
175
|
+
src,
|
|
176
|
+
className,
|
|
177
|
+
style: { border: 0, width: "100%", minHeight: initialHeight, ...style },
|
|
178
|
+
allow: "payment *",
|
|
179
|
+
title: "Throttle payment"
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
}
|
|
72
183
|
// Annotate the CommonJS export names for ESM import in node:
|
|
73
184
|
0 && (module.exports = {
|
|
74
|
-
|
|
185
|
+
CheckoutEmbed,
|
|
186
|
+
PaymentEmbed,
|
|
187
|
+
ThrottleCheckout,
|
|
188
|
+
useThrottleEvents
|
|
75
189
|
});
|
|
76
190
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.tsx"],"sourcesContent":["import { useEffect, useRef } from 'react';\n\nexport interface ThrottleCheckoutProps {\n /** The throttle checkout session id (sess_*). Required. */\n sessionId: string;\n /** Base URL of the deployed checkout-web app. Defaults to https://throttle-checkout-web.onrender.com */\n baseUrl?: string;\n /** Initial iframe height in pixels. Auto-resizes on throttle.resize events. */\n initialHeight?: number;\n /** Optional className for the iframe. */\n className?: string;\n /** Optional inline style for the iframe. */\n style?: React.CSSProperties;\n /** Fires once the iframe is ready to receive interactions. */\n onReady?: () => void;\n /** Fires after a successful payment. */\n onCompleted?: (orderId: string, paymentId: string) => void;\n /** Fires when the buyer cancels checkout. */\n onCancelled?: () => void;\n /** Fires on any error inside the iframe (Gr4vy decline, network, etc.). */\n onError?: (code: string, message: string) => void;\n}\n\nconst DEFAULT_BASE_URL = 'https://throttle-checkout-web.onrender.com';\n\n/**\n * Embedded Throttle checkout for React apps.\n *\n * Renders an iframe pointed at <baseUrl>/c/<sessionId>?embed=1 and\n * subscribes to the postMessage v1 protocol. Events fire on the\n * provided callbacks. Auto-resizes the iframe on throttle.resize.\n */\nexport function ThrottleCheckout({\n sessionId,\n baseUrl = DEFAULT_BASE_URL,\n initialHeight = 480,\n className,\n style,\n onReady,\n onCompleted,\n onCancelled,\n onError,\n}: ThrottleCheckoutProps) {\n const iframeRef = useRef<HTMLIFrameElement>(null);\n\n useEffect(() => {\n function handle(ev: MessageEvent) {\n const data = ev?.data;\n if (!data || typeof data !== 'object' || typeof data.type !== 'string') return;\n if (!data.type.startsWith('throttle.')) return;\n if (iframeRef.current && ev.source !== iframeRef.current.contentWindow) return;\n\n if (data.type === 'throttle.resize' && typeof data.height === 'number' && iframeRef.current) {\n iframeRef.current.style.height = `${data.height}px`;\n }\n if (data.type === 'throttle.ready') onReady?.();\n else if (data.type === 'throttle.completed') onCompleted?.(data.orderId, data.paymentId);\n else if (data.type === 'throttle.cancelled') onCancelled?.();\n else if (data.type === 'throttle.error') onError?.(data.code, data.message);\n }\n window.addEventListener('message', handle);\n return () => window.removeEventListener('message', handle);\n }, [onReady, onCompleted, onCancelled, onError]);\n\n const cleanBase = baseUrl.replace(/\\/$/, '');\n const src = `${cleanBase}/c/${encodeURIComponent(sessionId)}?embed=1`;\n\n return (\n <iframe\n ref={iframeRef}\n src={src}\n className={className}\n style={{ border: 0, width: '100%', minHeight: initialHeight, ...style }}\n allow=\"payment *\"\n title=\"Throttle checkout\"\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mBAAkC;AAoE9B;AA7CJ,IAAM,mBAAmB;AASlB,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA,UAAU;AAAA,EACV,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA0B;AACxB,QAAM,gBAAY,qBAA0B,IAAI;AAEhD,8BAAU,MAAM;AACd,aAAS,OAAO,IAAkB;AAChC,YAAM,OAAO,IAAI;AACjB,UAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,OAAO,KAAK,SAAS,SAAU;AACxE,UAAI,CAAC,KAAK,KAAK,WAAW,WAAW,EAAG;AACxC,UAAI,UAAU,WAAW,GAAG,WAAW,UAAU,QAAQ,cAAe;AAExE,UAAI,KAAK,SAAS,qBAAqB,OAAO,KAAK,WAAW,YAAY,UAAU,SAAS;AAC3F,kBAAU,QAAQ,MAAM,SAAS,GAAG,KAAK,MAAM;AAAA,MACjD;AACA,UAAI,KAAK,SAAS,iBAAkB,WAAU;AAAA,eACrC,KAAK,SAAS,qBAAsB,eAAc,KAAK,SAAS,KAAK,SAAS;AAAA,eAC9E,KAAK,SAAS,qBAAsB,eAAc;AAAA,eAClD,KAAK,SAAS,iBAAkB,WAAU,KAAK,MAAM,KAAK,OAAO;AAAA,IAC5E;AACA,WAAO,iBAAiB,WAAW,MAAM;AACzC,WAAO,MAAM,OAAO,oBAAoB,WAAW,MAAM;AAAA,EAC3D,GAAG,CAAC,SAAS,aAAa,aAAa,OAAO,CAAC;AAE/C,QAAM,YAAY,QAAQ,QAAQ,OAAO,EAAE;AAC3C,QAAM,MAAM,GAAG,SAAS,MAAM,mBAAmB,SAAS,CAAC;AAE3D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA,OAAO,EAAE,QAAQ,GAAG,OAAO,QAAQ,WAAW,eAAe,GAAG,MAAM;AAAA,MACtE,OAAM;AAAA,MACN,OAAM;AAAA;AAAA,EACR;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.tsx","../src/CheckoutEmbed.tsx","../src/useThrottleEvents.ts","../src/PaymentEmbed.tsx"],"sourcesContent":["export { CheckoutEmbed } from './CheckoutEmbed.js';\nexport { PaymentEmbed } from './PaymentEmbed.js';\nexport { useThrottleEvents } from './useThrottleEvents.js';\nexport type {\n ThrottleEvent,\n ThrottleMessage,\n ThrottleEnvelope,\n ThrottleEmbedProps,\n CheckoutEmbedExtraProps,\n} from './types.js';\n\n// v0.1.x compat alias. v0.1's <ThrottleCheckout> shape (positional\n// onCompleted/onError args, optional baseUrl, no parentOrigin) is\n// gone in v0.2 — the alias resolves to <CheckoutEmbed> which now\n// requires `parentOrigin`. Update call sites accordingly.\nexport { CheckoutEmbed as ThrottleCheckout } from './CheckoutEmbed.js';\n","import { useCallback, useRef } from 'react';\nimport { useThrottleEvents } from './useThrottleEvents.js';\nimport type {\n ThrottleEmbedProps,\n CheckoutEmbedExtraProps,\n ThrottleMessage,\n} from './types.js';\n\nconst DEFAULT_BASE_URL = 'https://throttle-checkout.vercel.app';\n\n/**\n * Full-checkout iframe embed. Renders the entire Throttle hosted\n * checkout (address / shipping / payment) inside an iframe and\n * surfaces lifecycle + terminal events through callbacks. The\n * iframe auto-resizes on `throttle.resize` events.\n *\n * `parentOrigin` must match an entry in the merchant's allow-list\n * (`throttle embed-config set --origins ...`). Mismatches render\n * an in-iframe \"Embed not authorized\" panel.\n */\nexport function CheckoutEmbed({\n sessionId,\n parentOrigin,\n baseUrl = DEFAULT_BASE_URL,\n primary,\n logo,\n initialHeight = 600,\n className,\n style,\n onReady,\n onProcessing,\n onSucceeded,\n onFailed,\n onCanceled,\n onStepChanged,\n}: ThrottleEmbedProps & CheckoutEmbedExtraProps) {\n const iframeRef = useRef<HTMLIFrameElement>(null);\n const expectedOrigin = new URL(baseUrl).origin;\n\n const dispatch = useCallback(\n (msg: ThrottleMessage) => {\n switch (msg.type) {\n case 'throttle.ready':\n onReady?.();\n break;\n case 'throttle.processing':\n onProcessing?.();\n break;\n case 'throttle.completed':\n onSucceeded?.({ orderId: msg.orderId, paymentId: msg.paymentId });\n break;\n case 'throttle.error':\n onFailed?.({ code: msg.code, message: msg.message });\n break;\n case 'throttle.cancelled':\n onCanceled?.();\n break;\n case 'throttle.step.changed':\n onStepChanged?.({ step: msg.step });\n break;\n case 'throttle.resize':\n if (iframeRef.current) iframeRef.current.style.height = `${msg.height}px`;\n break;\n }\n },\n [onReady, onProcessing, onSucceeded, onFailed, onCanceled, onStepChanged],\n );\n\n useThrottleEvents({ iframeRef, expectedOrigin, onMessage: dispatch });\n\n const params = new URLSearchParams({ embed: '1', mode: 'checkout-full', parentOrigin });\n if (primary) params.set('primary', primary);\n if (logo) params.set('logo', logo);\n const cleanBase = baseUrl.replace(/\\/$/, '');\n const src = `${cleanBase}/c/${encodeURIComponent(sessionId)}?${params.toString()}`;\n\n return (\n <iframe\n ref={iframeRef}\n src={src}\n className={className}\n style={{ border: 0, width: '100%', minHeight: initialHeight, ...style }}\n allow=\"payment *\"\n title=\"Throttle checkout\"\n />\n );\n}\n","import { useEffect, type RefObject } from 'react';\nimport type { ThrottleMessage } from './types.js';\n\nexport interface UseThrottleEventsOptions {\n iframeRef: RefObject<HTMLIFrameElement | null>;\n /**\n * The Throttle origin (e.g. https://throttle-checkout.vercel.app).\n * Strict-equals check against `MessageEvent.origin`. Anything else\n * is silently dropped.\n */\n expectedOrigin: string;\n onMessage: (msg: ThrottleMessage) => void;\n}\n\n/**\n * Subscribe to validated postMessage events from a Throttle iframe.\n *\n * Validation:\n * - event.origin === expectedOrigin (strict string match)\n * - event.source === iframeRef.current.contentWindow (defends against\n * other iframes on the same origin spoofing events)\n * - event.data.source === 'throttle' && event.data.version === 1\n * - event.data.type starts with 'throttle.'\n *\n * Anything failing validation is silently dropped — the parent page\n * may receive postMessages from analytics SDKs, browser extensions,\n * other iframes, etc. We must never throw on those.\n */\nexport function useThrottleEvents({ iframeRef, expectedOrigin, onMessage }: UseThrottleEventsOptions) {\n useEffect(() => {\n function handle(ev: MessageEvent) {\n if (ev.origin !== expectedOrigin) return;\n if (iframeRef.current && ev.source !== iframeRef.current.contentWindow) return;\n const data = ev.data as Partial<ThrottleMessage> | null | undefined;\n if (!data || typeof data !== 'object') return;\n if (data.source !== 'throttle' || data.version !== 1) return;\n if (typeof data.type !== 'string' || !data.type.startsWith('throttle.')) return;\n onMessage(data as ThrottleMessage);\n }\n window.addEventListener('message', handle);\n return () => window.removeEventListener('message', handle);\n }, [iframeRef, expectedOrigin, onMessage]);\n}\n","import { useCallback, useRef } from 'react';\nimport { useThrottleEvents } from './useThrottleEvents.js';\nimport type { ThrottleEmbedProps, ThrottleMessage } from './types.js';\n\nconst DEFAULT_BASE_URL = 'https://throttle-checkout.vercel.app';\n\n/**\n * Payment-only iframe embed. Renders just the Gr4vy provider iframe\n * (no address / shipping / cart UI). Email is collected by the Gr4vy\n * iframe in proxy-mode sessions. Used when the merchant has built\n * their own checkout UI and only needs Throttle to handle payment\n * authorization + capture.\n *\n * `parentOrigin` must match an entry in the merchant's allow-list.\n * The iframe auto-resizes on `throttle.resize` events.\n */\nexport function PaymentEmbed({\n sessionId,\n parentOrigin,\n baseUrl = DEFAULT_BASE_URL,\n primary,\n logo,\n initialHeight = 480,\n className,\n style,\n onReady,\n onProcessing,\n onSucceeded,\n onFailed,\n onCanceled,\n}: ThrottleEmbedProps) {\n const iframeRef = useRef<HTMLIFrameElement>(null);\n const expectedOrigin = new URL(baseUrl).origin;\n\n const dispatch = useCallback(\n (msg: ThrottleMessage) => {\n switch (msg.type) {\n case 'throttle.ready':\n onReady?.();\n break;\n case 'throttle.processing':\n onProcessing?.();\n break;\n case 'throttle.completed':\n onSucceeded?.({ orderId: msg.orderId, paymentId: msg.paymentId });\n break;\n case 'throttle.error':\n onFailed?.({ code: msg.code, message: msg.message });\n break;\n case 'throttle.cancelled':\n onCanceled?.();\n break;\n case 'throttle.resize':\n if (iframeRef.current) iframeRef.current.style.height = `${msg.height}px`;\n break;\n }\n },\n [onReady, onProcessing, onSucceeded, onFailed, onCanceled],\n );\n\n useThrottleEvents({ iframeRef, expectedOrigin, onMessage: dispatch });\n\n const params = new URLSearchParams({ embed: '1', mode: 'payment-only', parentOrigin });\n if (primary) params.set('primary', primary);\n if (logo) params.set('logo', logo);\n const cleanBase = baseUrl.replace(/\\/$/, '');\n const src = `${cleanBase}/c/${encodeURIComponent(sessionId)}?${params.toString()}`;\n\n return (\n <iframe\n ref={iframeRef}\n src={src}\n className={className}\n style={{ border: 0, width: '100%', minHeight: initialHeight, ...style }}\n allow=\"payment *\"\n title=\"Throttle payment\"\n />\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAAAA,gBAAoC;;;ACApC,mBAA0C;AA4BnC,SAAS,kBAAkB,EAAE,WAAW,gBAAgB,UAAU,GAA6B;AACpG,8BAAU,MAAM;AACd,aAAS,OAAO,IAAkB;AAChC,UAAI,GAAG,WAAW,eAAgB;AAClC,UAAI,UAAU,WAAW,GAAG,WAAW,UAAU,QAAQ,cAAe;AACxE,YAAM,OAAO,GAAG;AAChB,UAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AACvC,UAAI,KAAK,WAAW,cAAc,KAAK,YAAY,EAAG;AACtD,UAAI,OAAO,KAAK,SAAS,YAAY,CAAC,KAAK,KAAK,WAAW,WAAW,EAAG;AACzE,gBAAU,IAAuB;AAAA,IACnC;AACA,WAAO,iBAAiB,WAAW,MAAM;AACzC,WAAO,MAAM,OAAO,oBAAoB,WAAW,MAAM;AAAA,EAC3D,GAAG,CAAC,WAAW,gBAAgB,SAAS,CAAC;AAC3C;;;ADmCI;AArEJ,IAAM,mBAAmB;AAYlB,SAAS,cAAc;AAAA,EAC5B;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAiD;AAC/C,QAAM,gBAAY,sBAA0B,IAAI;AAChD,QAAM,iBAAiB,IAAI,IAAI,OAAO,EAAE;AAExC,QAAM,eAAW;AAAA,IACf,CAAC,QAAyB;AACxB,cAAQ,IAAI,MAAM;AAAA,QAChB,KAAK;AACH,oBAAU;AACV;AAAA,QACF,KAAK;AACH,yBAAe;AACf;AAAA,QACF,KAAK;AACH,wBAAc,EAAE,SAAS,IAAI,SAAS,WAAW,IAAI,UAAU,CAAC;AAChE;AAAA,QACF,KAAK;AACH,qBAAW,EAAE,MAAM,IAAI,MAAM,SAAS,IAAI,QAAQ,CAAC;AACnD;AAAA,QACF,KAAK;AACH,uBAAa;AACb;AAAA,QACF,KAAK;AACH,0BAAgB,EAAE,MAAM,IAAI,KAAK,CAAC;AAClC;AAAA,QACF,KAAK;AACH,cAAI,UAAU,QAAS,WAAU,QAAQ,MAAM,SAAS,GAAG,IAAI,MAAM;AACrE;AAAA,MACJ;AAAA,IACF;AAAA,IACA,CAAC,SAAS,cAAc,aAAa,UAAU,YAAY,aAAa;AAAA,EAC1E;AAEA,oBAAkB,EAAE,WAAW,gBAAgB,WAAW,SAAS,CAAC;AAEpE,QAAM,SAAS,IAAI,gBAAgB,EAAE,OAAO,KAAK,MAAM,iBAAiB,aAAa,CAAC;AACtF,MAAI,QAAS,QAAO,IAAI,WAAW,OAAO;AAC1C,MAAI,KAAM,QAAO,IAAI,QAAQ,IAAI;AACjC,QAAM,YAAY,QAAQ,QAAQ,OAAO,EAAE;AAC3C,QAAM,MAAM,GAAG,SAAS,MAAM,mBAAmB,SAAS,CAAC,IAAI,OAAO,SAAS,CAAC;AAEhF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA,OAAO,EAAE,QAAQ,GAAG,OAAO,QAAQ,WAAW,eAAe,GAAG,MAAM;AAAA,MACtE,OAAM;AAAA,MACN,OAAM;AAAA;AAAA,EACR;AAEJ;;;AEtFA,IAAAC,gBAAoC;AAqEhC,IAAAC,sBAAA;AAjEJ,IAAMC,oBAAmB;AAYlB,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA,UAAUA;AAAA,EACV;AAAA,EACA;AAAA,EACA,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAuB;AACrB,QAAM,gBAAY,sBAA0B,IAAI;AAChD,QAAM,iBAAiB,IAAI,IAAI,OAAO,EAAE;AAExC,QAAM,eAAW;AAAA,IACf,CAAC,QAAyB;AACxB,cAAQ,IAAI,MAAM;AAAA,QAChB,KAAK;AACH,oBAAU;AACV;AAAA,QACF,KAAK;AACH,yBAAe;AACf;AAAA,QACF,KAAK;AACH,wBAAc,EAAE,SAAS,IAAI,SAAS,WAAW,IAAI,UAAU,CAAC;AAChE;AAAA,QACF,KAAK;AACH,qBAAW,EAAE,MAAM,IAAI,MAAM,SAAS,IAAI,QAAQ,CAAC;AACnD;AAAA,QACF,KAAK;AACH,uBAAa;AACb;AAAA,QACF,KAAK;AACH,cAAI,UAAU,QAAS,WAAU,QAAQ,MAAM,SAAS,GAAG,IAAI,MAAM;AACrE;AAAA,MACJ;AAAA,IACF;AAAA,IACA,CAAC,SAAS,cAAc,aAAa,UAAU,UAAU;AAAA,EAC3D;AAEA,oBAAkB,EAAE,WAAW,gBAAgB,WAAW,SAAS,CAAC;AAEpE,QAAM,SAAS,IAAI,gBAAgB,EAAE,OAAO,KAAK,MAAM,gBAAgB,aAAa,CAAC;AACrF,MAAI,QAAS,QAAO,IAAI,WAAW,OAAO;AAC1C,MAAI,KAAM,QAAO,IAAI,QAAQ,IAAI;AACjC,QAAM,YAAY,QAAQ,QAAQ,OAAO,EAAE;AAC3C,QAAM,MAAM,GAAG,SAAS,MAAM,mBAAmB,SAAS,CAAC,IAAI,OAAO,SAAS,CAAC;AAEhF,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA,OAAO,EAAE,QAAQ,GAAG,OAAO,QAAQ,WAAW,eAAe,GAAG,MAAM;AAAA,MACtE,OAAM;AAAA,MACN,OAAM;AAAA;AAAA,EACR;AAEJ;","names":["import_react","import_react","import_jsx_runtime","DEFAULT_BASE_URL"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,32 +1,123 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { CSSProperties, RefObject } from 'react';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Envelope every Throttle iframe message carries. Lets parents
|
|
6
|
+
* disambiguate Throttle messages from other postMessage senders on the
|
|
7
|
+
* page (analytics SDKs, ad pixels, other iframes).
|
|
8
|
+
*/
|
|
9
|
+
interface ThrottleEnvelope {
|
|
10
|
+
source: 'throttle';
|
|
11
|
+
version: 1;
|
|
12
|
+
}
|
|
13
|
+
type ThrottleEvent = {
|
|
14
|
+
type: 'throttle.ready';
|
|
15
|
+
} | {
|
|
16
|
+
type: 'throttle.resize';
|
|
17
|
+
height: number;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'throttle.processing';
|
|
20
|
+
} | {
|
|
21
|
+
type: 'throttle.completed';
|
|
22
|
+
orderId: string;
|
|
23
|
+
paymentId: string;
|
|
24
|
+
} | {
|
|
25
|
+
type: 'throttle.cancelled';
|
|
26
|
+
} | {
|
|
27
|
+
type: 'throttle.error';
|
|
28
|
+
code: string;
|
|
29
|
+
message: string;
|
|
30
|
+
} | {
|
|
31
|
+
type: 'throttle.step.changed';
|
|
32
|
+
step: 'cart' | 'address' | 'shipping' | 'payment';
|
|
33
|
+
};
|
|
34
|
+
type ThrottleMessage = ThrottleEvent & ThrottleEnvelope;
|
|
35
|
+
interface ThrottleEmbedProps {
|
|
36
|
+
/** Throttle checkout session id (cs_*). Required. */
|
|
5
37
|
sessionId: string;
|
|
6
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Parent origin where this embed is mounted (e.g. https://shop.example.com).
|
|
40
|
+
* Must be in the merchant's allowed_origins list, set via
|
|
41
|
+
* `throttle embed-config set --origins ...` or PUT /api/v1/embed-config.
|
|
42
|
+
* Required.
|
|
43
|
+
*/
|
|
44
|
+
parentOrigin: string;
|
|
45
|
+
/** Base URL of the deployed checkout-web app. Defaults to https://throttle-checkout.vercel.app. */
|
|
7
46
|
baseUrl?: string;
|
|
8
|
-
/**
|
|
47
|
+
/** Optional brand color override (#rrggbb). */
|
|
48
|
+
primary?: string;
|
|
49
|
+
/** Optional logo URL override (https). */
|
|
50
|
+
logo?: string;
|
|
51
|
+
/** Initial iframe height in px. Auto-resizes on throttle.resize events. */
|
|
9
52
|
initialHeight?: number;
|
|
10
|
-
/** Optional className for the iframe. */
|
|
11
53
|
className?: string;
|
|
12
|
-
|
|
13
|
-
style?: React.CSSProperties;
|
|
14
|
-
/** Fires once the iframe is ready to receive interactions. */
|
|
54
|
+
style?: CSSProperties;
|
|
15
55
|
onReady?: () => void;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
56
|
+
onProcessing?: () => void;
|
|
57
|
+
onSucceeded?: (evt: {
|
|
58
|
+
orderId: string;
|
|
59
|
+
paymentId: string;
|
|
60
|
+
}) => void;
|
|
61
|
+
onFailed?: (evt: {
|
|
62
|
+
code: string;
|
|
63
|
+
message: string;
|
|
64
|
+
}) => void;
|
|
65
|
+
onCanceled?: () => void;
|
|
66
|
+
}
|
|
67
|
+
interface CheckoutEmbedExtraProps {
|
|
68
|
+
onStepChanged?: (evt: {
|
|
69
|
+
step: 'cart' | 'address' | 'shipping' | 'payment';
|
|
70
|
+
}) => void;
|
|
22
71
|
}
|
|
72
|
+
|
|
23
73
|
/**
|
|
24
|
-
*
|
|
74
|
+
* Full-checkout iframe embed. Renders the entire Throttle hosted
|
|
75
|
+
* checkout (address / shipping / payment) inside an iframe and
|
|
76
|
+
* surfaces lifecycle + terminal events through callbacks. The
|
|
77
|
+
* iframe auto-resizes on `throttle.resize` events.
|
|
78
|
+
*
|
|
79
|
+
* `parentOrigin` must match an entry in the merchant's allow-list
|
|
80
|
+
* (`throttle embed-config set --origins ...`). Mismatches render
|
|
81
|
+
* an in-iframe "Embed not authorized" panel.
|
|
82
|
+
*/
|
|
83
|
+
declare function CheckoutEmbed({ sessionId, parentOrigin, baseUrl, primary, logo, initialHeight, className, style, onReady, onProcessing, onSucceeded, onFailed, onCanceled, onStepChanged, }: ThrottleEmbedProps & CheckoutEmbedExtraProps): react_jsx_runtime.JSX.Element;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Payment-only iframe embed. Renders just the Gr4vy provider iframe
|
|
87
|
+
* (no address / shipping / cart UI). Email is collected by the Gr4vy
|
|
88
|
+
* iframe in proxy-mode sessions. Used when the merchant has built
|
|
89
|
+
* their own checkout UI and only needs Throttle to handle payment
|
|
90
|
+
* authorization + capture.
|
|
91
|
+
*
|
|
92
|
+
* `parentOrigin` must match an entry in the merchant's allow-list.
|
|
93
|
+
* The iframe auto-resizes on `throttle.resize` events.
|
|
94
|
+
*/
|
|
95
|
+
declare function PaymentEmbed({ sessionId, parentOrigin, baseUrl, primary, logo, initialHeight, className, style, onReady, onProcessing, onSucceeded, onFailed, onCanceled, }: ThrottleEmbedProps): react_jsx_runtime.JSX.Element;
|
|
96
|
+
|
|
97
|
+
interface UseThrottleEventsOptions {
|
|
98
|
+
iframeRef: RefObject<HTMLIFrameElement | null>;
|
|
99
|
+
/**
|
|
100
|
+
* The Throttle origin (e.g. https://throttle-checkout.vercel.app).
|
|
101
|
+
* Strict-equals check against `MessageEvent.origin`. Anything else
|
|
102
|
+
* is silently dropped.
|
|
103
|
+
*/
|
|
104
|
+
expectedOrigin: string;
|
|
105
|
+
onMessage: (msg: ThrottleMessage) => void;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Subscribe to validated postMessage events from a Throttle iframe.
|
|
109
|
+
*
|
|
110
|
+
* Validation:
|
|
111
|
+
* - event.origin === expectedOrigin (strict string match)
|
|
112
|
+
* - event.source === iframeRef.current.contentWindow (defends against
|
|
113
|
+
* other iframes on the same origin spoofing events)
|
|
114
|
+
* - event.data.source === 'throttle' && event.data.version === 1
|
|
115
|
+
* - event.data.type starts with 'throttle.'
|
|
25
116
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
117
|
+
* Anything failing validation is silently dropped — the parent page
|
|
118
|
+
* may receive postMessages from analytics SDKs, browser extensions,
|
|
119
|
+
* other iframes, etc. We must never throw on those.
|
|
29
120
|
*/
|
|
30
|
-
declare function
|
|
121
|
+
declare function useThrottleEvents({ iframeRef, expectedOrigin, onMessage }: UseThrottleEventsOptions): void;
|
|
31
122
|
|
|
32
|
-
export { ThrottleCheckout, type
|
|
123
|
+
export { CheckoutEmbed, type CheckoutEmbedExtraProps, PaymentEmbed, CheckoutEmbed as ThrottleCheckout, type ThrottleEmbedProps, type ThrottleEnvelope, type ThrottleEvent, type ThrottleMessage, useThrottleEvents };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,32 +1,123 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { CSSProperties, RefObject } from 'react';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Envelope every Throttle iframe message carries. Lets parents
|
|
6
|
+
* disambiguate Throttle messages from other postMessage senders on the
|
|
7
|
+
* page (analytics SDKs, ad pixels, other iframes).
|
|
8
|
+
*/
|
|
9
|
+
interface ThrottleEnvelope {
|
|
10
|
+
source: 'throttle';
|
|
11
|
+
version: 1;
|
|
12
|
+
}
|
|
13
|
+
type ThrottleEvent = {
|
|
14
|
+
type: 'throttle.ready';
|
|
15
|
+
} | {
|
|
16
|
+
type: 'throttle.resize';
|
|
17
|
+
height: number;
|
|
18
|
+
} | {
|
|
19
|
+
type: 'throttle.processing';
|
|
20
|
+
} | {
|
|
21
|
+
type: 'throttle.completed';
|
|
22
|
+
orderId: string;
|
|
23
|
+
paymentId: string;
|
|
24
|
+
} | {
|
|
25
|
+
type: 'throttle.cancelled';
|
|
26
|
+
} | {
|
|
27
|
+
type: 'throttle.error';
|
|
28
|
+
code: string;
|
|
29
|
+
message: string;
|
|
30
|
+
} | {
|
|
31
|
+
type: 'throttle.step.changed';
|
|
32
|
+
step: 'cart' | 'address' | 'shipping' | 'payment';
|
|
33
|
+
};
|
|
34
|
+
type ThrottleMessage = ThrottleEvent & ThrottleEnvelope;
|
|
35
|
+
interface ThrottleEmbedProps {
|
|
36
|
+
/** Throttle checkout session id (cs_*). Required. */
|
|
5
37
|
sessionId: string;
|
|
6
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Parent origin where this embed is mounted (e.g. https://shop.example.com).
|
|
40
|
+
* Must be in the merchant's allowed_origins list, set via
|
|
41
|
+
* `throttle embed-config set --origins ...` or PUT /api/v1/embed-config.
|
|
42
|
+
* Required.
|
|
43
|
+
*/
|
|
44
|
+
parentOrigin: string;
|
|
45
|
+
/** Base URL of the deployed checkout-web app. Defaults to https://throttle-checkout.vercel.app. */
|
|
7
46
|
baseUrl?: string;
|
|
8
|
-
/**
|
|
47
|
+
/** Optional brand color override (#rrggbb). */
|
|
48
|
+
primary?: string;
|
|
49
|
+
/** Optional logo URL override (https). */
|
|
50
|
+
logo?: string;
|
|
51
|
+
/** Initial iframe height in px. Auto-resizes on throttle.resize events. */
|
|
9
52
|
initialHeight?: number;
|
|
10
|
-
/** Optional className for the iframe. */
|
|
11
53
|
className?: string;
|
|
12
|
-
|
|
13
|
-
style?: React.CSSProperties;
|
|
14
|
-
/** Fires once the iframe is ready to receive interactions. */
|
|
54
|
+
style?: CSSProperties;
|
|
15
55
|
onReady?: () => void;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
56
|
+
onProcessing?: () => void;
|
|
57
|
+
onSucceeded?: (evt: {
|
|
58
|
+
orderId: string;
|
|
59
|
+
paymentId: string;
|
|
60
|
+
}) => void;
|
|
61
|
+
onFailed?: (evt: {
|
|
62
|
+
code: string;
|
|
63
|
+
message: string;
|
|
64
|
+
}) => void;
|
|
65
|
+
onCanceled?: () => void;
|
|
66
|
+
}
|
|
67
|
+
interface CheckoutEmbedExtraProps {
|
|
68
|
+
onStepChanged?: (evt: {
|
|
69
|
+
step: 'cart' | 'address' | 'shipping' | 'payment';
|
|
70
|
+
}) => void;
|
|
22
71
|
}
|
|
72
|
+
|
|
23
73
|
/**
|
|
24
|
-
*
|
|
74
|
+
* Full-checkout iframe embed. Renders the entire Throttle hosted
|
|
75
|
+
* checkout (address / shipping / payment) inside an iframe and
|
|
76
|
+
* surfaces lifecycle + terminal events through callbacks. The
|
|
77
|
+
* iframe auto-resizes on `throttle.resize` events.
|
|
78
|
+
*
|
|
79
|
+
* `parentOrigin` must match an entry in the merchant's allow-list
|
|
80
|
+
* (`throttle embed-config set --origins ...`). Mismatches render
|
|
81
|
+
* an in-iframe "Embed not authorized" panel.
|
|
82
|
+
*/
|
|
83
|
+
declare function CheckoutEmbed({ sessionId, parentOrigin, baseUrl, primary, logo, initialHeight, className, style, onReady, onProcessing, onSucceeded, onFailed, onCanceled, onStepChanged, }: ThrottleEmbedProps & CheckoutEmbedExtraProps): react_jsx_runtime.JSX.Element;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Payment-only iframe embed. Renders just the Gr4vy provider iframe
|
|
87
|
+
* (no address / shipping / cart UI). Email is collected by the Gr4vy
|
|
88
|
+
* iframe in proxy-mode sessions. Used when the merchant has built
|
|
89
|
+
* their own checkout UI and only needs Throttle to handle payment
|
|
90
|
+
* authorization + capture.
|
|
91
|
+
*
|
|
92
|
+
* `parentOrigin` must match an entry in the merchant's allow-list.
|
|
93
|
+
* The iframe auto-resizes on `throttle.resize` events.
|
|
94
|
+
*/
|
|
95
|
+
declare function PaymentEmbed({ sessionId, parentOrigin, baseUrl, primary, logo, initialHeight, className, style, onReady, onProcessing, onSucceeded, onFailed, onCanceled, }: ThrottleEmbedProps): react_jsx_runtime.JSX.Element;
|
|
96
|
+
|
|
97
|
+
interface UseThrottleEventsOptions {
|
|
98
|
+
iframeRef: RefObject<HTMLIFrameElement | null>;
|
|
99
|
+
/**
|
|
100
|
+
* The Throttle origin (e.g. https://throttle-checkout.vercel.app).
|
|
101
|
+
* Strict-equals check against `MessageEvent.origin`. Anything else
|
|
102
|
+
* is silently dropped.
|
|
103
|
+
*/
|
|
104
|
+
expectedOrigin: string;
|
|
105
|
+
onMessage: (msg: ThrottleMessage) => void;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Subscribe to validated postMessage events from a Throttle iframe.
|
|
109
|
+
*
|
|
110
|
+
* Validation:
|
|
111
|
+
* - event.origin === expectedOrigin (strict string match)
|
|
112
|
+
* - event.source === iframeRef.current.contentWindow (defends against
|
|
113
|
+
* other iframes on the same origin spoofing events)
|
|
114
|
+
* - event.data.source === 'throttle' && event.data.version === 1
|
|
115
|
+
* - event.data.type starts with 'throttle.'
|
|
25
116
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
117
|
+
* Anything failing validation is silently dropped — the parent page
|
|
118
|
+
* may receive postMessages from analytics SDKs, browser extensions,
|
|
119
|
+
* other iframes, etc. We must never throw on those.
|
|
29
120
|
*/
|
|
30
|
-
declare function
|
|
121
|
+
declare function useThrottleEvents({ iframeRef, expectedOrigin, onMessage }: UseThrottleEventsOptions): void;
|
|
31
122
|
|
|
32
|
-
export { ThrottleCheckout, type
|
|
123
|
+
export { CheckoutEmbed, type CheckoutEmbedExtraProps, PaymentEmbed, CheckoutEmbed as ThrottleCheckout, type ThrottleEmbedProps, type ThrottleEnvelope, type ThrottleEvent, type ThrottleMessage, useThrottleEvents };
|
package/dist/index.js
CHANGED
|
@@ -1,38 +1,79 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
1
|
+
// src/CheckoutEmbed.tsx
|
|
2
|
+
import { useCallback, useRef } from "react";
|
|
3
|
+
|
|
4
|
+
// src/useThrottleEvents.ts
|
|
5
|
+
import { useEffect } from "react";
|
|
6
|
+
function useThrottleEvents({ iframeRef, expectedOrigin, onMessage }) {
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
function handle(ev) {
|
|
9
|
+
if (ev.origin !== expectedOrigin) return;
|
|
10
|
+
if (iframeRef.current && ev.source !== iframeRef.current.contentWindow) return;
|
|
11
|
+
const data = ev.data;
|
|
12
|
+
if (!data || typeof data !== "object") return;
|
|
13
|
+
if (data.source !== "throttle" || data.version !== 1) return;
|
|
14
|
+
if (typeof data.type !== "string" || !data.type.startsWith("throttle.")) return;
|
|
15
|
+
onMessage(data);
|
|
16
|
+
}
|
|
17
|
+
window.addEventListener("message", handle);
|
|
18
|
+
return () => window.removeEventListener("message", handle);
|
|
19
|
+
}, [iframeRef, expectedOrigin, onMessage]);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/CheckoutEmbed.tsx
|
|
3
23
|
import { jsx } from "react/jsx-runtime";
|
|
4
|
-
var DEFAULT_BASE_URL = "https://throttle-checkout
|
|
5
|
-
function
|
|
24
|
+
var DEFAULT_BASE_URL = "https://throttle-checkout.vercel.app";
|
|
25
|
+
function CheckoutEmbed({
|
|
6
26
|
sessionId,
|
|
27
|
+
parentOrigin,
|
|
7
28
|
baseUrl = DEFAULT_BASE_URL,
|
|
8
|
-
|
|
29
|
+
primary,
|
|
30
|
+
logo,
|
|
31
|
+
initialHeight = 600,
|
|
9
32
|
className,
|
|
10
33
|
style,
|
|
11
34
|
onReady,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
35
|
+
onProcessing,
|
|
36
|
+
onSucceeded,
|
|
37
|
+
onFailed,
|
|
38
|
+
onCanceled,
|
|
39
|
+
onStepChanged
|
|
15
40
|
}) {
|
|
16
41
|
const iframeRef = useRef(null);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
42
|
+
const expectedOrigin = new URL(baseUrl).origin;
|
|
43
|
+
const dispatch = useCallback(
|
|
44
|
+
(msg) => {
|
|
45
|
+
switch (msg.type) {
|
|
46
|
+
case "throttle.ready":
|
|
47
|
+
onReady?.();
|
|
48
|
+
break;
|
|
49
|
+
case "throttle.processing":
|
|
50
|
+
onProcessing?.();
|
|
51
|
+
break;
|
|
52
|
+
case "throttle.completed":
|
|
53
|
+
onSucceeded?.({ orderId: msg.orderId, paymentId: msg.paymentId });
|
|
54
|
+
break;
|
|
55
|
+
case "throttle.error":
|
|
56
|
+
onFailed?.({ code: msg.code, message: msg.message });
|
|
57
|
+
break;
|
|
58
|
+
case "throttle.cancelled":
|
|
59
|
+
onCanceled?.();
|
|
60
|
+
break;
|
|
61
|
+
case "throttle.step.changed":
|
|
62
|
+
onStepChanged?.({ step: msg.step });
|
|
63
|
+
break;
|
|
64
|
+
case "throttle.resize":
|
|
65
|
+
if (iframeRef.current) iframeRef.current.style.height = `${msg.height}px`;
|
|
66
|
+
break;
|
|
25
67
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}, [onReady, onCompleted, onCancelled, onError]);
|
|
68
|
+
},
|
|
69
|
+
[onReady, onProcessing, onSucceeded, onFailed, onCanceled, onStepChanged]
|
|
70
|
+
);
|
|
71
|
+
useThrottleEvents({ iframeRef, expectedOrigin, onMessage: dispatch });
|
|
72
|
+
const params = new URLSearchParams({ embed: "1", mode: "checkout-full", parentOrigin });
|
|
73
|
+
if (primary) params.set("primary", primary);
|
|
74
|
+
if (logo) params.set("logo", logo);
|
|
34
75
|
const cleanBase = baseUrl.replace(/\/$/, "");
|
|
35
|
-
const src = `${cleanBase}/c/${encodeURIComponent(sessionId)}
|
|
76
|
+
const src = `${cleanBase}/c/${encodeURIComponent(sessionId)}?${params.toString()}`;
|
|
36
77
|
return /* @__PURE__ */ jsx(
|
|
37
78
|
"iframe",
|
|
38
79
|
{
|
|
@@ -45,7 +86,75 @@ function ThrottleCheckout({
|
|
|
45
86
|
}
|
|
46
87
|
);
|
|
47
88
|
}
|
|
89
|
+
|
|
90
|
+
// src/PaymentEmbed.tsx
|
|
91
|
+
import { useCallback as useCallback2, useRef as useRef2 } from "react";
|
|
92
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
93
|
+
var DEFAULT_BASE_URL2 = "https://throttle-checkout.vercel.app";
|
|
94
|
+
function PaymentEmbed({
|
|
95
|
+
sessionId,
|
|
96
|
+
parentOrigin,
|
|
97
|
+
baseUrl = DEFAULT_BASE_URL2,
|
|
98
|
+
primary,
|
|
99
|
+
logo,
|
|
100
|
+
initialHeight = 480,
|
|
101
|
+
className,
|
|
102
|
+
style,
|
|
103
|
+
onReady,
|
|
104
|
+
onProcessing,
|
|
105
|
+
onSucceeded,
|
|
106
|
+
onFailed,
|
|
107
|
+
onCanceled
|
|
108
|
+
}) {
|
|
109
|
+
const iframeRef = useRef2(null);
|
|
110
|
+
const expectedOrigin = new URL(baseUrl).origin;
|
|
111
|
+
const dispatch = useCallback2(
|
|
112
|
+
(msg) => {
|
|
113
|
+
switch (msg.type) {
|
|
114
|
+
case "throttle.ready":
|
|
115
|
+
onReady?.();
|
|
116
|
+
break;
|
|
117
|
+
case "throttle.processing":
|
|
118
|
+
onProcessing?.();
|
|
119
|
+
break;
|
|
120
|
+
case "throttle.completed":
|
|
121
|
+
onSucceeded?.({ orderId: msg.orderId, paymentId: msg.paymentId });
|
|
122
|
+
break;
|
|
123
|
+
case "throttle.error":
|
|
124
|
+
onFailed?.({ code: msg.code, message: msg.message });
|
|
125
|
+
break;
|
|
126
|
+
case "throttle.cancelled":
|
|
127
|
+
onCanceled?.();
|
|
128
|
+
break;
|
|
129
|
+
case "throttle.resize":
|
|
130
|
+
if (iframeRef.current) iframeRef.current.style.height = `${msg.height}px`;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
[onReady, onProcessing, onSucceeded, onFailed, onCanceled]
|
|
135
|
+
);
|
|
136
|
+
useThrottleEvents({ iframeRef, expectedOrigin, onMessage: dispatch });
|
|
137
|
+
const params = new URLSearchParams({ embed: "1", mode: "payment-only", parentOrigin });
|
|
138
|
+
if (primary) params.set("primary", primary);
|
|
139
|
+
if (logo) params.set("logo", logo);
|
|
140
|
+
const cleanBase = baseUrl.replace(/\/$/, "");
|
|
141
|
+
const src = `${cleanBase}/c/${encodeURIComponent(sessionId)}?${params.toString()}`;
|
|
142
|
+
return /* @__PURE__ */ jsx2(
|
|
143
|
+
"iframe",
|
|
144
|
+
{
|
|
145
|
+
ref: iframeRef,
|
|
146
|
+
src,
|
|
147
|
+
className,
|
|
148
|
+
style: { border: 0, width: "100%", minHeight: initialHeight, ...style },
|
|
149
|
+
allow: "payment *",
|
|
150
|
+
title: "Throttle payment"
|
|
151
|
+
}
|
|
152
|
+
);
|
|
153
|
+
}
|
|
48
154
|
export {
|
|
49
|
-
|
|
155
|
+
CheckoutEmbed,
|
|
156
|
+
PaymentEmbed,
|
|
157
|
+
CheckoutEmbed as ThrottleCheckout,
|
|
158
|
+
useThrottleEvents
|
|
50
159
|
};
|
|
51
160
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.tsx"],"sourcesContent":["import { useEffect, useRef } from 'react';\n\nexport interface ThrottleCheckoutProps {\n /** The throttle checkout session id (sess_*). Required. */\n sessionId: string;\n /** Base URL of the deployed checkout-web app. Defaults to https://throttle-checkout-web.onrender.com */\n baseUrl?: string;\n /** Initial iframe height in pixels. Auto-resizes on throttle.resize events. */\n initialHeight?: number;\n /** Optional className for the iframe. */\n className?: string;\n /** Optional inline style for the iframe. */\n style?: React.CSSProperties;\n /** Fires once the iframe is ready to receive interactions. */\n onReady?: () => void;\n /** Fires after a successful payment. */\n onCompleted?: (orderId: string, paymentId: string) => void;\n /** Fires when the buyer cancels checkout. */\n onCancelled?: () => void;\n /** Fires on any error inside the iframe (Gr4vy decline, network, etc.). */\n onError?: (code: string, message: string) => void;\n}\n\nconst DEFAULT_BASE_URL = 'https://throttle-checkout-web.onrender.com';\n\n/**\n * Embedded Throttle checkout for React apps.\n *\n * Renders an iframe pointed at <baseUrl>/c/<sessionId>?embed=1 and\n * subscribes to the postMessage v1 protocol. Events fire on the\n * provided callbacks. Auto-resizes the iframe on throttle.resize.\n */\nexport function ThrottleCheckout({\n sessionId,\n baseUrl = DEFAULT_BASE_URL,\n initialHeight = 480,\n className,\n style,\n onReady,\n onCompleted,\n onCancelled,\n onError,\n}: ThrottleCheckoutProps) {\n const iframeRef = useRef<HTMLIFrameElement>(null);\n\n useEffect(() => {\n function handle(ev: MessageEvent) {\n const data = ev?.data;\n if (!data || typeof data !== 'object' || typeof data.type !== 'string') return;\n if (!data.type.startsWith('throttle.')) return;\n if (iframeRef.current && ev.source !== iframeRef.current.contentWindow) return;\n\n if (data.type === 'throttle.resize' && typeof data.height === 'number' && iframeRef.current) {\n iframeRef.current.style.height = `${data.height}px`;\n }\n if (data.type === 'throttle.ready') onReady?.();\n else if (data.type === 'throttle.completed') onCompleted?.(data.orderId, data.paymentId);\n else if (data.type === 'throttle.cancelled') onCancelled?.();\n else if (data.type === 'throttle.error') onError?.(data.code, data.message);\n }\n window.addEventListener('message', handle);\n return () => window.removeEventListener('message', handle);\n }, [onReady, onCompleted, onCancelled, onError]);\n\n const cleanBase = baseUrl.replace(/\\/$/, '');\n const src = `${cleanBase}/c/${encodeURIComponent(sessionId)}?embed=1`;\n\n return (\n <iframe\n ref={iframeRef}\n src={src}\n className={className}\n style={{ border: 0, width: '100%', minHeight: initialHeight, ...style }}\n allow=\"payment *\"\n title=\"Throttle checkout\"\n />\n );\n}\n"],"mappings":";AAAA,SAAS,WAAW,cAAc;AAoE9B;AA7CJ,IAAM,mBAAmB;AASlB,SAAS,iBAAiB;AAAA,EAC/B;AAAA,EACA,UAAU;AAAA,EACV,gBAAgB;AAAA,EAChB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA0B;AACxB,QAAM,YAAY,OAA0B,IAAI;AAEhD,YAAU,MAAM;AACd,aAAS,OAAO,IAAkB;AAChC,YAAM,OAAO,IAAI;AACjB,UAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,OAAO,KAAK,SAAS,SAAU;AACxE,UAAI,CAAC,KAAK,KAAK,WAAW,WAAW,EAAG;AACxC,UAAI,UAAU,WAAW,GAAG,WAAW,UAAU,QAAQ,cAAe;AAExE,UAAI,KAAK,SAAS,qBAAqB,OAAO,KAAK,WAAW,YAAY,UAAU,SAAS;AAC3F,kBAAU,QAAQ,MAAM,SAAS,GAAG,KAAK,MAAM;AAAA,MACjD;AACA,UAAI,KAAK,SAAS,iBAAkB,WAAU;AAAA,eACrC,KAAK,SAAS,qBAAsB,eAAc,KAAK,SAAS,KAAK,SAAS;AAAA,eAC9E,KAAK,SAAS,qBAAsB,eAAc;AAAA,eAClD,KAAK,SAAS,iBAAkB,WAAU,KAAK,MAAM,KAAK,OAAO;AAAA,IAC5E;AACA,WAAO,iBAAiB,WAAW,MAAM;AACzC,WAAO,MAAM,OAAO,oBAAoB,WAAW,MAAM;AAAA,EAC3D,GAAG,CAAC,SAAS,aAAa,aAAa,OAAO,CAAC;AAE/C,QAAM,YAAY,QAAQ,QAAQ,OAAO,EAAE;AAC3C,QAAM,MAAM,GAAG,SAAS,MAAM,mBAAmB,SAAS,CAAC;AAE3D,SACE;AAAA,IAAC;AAAA;AAAA,MACC,KAAK;AAAA,MACL;AAAA,MACA;AAAA,MACA,OAAO,EAAE,QAAQ,GAAG,OAAO,QAAQ,WAAW,eAAe,GAAG,MAAM;AAAA,MACtE,OAAM;AAAA,MACN,OAAM;AAAA;AAAA,EACR;AAEJ;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/CheckoutEmbed.tsx","../src/useThrottleEvents.ts","../src/PaymentEmbed.tsx"],"sourcesContent":["import { useCallback, useRef } from 'react';\nimport { useThrottleEvents } from './useThrottleEvents.js';\nimport type {\n ThrottleEmbedProps,\n CheckoutEmbedExtraProps,\n ThrottleMessage,\n} from './types.js';\n\nconst DEFAULT_BASE_URL = 'https://throttle-checkout.vercel.app';\n\n/**\n * Full-checkout iframe embed. Renders the entire Throttle hosted\n * checkout (address / shipping / payment) inside an iframe and\n * surfaces lifecycle + terminal events through callbacks. The\n * iframe auto-resizes on `throttle.resize` events.\n *\n * `parentOrigin` must match an entry in the merchant's allow-list\n * (`throttle embed-config set --origins ...`). Mismatches render\n * an in-iframe \"Embed not authorized\" panel.\n */\nexport function CheckoutEmbed({\n sessionId,\n parentOrigin,\n baseUrl = DEFAULT_BASE_URL,\n primary,\n logo,\n initialHeight = 600,\n className,\n style,\n onReady,\n onProcessing,\n onSucceeded,\n onFailed,\n onCanceled,\n onStepChanged,\n}: ThrottleEmbedProps & CheckoutEmbedExtraProps) {\n const iframeRef = useRef<HTMLIFrameElement>(null);\n const expectedOrigin = new URL(baseUrl).origin;\n\n const dispatch = useCallback(\n (msg: ThrottleMessage) => {\n switch (msg.type) {\n case 'throttle.ready':\n onReady?.();\n break;\n case 'throttle.processing':\n onProcessing?.();\n break;\n case 'throttle.completed':\n onSucceeded?.({ orderId: msg.orderId, paymentId: msg.paymentId });\n break;\n case 'throttle.error':\n onFailed?.({ code: msg.code, message: msg.message });\n break;\n case 'throttle.cancelled':\n onCanceled?.();\n break;\n case 'throttle.step.changed':\n onStepChanged?.({ step: msg.step });\n break;\n case 'throttle.resize':\n if (iframeRef.current) iframeRef.current.style.height = `${msg.height}px`;\n break;\n }\n },\n [onReady, onProcessing, onSucceeded, onFailed, onCanceled, onStepChanged],\n );\n\n useThrottleEvents({ iframeRef, expectedOrigin, onMessage: dispatch });\n\n const params = new URLSearchParams({ embed: '1', mode: 'checkout-full', parentOrigin });\n if (primary) params.set('primary', primary);\n if (logo) params.set('logo', logo);\n const cleanBase = baseUrl.replace(/\\/$/, '');\n const src = `${cleanBase}/c/${encodeURIComponent(sessionId)}?${params.toString()}`;\n\n return (\n <iframe\n ref={iframeRef}\n src={src}\n className={className}\n style={{ border: 0, width: '100%', minHeight: initialHeight, ...style }}\n allow=\"payment *\"\n title=\"Throttle checkout\"\n />\n );\n}\n","import { useEffect, type RefObject } from 'react';\nimport type { ThrottleMessage } from './types.js';\n\nexport interface UseThrottleEventsOptions {\n iframeRef: RefObject<HTMLIFrameElement | null>;\n /**\n * The Throttle origin (e.g. https://throttle-checkout.vercel.app).\n * Strict-equals check against `MessageEvent.origin`. Anything else\n * is silently dropped.\n */\n expectedOrigin: string;\n onMessage: (msg: ThrottleMessage) => void;\n}\n\n/**\n * Subscribe to validated postMessage events from a Throttle iframe.\n *\n * Validation:\n * - event.origin === expectedOrigin (strict string match)\n * - event.source === iframeRef.current.contentWindow (defends against\n * other iframes on the same origin spoofing events)\n * - event.data.source === 'throttle' && event.data.version === 1\n * - event.data.type starts with 'throttle.'\n *\n * Anything failing validation is silently dropped — the parent page\n * may receive postMessages from analytics SDKs, browser extensions,\n * other iframes, etc. We must never throw on those.\n */\nexport function useThrottleEvents({ iframeRef, expectedOrigin, onMessage }: UseThrottleEventsOptions) {\n useEffect(() => {\n function handle(ev: MessageEvent) {\n if (ev.origin !== expectedOrigin) return;\n if (iframeRef.current && ev.source !== iframeRef.current.contentWindow) return;\n const data = ev.data as Partial<ThrottleMessage> | null | undefined;\n if (!data || typeof data !== 'object') return;\n if (data.source !== 'throttle' || data.version !== 1) return;\n if (typeof data.type !== 'string' || !data.type.startsWith('throttle.')) return;\n onMessage(data as ThrottleMessage);\n }\n window.addEventListener('message', handle);\n return () => window.removeEventListener('message', handle);\n }, [iframeRef, expectedOrigin, onMessage]);\n}\n","import { useCallback, useRef } from 'react';\nimport { useThrottleEvents } from './useThrottleEvents.js';\nimport type { ThrottleEmbedProps, ThrottleMessage } from './types.js';\n\nconst DEFAULT_BASE_URL = 'https://throttle-checkout.vercel.app';\n\n/**\n * Payment-only iframe embed. Renders just the Gr4vy provider iframe\n * (no address / shipping / cart UI). Email is collected by the Gr4vy\n * iframe in proxy-mode sessions. Used when the merchant has built\n * their own checkout UI and only needs Throttle to handle payment\n * authorization + capture.\n *\n * `parentOrigin` must match an entry in the merchant's allow-list.\n * The iframe auto-resizes on `throttle.resize` events.\n */\nexport function PaymentEmbed({\n sessionId,\n parentOrigin,\n baseUrl = DEFAULT_BASE_URL,\n primary,\n logo,\n initialHeight = 480,\n className,\n style,\n onReady,\n onProcessing,\n onSucceeded,\n onFailed,\n onCanceled,\n}: ThrottleEmbedProps) {\n const iframeRef = useRef<HTMLIFrameElement>(null);\n const expectedOrigin = new URL(baseUrl).origin;\n\n const dispatch = useCallback(\n (msg: ThrottleMessage) => {\n switch (msg.type) {\n case 'throttle.ready':\n onReady?.();\n break;\n case 'throttle.processing':\n onProcessing?.();\n break;\n case 'throttle.completed':\n onSucceeded?.({ orderId: msg.orderId, paymentId: msg.paymentId });\n break;\n case 'throttle.error':\n onFailed?.({ code: msg.code, message: msg.message });\n break;\n case 'throttle.cancelled':\n onCanceled?.();\n break;\n case 'throttle.resize':\n if (iframeRef.current) iframeRef.current.style.height = `${msg.height}px`;\n break;\n }\n },\n [onReady, onProcessing, onSucceeded, onFailed, onCanceled],\n );\n\n useThrottleEvents({ iframeRef, expectedOrigin, onMessage: dispatch });\n\n const params = new URLSearchParams({ embed: '1', mode: 'payment-only', parentOrigin });\n if (primary) params.set('primary', primary);\n if (logo) params.set('logo', logo);\n const cleanBase = baseUrl.replace(/\\/$/, '');\n const src = `${cleanBase}/c/${encodeURIComponent(sessionId)}?${params.toString()}`;\n\n return (\n <iframe\n ref={iframeRef}\n src={src}\n className={className}\n style={{ border: 0, width: '100%', minHeight: initialHeight, ...style }}\n allow=\"payment *\"\n title=\"Throttle payment\"\n />\n );\n}\n"],"mappings":"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","names":["useCallback","useRef","jsx","DEFAULT_BASE_URL","useRef","useCallback"]}
|