@coincircuit/checkout 0.4.0 → 0.4.2
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 +35 -0
- package/dist/chunk-FKC27TV4.js +561 -0
- package/dist/index.cjs +288 -104
- package/dist/index.js +1 -1
- package/dist/react.cjs +288 -104
- package/dist/react.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-RJ3FMO5K.js +0 -377
package/README.md
CHANGED
|
@@ -57,6 +57,41 @@ function PayPage() {
|
|
|
57
57
|
5. Your callbacks fire with payment data
|
|
58
58
|
6. Modal closes automatically on success
|
|
59
59
|
|
|
60
|
+
## Overlay appearance
|
|
61
|
+
|
|
62
|
+
The overlay follows the `theme` passed to `open()`. On desktop it uses a softly
|
|
63
|
+
blurred backdrop, a rounded checkout frame, and a close button beside the frame.
|
|
64
|
+
The loading state appears inside the frame while the checkout connects. On mobile
|
|
65
|
+
it fills the screen with a separate close row, keeping the checkout unobstructed.
|
|
66
|
+
Reduced motion and reduced transparency preferences are respected.
|
|
67
|
+
|
|
68
|
+
## Local development in the ecommerce demo
|
|
69
|
+
|
|
70
|
+
From `../../coincircuit-ecommerce-demo`, run `npm run dev:local-sdk` to use this
|
|
71
|
+
SDK's source directly. The demo's regular development command and production use
|
|
72
|
+
the published package. No SDK build or package linking is needed for this mode.
|
|
73
|
+
|
|
74
|
+
## Publishing
|
|
75
|
+
|
|
76
|
+
Pushing to `main` runs the npm publishing workflow. It automatically advances the
|
|
77
|
+
patch version beyond the highest stable version on npm (for example, `0.4.1` to
|
|
78
|
+
`0.4.2`), updates both `package.json` and `package-lock.json`, checks the release,
|
|
79
|
+
and builds the SDK. It then saves the version in a bot commit on `main` before
|
|
80
|
+
publishing. A manually chosen version higher than npm's latest stable version is
|
|
81
|
+
preserved, so minor and major releases can still be chosen explicitly.
|
|
82
|
+
|
|
83
|
+
Releases run one at a time. Rerunning a failed publish retries its saved version;
|
|
84
|
+
an already published release at the tip of `main` is skipped. Registry lookup
|
|
85
|
+
errors stop the release. If `main` changes during the build, the version push
|
|
86
|
+
stops and the queued run handles the newer code.
|
|
87
|
+
|
|
88
|
+
The workflow uses `NPM_TOKEN` to publish and `GITHUB_TOKEN` to save the version.
|
|
89
|
+
Repository rules must allow the workflow to push version commits to `main`.
|
|
90
|
+
Commits pushed with `GITHUB_TOKEN` do not trigger another publishing run.
|
|
91
|
+
Pull the bot's version commit before pushing subsequent local changes.
|
|
92
|
+
|
|
93
|
+
Publishing a package does not update or redeploy applications that consume it.
|
|
94
|
+
|
|
60
95
|
## Callbacks
|
|
61
96
|
|
|
62
97
|
| Callback | When |
|
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
// src/events.ts
|
|
2
|
+
var CHECKOUT_MESSAGE_TYPES = {
|
|
3
|
+
READY: "coincircuit:ready",
|
|
4
|
+
PAYMENT_COMPLETE: "coincircuit:payment_complete",
|
|
5
|
+
PAYMENT_FAILED: "coincircuit:payment_failed",
|
|
6
|
+
EXPIRED: "coincircuit:expired",
|
|
7
|
+
CLOSE: "coincircuit:close"
|
|
8
|
+
};
|
|
9
|
+
function isCheckoutMessage(value) {
|
|
10
|
+
if (typeof value !== "object" || value === null) return false;
|
|
11
|
+
const msg = value;
|
|
12
|
+
return typeof msg.type === "string" && msg.type.startsWith("coincircuit:");
|
|
13
|
+
}
|
|
14
|
+
function createMessageListener(allowedOrigin, handlers) {
|
|
15
|
+
function onMessage(event) {
|
|
16
|
+
if (event.origin !== allowedOrigin) return;
|
|
17
|
+
const message = event.data;
|
|
18
|
+
if (!isCheckoutMessage(message)) return;
|
|
19
|
+
switch (message.type) {
|
|
20
|
+
case CHECKOUT_MESSAGE_TYPES.READY:
|
|
21
|
+
handlers.onReady?.();
|
|
22
|
+
break;
|
|
23
|
+
case CHECKOUT_MESSAGE_TYPES.PAYMENT_COMPLETE:
|
|
24
|
+
if (message.data) {
|
|
25
|
+
handlers.onPaymentComplete?.(message.data);
|
|
26
|
+
}
|
|
27
|
+
break;
|
|
28
|
+
case CHECKOUT_MESSAGE_TYPES.PAYMENT_FAILED:
|
|
29
|
+
if (message.data) {
|
|
30
|
+
handlers.onPaymentFailed?.(message.data);
|
|
31
|
+
}
|
|
32
|
+
break;
|
|
33
|
+
case CHECKOUT_MESSAGE_TYPES.EXPIRED:
|
|
34
|
+
handlers.onExpired?.();
|
|
35
|
+
break;
|
|
36
|
+
case CHECKOUT_MESSAGE_TYPES.CLOSE:
|
|
37
|
+
handlers.onClose?.();
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
window.addEventListener("message", onMessage);
|
|
42
|
+
return () => window.removeEventListener("message", onMessage);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/overlay-styles.ts
|
|
46
|
+
var OVERLAY_ID = "coincircuit-checkout-overlay";
|
|
47
|
+
var OVERLAY_STYLES = `
|
|
48
|
+
#${OVERLAY_ID} {
|
|
49
|
+
--cc-surface: #fafafa;
|
|
50
|
+
--cc-foreground: #171717;
|
|
51
|
+
--cc-muted: #666;
|
|
52
|
+
--cc-edge: rgba(255, 255, 255, 0.85);
|
|
53
|
+
--cc-control: rgba(250, 250, 250, 0.88);
|
|
54
|
+
--cc-track: rgba(0, 0, 0, 0.1);
|
|
55
|
+
--cc-accent: oklch(0.52 0.22 255);
|
|
56
|
+
position: fixed;
|
|
57
|
+
inset: 0;
|
|
58
|
+
z-index: 999999;
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
justify-content: center;
|
|
62
|
+
box-sizing: border-box;
|
|
63
|
+
padding: 24px 64px;
|
|
64
|
+
background: rgba(12, 14, 18, 0.42);
|
|
65
|
+
-webkit-backdrop-filter: blur(10px);
|
|
66
|
+
backdrop-filter: blur(10px);
|
|
67
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
68
|
+
color: var(--cc-foreground);
|
|
69
|
+
opacity: 0;
|
|
70
|
+
transition: opacity 200ms ease;
|
|
71
|
+
}
|
|
72
|
+
#${OVERLAY_ID}[data-theme="dark"] {
|
|
73
|
+
--cc-surface: #111216;
|
|
74
|
+
--cc-foreground: #f5f5f5;
|
|
75
|
+
--cc-muted: #aaa;
|
|
76
|
+
--cc-edge: rgba(255, 255, 255, 0.18);
|
|
77
|
+
--cc-control: rgba(28, 29, 33, 0.9);
|
|
78
|
+
--cc-track: rgba(255, 255, 255, 0.14);
|
|
79
|
+
--cc-accent: oklch(0.72 0.29 263.25);
|
|
80
|
+
color-scheme: dark;
|
|
81
|
+
}
|
|
82
|
+
#${OVERLAY_ID}, #${OVERLAY_ID} * {
|
|
83
|
+
box-sizing: border-box;
|
|
84
|
+
}
|
|
85
|
+
#${OVERLAY_ID}.cc-visible {
|
|
86
|
+
opacity: 1;
|
|
87
|
+
}
|
|
88
|
+
#${OVERLAY_ID} .cc-modal {
|
|
89
|
+
position: relative;
|
|
90
|
+
width: 100%;
|
|
91
|
+
max-width: 740px;
|
|
92
|
+
height: min(900px, calc(100vh - 48px));
|
|
93
|
+
height: min(900px, calc(100dvh - 48px));
|
|
94
|
+
border: 1px solid var(--cc-edge);
|
|
95
|
+
border-radius: 30px;
|
|
96
|
+
background: var(--cc-surface);
|
|
97
|
+
box-shadow: 0 32px 100px -24px rgba(0, 0, 0, 0.38);
|
|
98
|
+
transform: translateY(8px) scale(0.99);
|
|
99
|
+
transition: transform 200ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
100
|
+
}
|
|
101
|
+
#${OVERLAY_ID}.cc-visible .cc-modal {
|
|
102
|
+
transform: translateY(0) scale(1);
|
|
103
|
+
}
|
|
104
|
+
#${OVERLAY_ID} .cc-frame {
|
|
105
|
+
position: relative;
|
|
106
|
+
width: 100%;
|
|
107
|
+
height: 100%;
|
|
108
|
+
overflow: hidden;
|
|
109
|
+
border-radius: inherit;
|
|
110
|
+
background: var(--cc-surface);
|
|
111
|
+
}
|
|
112
|
+
#${OVERLAY_ID} .cc-close {
|
|
113
|
+
position: absolute;
|
|
114
|
+
top: 0;
|
|
115
|
+
right: -56px;
|
|
116
|
+
z-index: 2;
|
|
117
|
+
display: flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
justify-content: center;
|
|
120
|
+
width: 44px;
|
|
121
|
+
height: 44px;
|
|
122
|
+
margin: 0;
|
|
123
|
+
padding: 0;
|
|
124
|
+
border: 1px solid var(--cc-edge);
|
|
125
|
+
border-radius: 50%;
|
|
126
|
+
background: var(--cc-control);
|
|
127
|
+
-webkit-backdrop-filter: blur(16px);
|
|
128
|
+
backdrop-filter: blur(16px);
|
|
129
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
|
130
|
+
color: var(--cc-foreground);
|
|
131
|
+
cursor: pointer;
|
|
132
|
+
touch-action: manipulation;
|
|
133
|
+
transition: background-color 160ms ease, transform 160ms ease;
|
|
134
|
+
}
|
|
135
|
+
#${OVERLAY_ID} .cc-close:hover {
|
|
136
|
+
background: var(--cc-surface);
|
|
137
|
+
}
|
|
138
|
+
#${OVERLAY_ID} .cc-close:active {
|
|
139
|
+
transform: scale(0.96);
|
|
140
|
+
}
|
|
141
|
+
#${OVERLAY_ID} .cc-close:focus-visible {
|
|
142
|
+
outline: 2px solid var(--cc-accent);
|
|
143
|
+
outline-offset: 4px;
|
|
144
|
+
}
|
|
145
|
+
#${OVERLAY_ID} .cc-close svg {
|
|
146
|
+
display: block;
|
|
147
|
+
width: 18px;
|
|
148
|
+
height: 18px;
|
|
149
|
+
pointer-events: none;
|
|
150
|
+
}
|
|
151
|
+
#${OVERLAY_ID} iframe {
|
|
152
|
+
display: block;
|
|
153
|
+
width: 100%;
|
|
154
|
+
height: 100%;
|
|
155
|
+
border: 0;
|
|
156
|
+
opacity: 0;
|
|
157
|
+
visibility: hidden;
|
|
158
|
+
transition: opacity 200ms ease;
|
|
159
|
+
}
|
|
160
|
+
#${OVERLAY_ID} .cc-ready iframe {
|
|
161
|
+
opacity: 1;
|
|
162
|
+
visibility: visible;
|
|
163
|
+
}
|
|
164
|
+
#${OVERLAY_ID} .cc-spinner {
|
|
165
|
+
position: absolute;
|
|
166
|
+
inset: 0;
|
|
167
|
+
z-index: 1;
|
|
168
|
+
display: flex;
|
|
169
|
+
flex-direction: column;
|
|
170
|
+
align-items: center;
|
|
171
|
+
justify-content: center;
|
|
172
|
+
gap: 16px;
|
|
173
|
+
background: var(--cc-surface);
|
|
174
|
+
color: var(--cc-muted);
|
|
175
|
+
font-size: 14px;
|
|
176
|
+
line-height: 1.5;
|
|
177
|
+
transition: opacity 200ms ease;
|
|
178
|
+
}
|
|
179
|
+
#${OVERLAY_ID} .cc-spinner-ring {
|
|
180
|
+
width: 32px;
|
|
181
|
+
height: 32px;
|
|
182
|
+
border: 2px solid var(--cc-track);
|
|
183
|
+
border-top-color: var(--cc-accent);
|
|
184
|
+
border-radius: 50%;
|
|
185
|
+
animation: cc-spin 800ms linear infinite;
|
|
186
|
+
}
|
|
187
|
+
#${OVERLAY_ID} .cc-ready .cc-spinner {
|
|
188
|
+
opacity: 0;
|
|
189
|
+
pointer-events: none;
|
|
190
|
+
}
|
|
191
|
+
@keyframes cc-spin { to { transform: rotate(360deg); } }
|
|
192
|
+
@media (max-width: 720px) {
|
|
193
|
+
#${OVERLAY_ID} {
|
|
194
|
+
padding: 0;
|
|
195
|
+
background: var(--cc-surface);
|
|
196
|
+
-webkit-backdrop-filter: none;
|
|
197
|
+
backdrop-filter: none;
|
|
198
|
+
}
|
|
199
|
+
#${OVERLAY_ID} .cc-modal {
|
|
200
|
+
max-width: none;
|
|
201
|
+
height: 100vh;
|
|
202
|
+
height: 100dvh;
|
|
203
|
+
padding-top: calc(56px + env(safe-area-inset-top, 0px));
|
|
204
|
+
border: 0;
|
|
205
|
+
border-radius: 0;
|
|
206
|
+
box-shadow: none;
|
|
207
|
+
transform: none;
|
|
208
|
+
}
|
|
209
|
+
#${OVERLAY_ID} .cc-close {
|
|
210
|
+
top: calc(6px + env(safe-area-inset-top, 0px));
|
|
211
|
+
right: max(12px, env(safe-area-inset-right, 0px));
|
|
212
|
+
border-color: var(--cc-track);
|
|
213
|
+
background: transparent;
|
|
214
|
+
box-shadow: none;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
@media (prefers-reduced-motion: reduce) {
|
|
218
|
+
#${OVERLAY_ID}, #${OVERLAY_ID} * {
|
|
219
|
+
animation: none !important;
|
|
220
|
+
transition: none !important;
|
|
221
|
+
}
|
|
222
|
+
#${OVERLAY_ID} .cc-modal, #${OVERLAY_ID} .cc-close:active {
|
|
223
|
+
transform: none;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
|
|
227
|
+
#${OVERLAY_ID} {
|
|
228
|
+
background: rgba(12, 14, 18, 0.85);
|
|
229
|
+
-webkit-backdrop-filter: none;
|
|
230
|
+
backdrop-filter: none;
|
|
231
|
+
}
|
|
232
|
+
#${OVERLAY_ID} .cc-close {
|
|
233
|
+
background: var(--cc-surface);
|
|
234
|
+
-webkit-backdrop-filter: none;
|
|
235
|
+
backdrop-filter: none;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
@media (prefers-contrast: more) {
|
|
239
|
+
#${OVERLAY_ID} .cc-modal, #${OVERLAY_ID} .cc-close {
|
|
240
|
+
border-color: var(--cc-foreground);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
`;
|
|
244
|
+
|
|
245
|
+
// src/utils.ts
|
|
246
|
+
var DEFAULT_BASE_URL = "https://checkout.coincircuit.io";
|
|
247
|
+
var ALLOWED_BASE_URLS = [
|
|
248
|
+
"https://checkout.coincircuit.io",
|
|
249
|
+
"https://sandbox-checkout.coincircuit.io"
|
|
250
|
+
];
|
|
251
|
+
function validateBaseUrl(url) {
|
|
252
|
+
const normalized = url.replace(/\/+$/, "");
|
|
253
|
+
if (!ALLOWED_BASE_URLS.includes(normalized)) {
|
|
254
|
+
throw new Error(
|
|
255
|
+
`CoinCircuitCheckout: invalid baseUrl "${url}". Must be one of: ${ALLOWED_BASE_URLS.join(", ")}`
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
return normalized;
|
|
259
|
+
}
|
|
260
|
+
function buildCheckoutUrl(baseUrl, reference, theme) {
|
|
261
|
+
const url = new URL(`/pay/${encodeURIComponent(reference)}`, baseUrl);
|
|
262
|
+
url.searchParams.set("embed", "true");
|
|
263
|
+
if (theme) {
|
|
264
|
+
url.searchParams.set("theme", theme);
|
|
265
|
+
}
|
|
266
|
+
return url.toString();
|
|
267
|
+
}
|
|
268
|
+
function extractOrigin(baseUrl) {
|
|
269
|
+
try {
|
|
270
|
+
return new URL(baseUrl).origin;
|
|
271
|
+
} catch {
|
|
272
|
+
return baseUrl;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
var STYLES_ID = "coincircuit-checkout-styles";
|
|
276
|
+
var overlayStates = /* @__PURE__ */ new WeakMap();
|
|
277
|
+
function injectStyles() {
|
|
278
|
+
if (document.getElementById(STYLES_ID)) return;
|
|
279
|
+
const style = document.createElement("style");
|
|
280
|
+
style.id = STYLES_ID;
|
|
281
|
+
style.textContent = OVERLAY_STYLES;
|
|
282
|
+
document.head.appendChild(style);
|
|
283
|
+
}
|
|
284
|
+
function createOverlay(checkoutUrl, onCloseClick, theme = "light") {
|
|
285
|
+
injectStyles();
|
|
286
|
+
const overlay = document.createElement("div");
|
|
287
|
+
overlay.id = OVERLAY_ID;
|
|
288
|
+
overlay.dataset.theme = theme;
|
|
289
|
+
let dismissed = false;
|
|
290
|
+
const dismiss = () => {
|
|
291
|
+
if (dismissed) return;
|
|
292
|
+
dismissed = true;
|
|
293
|
+
onCloseClick();
|
|
294
|
+
};
|
|
295
|
+
overlay.addEventListener("click", (e) => {
|
|
296
|
+
if (e.target === overlay) dismiss();
|
|
297
|
+
});
|
|
298
|
+
const isTopmostOverlay = () => {
|
|
299
|
+
const overlays = document.querySelectorAll(`#${OVERLAY_ID}.cc-visible`);
|
|
300
|
+
return overlays[overlays.length - 1] === overlay;
|
|
301
|
+
};
|
|
302
|
+
const onKeydown = (e) => {
|
|
303
|
+
if (!isTopmostOverlay()) return;
|
|
304
|
+
if (e.key === "Escape") {
|
|
305
|
+
e.preventDefault();
|
|
306
|
+
dismiss();
|
|
307
|
+
}
|
|
308
|
+
if (e.key === "Tab" && e.shiftKey && document.activeElement === closeBtn) {
|
|
309
|
+
e.preventDefault();
|
|
310
|
+
if (modal.classList.contains("cc-ready")) iframe.focus();
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
const onFocus = (e) => {
|
|
314
|
+
if (isTopmostOverlay() && !overlay.contains(e.target)) closeBtn.focus();
|
|
315
|
+
};
|
|
316
|
+
document.addEventListener("keydown", onKeydown);
|
|
317
|
+
document.addEventListener("focusin", onFocus);
|
|
318
|
+
overlayStates.set(overlay, {
|
|
319
|
+
removeListeners: () => {
|
|
320
|
+
document.removeEventListener("keydown", onKeydown);
|
|
321
|
+
document.removeEventListener("focusin", onFocus);
|
|
322
|
+
},
|
|
323
|
+
previousFocus: document.activeElement instanceof HTMLElement ? document.activeElement : null,
|
|
324
|
+
previousOverflow: document.body.style.overflow,
|
|
325
|
+
previousPaddingRight: document.body.style.paddingRight
|
|
326
|
+
});
|
|
327
|
+
const modal = document.createElement("div");
|
|
328
|
+
modal.className = "cc-modal";
|
|
329
|
+
modal.setAttribute("role", "dialog");
|
|
330
|
+
modal.setAttribute("aria-modal", "true");
|
|
331
|
+
modal.setAttribute("aria-label", "CoinCircuit checkout");
|
|
332
|
+
const frame = document.createElement("div");
|
|
333
|
+
frame.className = "cc-frame";
|
|
334
|
+
frame.setAttribute("aria-busy", "true");
|
|
335
|
+
const spinner = document.createElement("div");
|
|
336
|
+
spinner.className = "cc-spinner";
|
|
337
|
+
spinner.setAttribute("role", "status");
|
|
338
|
+
const spinnerRing = document.createElement("div");
|
|
339
|
+
spinnerRing.className = "cc-spinner-ring";
|
|
340
|
+
spinnerRing.setAttribute("aria-hidden", "true");
|
|
341
|
+
const spinnerLabel = document.createElement("span");
|
|
342
|
+
spinnerLabel.textContent = "Loading checkout";
|
|
343
|
+
spinner.appendChild(spinnerRing);
|
|
344
|
+
spinner.appendChild(spinnerLabel);
|
|
345
|
+
const iframe = document.createElement("iframe");
|
|
346
|
+
iframe.src = checkoutUrl;
|
|
347
|
+
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox");
|
|
348
|
+
iframe.setAttribute("referrerpolicy", "strict-origin-when-cross-origin");
|
|
349
|
+
iframe.setAttribute("title", "CoinCircuit Checkout");
|
|
350
|
+
iframe.setAttribute("allow", "payment");
|
|
351
|
+
iframe.tabIndex = -1;
|
|
352
|
+
const closeBtn = document.createElement("button");
|
|
353
|
+
closeBtn.className = "cc-close";
|
|
354
|
+
closeBtn.type = "button";
|
|
355
|
+
closeBtn.setAttribute("aria-label", "Close checkout");
|
|
356
|
+
const closeIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
357
|
+
closeIcon.setAttribute("viewBox", "0 0 24 24");
|
|
358
|
+
closeIcon.setAttribute("aria-hidden", "true");
|
|
359
|
+
closeIcon.setAttribute("fill", "none");
|
|
360
|
+
closeIcon.setAttribute("stroke", "currentColor");
|
|
361
|
+
closeIcon.setAttribute("stroke-width", "1.75");
|
|
362
|
+
closeIcon.setAttribute("stroke-linecap", "round");
|
|
363
|
+
const closePath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
364
|
+
closePath.setAttribute("d", "M6 6l12 12M18 6L6 18");
|
|
365
|
+
closeIcon.appendChild(closePath);
|
|
366
|
+
closeBtn.appendChild(closeIcon);
|
|
367
|
+
closeBtn.addEventListener("click", dismiss);
|
|
368
|
+
frame.appendChild(iframe);
|
|
369
|
+
frame.appendChild(spinner);
|
|
370
|
+
modal.appendChild(closeBtn);
|
|
371
|
+
modal.appendChild(frame);
|
|
372
|
+
overlay.appendChild(modal);
|
|
373
|
+
return { overlay, iframe };
|
|
374
|
+
}
|
|
375
|
+
function showOverlay(overlay) {
|
|
376
|
+
document.body.appendChild(overlay);
|
|
377
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
378
|
+
if (scrollbarWidth > 0) {
|
|
379
|
+
const padding = parseFloat(window.getComputedStyle(document.body).paddingRight) || 0;
|
|
380
|
+
document.body.style.paddingRight = `${padding + scrollbarWidth}px`;
|
|
381
|
+
}
|
|
382
|
+
document.body.style.overflow = "hidden";
|
|
383
|
+
overlay.offsetHeight;
|
|
384
|
+
overlay.classList.add("cc-visible");
|
|
385
|
+
overlay.querySelector(".cc-close")?.focus({ preventScroll: true });
|
|
386
|
+
}
|
|
387
|
+
function removeOverlay(overlay) {
|
|
388
|
+
const state = overlayStates.get(overlay);
|
|
389
|
+
state?.removeListeners();
|
|
390
|
+
overlay.classList.remove("cc-visible");
|
|
391
|
+
overlay.style.pointerEvents = "none";
|
|
392
|
+
overlay.inert = true;
|
|
393
|
+
if (state) {
|
|
394
|
+
document.body.style.overflow = state.previousOverflow;
|
|
395
|
+
document.body.style.paddingRight = state.previousPaddingRight;
|
|
396
|
+
state.previousFocus?.focus({ preventScroll: true });
|
|
397
|
+
overlayStates.delete(overlay);
|
|
398
|
+
}
|
|
399
|
+
setTimeout(() => {
|
|
400
|
+
overlay.parentNode?.removeChild(overlay);
|
|
401
|
+
}, 200);
|
|
402
|
+
}
|
|
403
|
+
function hideSpinner(overlay) {
|
|
404
|
+
overlay.querySelector(".cc-frame")?.setAttribute("aria-busy", "false");
|
|
405
|
+
const iframe = overlay.querySelector("iframe");
|
|
406
|
+
if (iframe) iframe.tabIndex = 0;
|
|
407
|
+
const spinner = overlay.querySelector(".cc-spinner");
|
|
408
|
+
if (spinner) {
|
|
409
|
+
spinner.style.opacity = "0";
|
|
410
|
+
setTimeout(() => spinner.parentNode?.removeChild(spinner), 200);
|
|
411
|
+
}
|
|
412
|
+
const modal = overlay.querySelector(".cc-modal");
|
|
413
|
+
if (modal) {
|
|
414
|
+
modal.classList.add("cc-ready");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function showErrorToast(message) {
|
|
418
|
+
const toast = document.createElement("div");
|
|
419
|
+
toast.textContent = message;
|
|
420
|
+
Object.assign(toast.style, {
|
|
421
|
+
position: "fixed",
|
|
422
|
+
top: "20px",
|
|
423
|
+
left: "50%",
|
|
424
|
+
transform: "translateX(-50%) translateY(-10px)",
|
|
425
|
+
zIndex: "9999999",
|
|
426
|
+
background: "#ef4444",
|
|
427
|
+
color: "#fff",
|
|
428
|
+
padding: "12px 24px",
|
|
429
|
+
borderRadius: "8px",
|
|
430
|
+
fontSize: "14px",
|
|
431
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
432
|
+
boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
|
|
433
|
+
opacity: "0",
|
|
434
|
+
transition: "opacity 0.2s, transform 0.2s",
|
|
435
|
+
maxWidth: "90vw",
|
|
436
|
+
textAlign: "center"
|
|
437
|
+
});
|
|
438
|
+
document.body.appendChild(toast);
|
|
439
|
+
toast.offsetHeight;
|
|
440
|
+
toast.style.opacity = "1";
|
|
441
|
+
toast.style.transform = "translateX(-50%) translateY(0)";
|
|
442
|
+
setTimeout(() => {
|
|
443
|
+
toast.style.opacity = "0";
|
|
444
|
+
toast.style.transform = "translateX(-50%) translateY(-10px)";
|
|
445
|
+
setTimeout(() => toast.parentNode?.removeChild(toast), 200);
|
|
446
|
+
}, 6e3);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/checkout.ts
|
|
450
|
+
var CoinCircuitCheckout = class {
|
|
451
|
+
baseUrl;
|
|
452
|
+
origin;
|
|
453
|
+
publicKey;
|
|
454
|
+
overlay = null;
|
|
455
|
+
removeMessageListener = null;
|
|
456
|
+
loadTimeout = null;
|
|
457
|
+
openedAt = 0;
|
|
458
|
+
constructor(config = {}) {
|
|
459
|
+
this.baseUrl = config.baseUrl ? validateBaseUrl(config.baseUrl) : DEFAULT_BASE_URL;
|
|
460
|
+
this.origin = extractOrigin(this.baseUrl);
|
|
461
|
+
this.publicKey = config.publicKey;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Opens the checkout modal overlay.
|
|
465
|
+
*
|
|
466
|
+
* @param options - Configuration for this checkout session.
|
|
467
|
+
* @throws If reference is missing.
|
|
468
|
+
*/
|
|
469
|
+
open(options) {
|
|
470
|
+
this.close();
|
|
471
|
+
if (!options.reference) {
|
|
472
|
+
throw new Error('CoinCircuitCheckout: "reference" is required.');
|
|
473
|
+
}
|
|
474
|
+
this.openedAt = performance.now();
|
|
475
|
+
console.log("[CoinCircuit] open() called \u2014 waiting for coincircuit:ready");
|
|
476
|
+
const checkoutUrl = buildCheckoutUrl(
|
|
477
|
+
this.baseUrl,
|
|
478
|
+
options.reference,
|
|
479
|
+
options.theme
|
|
480
|
+
);
|
|
481
|
+
const { overlay, iframe } = createOverlay(checkoutUrl, () => {
|
|
482
|
+
options.onClose?.();
|
|
483
|
+
this.close();
|
|
484
|
+
}, options.theme);
|
|
485
|
+
this.overlay = overlay;
|
|
486
|
+
this.loadTimeout = setTimeout(() => {
|
|
487
|
+
showErrorToast(
|
|
488
|
+
"Checkout failed to load."
|
|
489
|
+
);
|
|
490
|
+
this.close();
|
|
491
|
+
}, 3e4);
|
|
492
|
+
iframe.addEventListener("error", () => {
|
|
493
|
+
if (this.loadTimeout) clearTimeout(this.loadTimeout);
|
|
494
|
+
console.error(`[CoinCircuit] iframe error after ${Math.round(performance.now() - this.openedAt)}ms`);
|
|
495
|
+
showErrorToast("Checkout failed to load. Please try again.");
|
|
496
|
+
this.close();
|
|
497
|
+
});
|
|
498
|
+
iframe.addEventListener("load", () => {
|
|
499
|
+
console.log(`[CoinCircuit] iframe DOM loaded at ${Math.round(performance.now() - this.openedAt)}ms (still waiting for coincircuit:ready)`);
|
|
500
|
+
});
|
|
501
|
+
this.removeMessageListener = createMessageListener(this.origin, {
|
|
502
|
+
onReady: () => {
|
|
503
|
+
const elapsed = Math.round(performance.now() - this.openedAt);
|
|
504
|
+
console.log(`[CoinCircuit] coincircuit:ready received at ${elapsed}ms`);
|
|
505
|
+
if (this.loadTimeout) {
|
|
506
|
+
clearTimeout(this.loadTimeout);
|
|
507
|
+
this.loadTimeout = null;
|
|
508
|
+
}
|
|
509
|
+
if (this.overlay) hideSpinner(this.overlay);
|
|
510
|
+
options.onReady?.();
|
|
511
|
+
},
|
|
512
|
+
onPaymentComplete: (data) => {
|
|
513
|
+
console.log(`[CoinCircuit] coincircuit:payment_complete at ${Math.round(performance.now() - this.openedAt)}ms`);
|
|
514
|
+
options.onPaymentComplete?.(data);
|
|
515
|
+
},
|
|
516
|
+
onPaymentFailed: (data) => {
|
|
517
|
+
options.onPaymentFailed?.(data);
|
|
518
|
+
},
|
|
519
|
+
onExpired: () => {
|
|
520
|
+
options.onExpired?.();
|
|
521
|
+
},
|
|
522
|
+
onClose: () => {
|
|
523
|
+
options.onClose?.();
|
|
524
|
+
this.close();
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
showOverlay(overlay);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Closes the checkout modal and cleans up listeners.
|
|
531
|
+
*/
|
|
532
|
+
close() {
|
|
533
|
+
if (this.loadTimeout) {
|
|
534
|
+
clearTimeout(this.loadTimeout);
|
|
535
|
+
this.loadTimeout = null;
|
|
536
|
+
}
|
|
537
|
+
if (this.removeMessageListener) {
|
|
538
|
+
this.removeMessageListener();
|
|
539
|
+
this.removeMessageListener = null;
|
|
540
|
+
}
|
|
541
|
+
if (this.overlay) {
|
|
542
|
+
removeOverlay(this.overlay);
|
|
543
|
+
this.overlay = null;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Returns true if the checkout modal is currently open.
|
|
548
|
+
*/
|
|
549
|
+
isOpen() {
|
|
550
|
+
return this.overlay !== null;
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
export {
|
|
555
|
+
CHECKOUT_MESSAGE_TYPES,
|
|
556
|
+
createMessageListener,
|
|
557
|
+
DEFAULT_BASE_URL,
|
|
558
|
+
buildCheckoutUrl,
|
|
559
|
+
extractOrigin,
|
|
560
|
+
CoinCircuitCheckout
|
|
561
|
+
};
|