@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/dist/chunk-RJ3FMO5K.js
DELETED
|
@@ -1,377 +0,0 @@
|
|
|
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/utils.ts
|
|
46
|
-
var DEFAULT_BASE_URL = "https://checkout.coincircuit.io";
|
|
47
|
-
var ALLOWED_BASE_URLS = [
|
|
48
|
-
"https://checkout.coincircuit.io",
|
|
49
|
-
"https://sandbox-checkout.coincircuit.io"
|
|
50
|
-
];
|
|
51
|
-
function validateBaseUrl(url) {
|
|
52
|
-
const normalized = url.replace(/\/+$/, "");
|
|
53
|
-
if (!ALLOWED_BASE_URLS.includes(normalized)) {
|
|
54
|
-
throw new Error(
|
|
55
|
-
`CoinCircuitCheckout: invalid baseUrl "${url}". Must be one of: ${ALLOWED_BASE_URLS.join(", ")}`
|
|
56
|
-
);
|
|
57
|
-
}
|
|
58
|
-
return normalized;
|
|
59
|
-
}
|
|
60
|
-
function buildCheckoutUrl(baseUrl, reference, theme) {
|
|
61
|
-
const url = new URL(`/pay/${encodeURIComponent(reference)}`, baseUrl);
|
|
62
|
-
url.searchParams.set("embed", "true");
|
|
63
|
-
if (theme) {
|
|
64
|
-
url.searchParams.set("theme", theme);
|
|
65
|
-
}
|
|
66
|
-
return url.toString();
|
|
67
|
-
}
|
|
68
|
-
function extractOrigin(baseUrl) {
|
|
69
|
-
try {
|
|
70
|
-
return new URL(baseUrl).origin;
|
|
71
|
-
} catch {
|
|
72
|
-
return baseUrl;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
var OVERLAY_ID = "coincircuit-checkout-overlay";
|
|
76
|
-
var STYLES_ID = "coincircuit-checkout-styles";
|
|
77
|
-
function injectStyles() {
|
|
78
|
-
if (document.getElementById(STYLES_ID)) return;
|
|
79
|
-
const style = document.createElement("style");
|
|
80
|
-
style.id = STYLES_ID;
|
|
81
|
-
style.textContent = `
|
|
82
|
-
#${OVERLAY_ID} {
|
|
83
|
-
position: fixed;
|
|
84
|
-
inset: 0;
|
|
85
|
-
z-index: 999999;
|
|
86
|
-
display: flex;
|
|
87
|
-
align-items: center;
|
|
88
|
-
justify-content: center;
|
|
89
|
-
background: rgba(0, 0, 0, 0.6);
|
|
90
|
-
backdrop-filter: blur(4px);
|
|
91
|
-
opacity: 0;
|
|
92
|
-
transition: opacity 0.2s ease;
|
|
93
|
-
}
|
|
94
|
-
#${OVERLAY_ID}.cc-visible {
|
|
95
|
-
opacity: 1;
|
|
96
|
-
}
|
|
97
|
-
#${OVERLAY_ID} .cc-modal {
|
|
98
|
-
position: relative;
|
|
99
|
-
width: min(600px, 96vw);
|
|
100
|
-
max-width: 600px;
|
|
101
|
-
height: min(88vh, 800px);
|
|
102
|
-
border-radius: 16px;
|
|
103
|
-
background: #fff;
|
|
104
|
-
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
|
|
105
|
-
opacity: 0;
|
|
106
|
-
transform: scale(0.96);
|
|
107
|
-
transition: opacity 0.25s ease, transform 0.25s ease;
|
|
108
|
-
}
|
|
109
|
-
#${OVERLAY_ID} .cc-modal.cc-ready {
|
|
110
|
-
opacity: 1;
|
|
111
|
-
transform: scale(1);
|
|
112
|
-
}
|
|
113
|
-
#${OVERLAY_ID} .cc-close {
|
|
114
|
-
position: fixed;
|
|
115
|
-
top: 16px;
|
|
116
|
-
right: 16px;
|
|
117
|
-
z-index: 1000000;
|
|
118
|
-
width: 36px;
|
|
119
|
-
height: 36px;
|
|
120
|
-
border: none;
|
|
121
|
-
border-radius: 50%;
|
|
122
|
-
background: rgba(255, 255, 255, 0.15);
|
|
123
|
-
backdrop-filter: blur(8px);
|
|
124
|
-
color: #fff;
|
|
125
|
-
font-size: 20px;
|
|
126
|
-
line-height: 1;
|
|
127
|
-
cursor: pointer;
|
|
128
|
-
display: flex;
|
|
129
|
-
align-items: center;
|
|
130
|
-
justify-content: center;
|
|
131
|
-
transition: background 0.15s, transform 0.15s;
|
|
132
|
-
}
|
|
133
|
-
#${OVERLAY_ID} .cc-close:hover {
|
|
134
|
-
background: rgba(255, 255, 255, 0.25);
|
|
135
|
-
transform: scale(1.1);
|
|
136
|
-
}
|
|
137
|
-
#${OVERLAY_ID} iframe {
|
|
138
|
-
width: 100%;
|
|
139
|
-
height: 100%;
|
|
140
|
-
border: none;
|
|
141
|
-
display: block;
|
|
142
|
-
}
|
|
143
|
-
#${OVERLAY_ID} .cc-spinner {
|
|
144
|
-
position: absolute;
|
|
145
|
-
inset: 0;
|
|
146
|
-
display: flex;
|
|
147
|
-
align-items: center;
|
|
148
|
-
justify-content: center;
|
|
149
|
-
z-index: 1;
|
|
150
|
-
transition: opacity 0.2s;
|
|
151
|
-
}
|
|
152
|
-
#${OVERLAY_ID} .cc-spinner div {
|
|
153
|
-
width: 36px;
|
|
154
|
-
height: 36px;
|
|
155
|
-
border: 3px solid rgba(255, 255, 255, 0.3);
|
|
156
|
-
border-top-color: #fff;
|
|
157
|
-
border-radius: 50%;
|
|
158
|
-
animation: cc-spin 0.8s linear infinite;
|
|
159
|
-
}
|
|
160
|
-
@keyframes cc-spin { to { transform: rotate(360deg); } }
|
|
161
|
-
@media (max-width: 720px) {
|
|
162
|
-
#${OVERLAY_ID} .cc-modal {
|
|
163
|
-
width: 100%;
|
|
164
|
-
max-width: 100%;
|
|
165
|
-
height: 100dvh;
|
|
166
|
-
border-radius: 0;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
`;
|
|
170
|
-
document.head.appendChild(style);
|
|
171
|
-
}
|
|
172
|
-
function createOverlay(checkoutUrl, onCloseClick) {
|
|
173
|
-
injectStyles();
|
|
174
|
-
const overlay = document.createElement("div");
|
|
175
|
-
overlay.id = OVERLAY_ID;
|
|
176
|
-
overlay.addEventListener("click", (e) => {
|
|
177
|
-
if (e.target === overlay) onCloseClick();
|
|
178
|
-
});
|
|
179
|
-
const onKeydown = (e) => {
|
|
180
|
-
if (e.key === "Escape") onCloseClick();
|
|
181
|
-
};
|
|
182
|
-
document.addEventListener("keydown", onKeydown);
|
|
183
|
-
overlay.dataset.keydownAttached = "true";
|
|
184
|
-
overlay._removeKeydown = () => document.removeEventListener("keydown", onKeydown);
|
|
185
|
-
const modal = document.createElement("div");
|
|
186
|
-
modal.className = "cc-modal";
|
|
187
|
-
const spinner = document.createElement("div");
|
|
188
|
-
spinner.className = "cc-spinner";
|
|
189
|
-
const spinnerRing = document.createElement("div");
|
|
190
|
-
spinner.appendChild(spinnerRing);
|
|
191
|
-
const iframe = document.createElement("iframe");
|
|
192
|
-
iframe.src = checkoutUrl;
|
|
193
|
-
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox");
|
|
194
|
-
iframe.setAttribute("referrerpolicy", "strict-origin-when-cross-origin");
|
|
195
|
-
iframe.setAttribute("title", "CoinCircuit Checkout");
|
|
196
|
-
iframe.setAttribute("allow", "payment");
|
|
197
|
-
const closeBtn = document.createElement("button");
|
|
198
|
-
closeBtn.className = "cc-close";
|
|
199
|
-
closeBtn.setAttribute("aria-label", "Close checkout");
|
|
200
|
-
closeBtn.textContent = "\u2715";
|
|
201
|
-
closeBtn.addEventListener("click", onCloseClick);
|
|
202
|
-
modal.appendChild(iframe);
|
|
203
|
-
overlay.appendChild(spinner);
|
|
204
|
-
overlay.appendChild(modal);
|
|
205
|
-
overlay.appendChild(closeBtn);
|
|
206
|
-
return { overlay, iframe };
|
|
207
|
-
}
|
|
208
|
-
function showOverlay(overlay) {
|
|
209
|
-
document.body.appendChild(overlay);
|
|
210
|
-
document.body.style.overflow = "hidden";
|
|
211
|
-
overlay.offsetHeight;
|
|
212
|
-
overlay.classList.add("cc-visible");
|
|
213
|
-
}
|
|
214
|
-
function removeOverlay(overlay) {
|
|
215
|
-
overlay._removeKeydown?.();
|
|
216
|
-
overlay.classList.remove("cc-visible");
|
|
217
|
-
document.body.style.overflow = "";
|
|
218
|
-
setTimeout(() => {
|
|
219
|
-
overlay.parentNode?.removeChild(overlay);
|
|
220
|
-
}, 200);
|
|
221
|
-
}
|
|
222
|
-
function hideSpinner(overlay) {
|
|
223
|
-
const spinner = overlay.querySelector(".cc-spinner");
|
|
224
|
-
if (spinner) {
|
|
225
|
-
spinner.style.opacity = "0";
|
|
226
|
-
setTimeout(() => spinner.parentNode?.removeChild(spinner), 200);
|
|
227
|
-
}
|
|
228
|
-
const modal = overlay.querySelector(".cc-modal");
|
|
229
|
-
if (modal) {
|
|
230
|
-
modal.classList.add("cc-ready");
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
function showErrorToast(message) {
|
|
234
|
-
const toast = document.createElement("div");
|
|
235
|
-
toast.textContent = message;
|
|
236
|
-
Object.assign(toast.style, {
|
|
237
|
-
position: "fixed",
|
|
238
|
-
top: "20px",
|
|
239
|
-
left: "50%",
|
|
240
|
-
transform: "translateX(-50%) translateY(-10px)",
|
|
241
|
-
zIndex: "9999999",
|
|
242
|
-
background: "#ef4444",
|
|
243
|
-
color: "#fff",
|
|
244
|
-
padding: "12px 24px",
|
|
245
|
-
borderRadius: "8px",
|
|
246
|
-
fontSize: "14px",
|
|
247
|
-
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
248
|
-
boxShadow: "0 4px 12px rgba(0,0,0,0.3)",
|
|
249
|
-
opacity: "0",
|
|
250
|
-
transition: "opacity 0.2s, transform 0.2s",
|
|
251
|
-
maxWidth: "90vw",
|
|
252
|
-
textAlign: "center"
|
|
253
|
-
});
|
|
254
|
-
document.body.appendChild(toast);
|
|
255
|
-
toast.offsetHeight;
|
|
256
|
-
toast.style.opacity = "1";
|
|
257
|
-
toast.style.transform = "translateX(-50%) translateY(0)";
|
|
258
|
-
setTimeout(() => {
|
|
259
|
-
toast.style.opacity = "0";
|
|
260
|
-
toast.style.transform = "translateX(-50%) translateY(-10px)";
|
|
261
|
-
setTimeout(() => toast.parentNode?.removeChild(toast), 200);
|
|
262
|
-
}, 6e3);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// src/checkout.ts
|
|
266
|
-
var CoinCircuitCheckout = class {
|
|
267
|
-
baseUrl;
|
|
268
|
-
origin;
|
|
269
|
-
publicKey;
|
|
270
|
-
overlay = null;
|
|
271
|
-
removeMessageListener = null;
|
|
272
|
-
loadTimeout = null;
|
|
273
|
-
openedAt = 0;
|
|
274
|
-
constructor(config = {}) {
|
|
275
|
-
this.baseUrl = config.baseUrl ? validateBaseUrl(config.baseUrl) : DEFAULT_BASE_URL;
|
|
276
|
-
this.origin = extractOrigin(this.baseUrl);
|
|
277
|
-
this.publicKey = config.publicKey;
|
|
278
|
-
}
|
|
279
|
-
/**
|
|
280
|
-
* Opens the checkout modal overlay.
|
|
281
|
-
*
|
|
282
|
-
* @param options - Configuration for this checkout session.
|
|
283
|
-
* @throws If reference is missing.
|
|
284
|
-
*/
|
|
285
|
-
open(options) {
|
|
286
|
-
this.close();
|
|
287
|
-
if (!options.reference) {
|
|
288
|
-
throw new Error('CoinCircuitCheckout: "reference" is required.');
|
|
289
|
-
}
|
|
290
|
-
this.openedAt = performance.now();
|
|
291
|
-
console.log("[CoinCircuit] open() called \u2014 waiting for coincircuit:ready");
|
|
292
|
-
const checkoutUrl = buildCheckoutUrl(
|
|
293
|
-
this.baseUrl,
|
|
294
|
-
options.reference,
|
|
295
|
-
options.theme
|
|
296
|
-
);
|
|
297
|
-
const { overlay, iframe } = createOverlay(checkoutUrl, () => {
|
|
298
|
-
options.onClose?.();
|
|
299
|
-
this.close();
|
|
300
|
-
});
|
|
301
|
-
this.overlay = overlay;
|
|
302
|
-
this.loadTimeout = setTimeout(() => {
|
|
303
|
-
showErrorToast(
|
|
304
|
-
"Checkout failed to load."
|
|
305
|
-
);
|
|
306
|
-
this.close();
|
|
307
|
-
}, 15e3);
|
|
308
|
-
iframe.addEventListener("error", () => {
|
|
309
|
-
if (this.loadTimeout) clearTimeout(this.loadTimeout);
|
|
310
|
-
console.error(`[CoinCircuit] iframe error after ${Math.round(performance.now() - this.openedAt)}ms`);
|
|
311
|
-
showErrorToast("Checkout failed to load. Please try again.");
|
|
312
|
-
this.close();
|
|
313
|
-
});
|
|
314
|
-
iframe.addEventListener("load", () => {
|
|
315
|
-
console.log(`[CoinCircuit] iframe DOM loaded at ${Math.round(performance.now() - this.openedAt)}ms (still waiting for coincircuit:ready)`);
|
|
316
|
-
});
|
|
317
|
-
this.removeMessageListener = createMessageListener(this.origin, {
|
|
318
|
-
onReady: () => {
|
|
319
|
-
const elapsed = Math.round(performance.now() - this.openedAt);
|
|
320
|
-
console.log(`[CoinCircuit] coincircuit:ready received at ${elapsed}ms`);
|
|
321
|
-
if (this.loadTimeout) {
|
|
322
|
-
clearTimeout(this.loadTimeout);
|
|
323
|
-
this.loadTimeout = null;
|
|
324
|
-
}
|
|
325
|
-
if (this.overlay) hideSpinner(this.overlay);
|
|
326
|
-
options.onReady?.();
|
|
327
|
-
},
|
|
328
|
-
onPaymentComplete: (data) => {
|
|
329
|
-
console.log(`[CoinCircuit] coincircuit:payment_complete at ${Math.round(performance.now() - this.openedAt)}ms`);
|
|
330
|
-
options.onPaymentComplete?.(data);
|
|
331
|
-
},
|
|
332
|
-
onPaymentFailed: (data) => {
|
|
333
|
-
options.onPaymentFailed?.(data);
|
|
334
|
-
},
|
|
335
|
-
onExpired: () => {
|
|
336
|
-
options.onExpired?.();
|
|
337
|
-
},
|
|
338
|
-
onClose: () => {
|
|
339
|
-
options.onClose?.();
|
|
340
|
-
this.close();
|
|
341
|
-
}
|
|
342
|
-
});
|
|
343
|
-
showOverlay(overlay);
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Closes the checkout modal and cleans up listeners.
|
|
347
|
-
*/
|
|
348
|
-
close() {
|
|
349
|
-
if (this.loadTimeout) {
|
|
350
|
-
clearTimeout(this.loadTimeout);
|
|
351
|
-
this.loadTimeout = null;
|
|
352
|
-
}
|
|
353
|
-
if (this.removeMessageListener) {
|
|
354
|
-
this.removeMessageListener();
|
|
355
|
-
this.removeMessageListener = null;
|
|
356
|
-
}
|
|
357
|
-
if (this.overlay) {
|
|
358
|
-
removeOverlay(this.overlay);
|
|
359
|
-
this.overlay = null;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* Returns true if the checkout modal is currently open.
|
|
364
|
-
*/
|
|
365
|
-
isOpen() {
|
|
366
|
-
return this.overlay !== null;
|
|
367
|
-
}
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
export {
|
|
371
|
-
CHECKOUT_MESSAGE_TYPES,
|
|
372
|
-
createMessageListener,
|
|
373
|
-
DEFAULT_BASE_URL,
|
|
374
|
-
buildCheckoutUrl,
|
|
375
|
-
extractOrigin,
|
|
376
|
-
CoinCircuitCheckout
|
|
377
|
-
};
|