@coincircuit/checkout 0.4.1 → 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 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
+ };