@billing-io/designs 1.0.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.
@@ -0,0 +1,876 @@
1
+ /**
2
+ * billing.js — Checkout overlay for billing.io
3
+ *
4
+ * Load via:
5
+ * <script src="https://js.billing.io/v1/billing.js"></script>
6
+ *
7
+ * Usage:
8
+ * billing.openCheckout({
9
+ * checkoutId: "co_123",
10
+ * variation: "centered", // "centered" | "panel" | "bottom" | "fullscreen" | "popup"
11
+ * onSuccess: ({ checkoutId, txHash }) => {},
12
+ * onClose: () => {},
13
+ * onError: (err) => {}
14
+ * });
15
+ *
16
+ * billing.redirectToCheckout({ checkoutId: "co_123" });
17
+ */
18
+ (function () {
19
+ "use strict";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Config
23
+ // ---------------------------------------------------------------------------
24
+
25
+ var BILLING_HOST = (function () {
26
+ if (document.currentScript && document.currentScript.getAttribute("data-host")) {
27
+ return document.currentScript.getAttribute("data-host");
28
+ }
29
+ if (window.__BILLING_HOST__) return window.__BILLING_HOST__;
30
+ if (document.currentScript && document.currentScript.src) {
31
+ try { return new URL(document.currentScript.src).origin; } catch (e) { /* noop */ }
32
+ }
33
+ return window.location.origin;
34
+ })();
35
+
36
+ var NAMESPACE = "billing";
37
+ var VERSION = "1.0.0";
38
+ var MSG_PREFIX = "billing:";
39
+ var Z_INDEX = 2147483647;
40
+
41
+ // Spring-like easing curves
42
+ var EASE_OUT_EXPO = "cubic-bezier(0.16, 1, 0.3, 1)";
43
+ var EASE_OUT_QUINT = "cubic-bezier(0.22, 1, 0.36, 1)";
44
+ var EASE_IN_EXPO = "cubic-bezier(0.7, 0, 0.84, 0)";
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // State
48
+ // ---------------------------------------------------------------------------
49
+
50
+ var _overlay = null;
51
+ var _iframe = null;
52
+ var _callbacks = {};
53
+ var _currentCheckoutId = null;
54
+ var _variation = parseInt(localStorage.getItem("billing_overlay_variation") || "1", 10);
55
+ var _isOpen = false;
56
+ var _lastStatus = null;
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Variation name ↔ number mapping
60
+ // ---------------------------------------------------------------------------
61
+
62
+ var VARIATION_NAMES = {
63
+ centered: 1, card: 1,
64
+ panel: 2, side: 2, sidepanel: 2,
65
+ bottom: 3, sheet: 3, bottomsheet: 3,
66
+ fullscreen: 4, full: 4, takeover: 4,
67
+ popup: 5, floating: 5, pill: 5,
68
+ };
69
+
70
+ function resolveVariation(v) {
71
+ if (typeof v === "number" && VARIATIONS[v]) return v;
72
+ if (typeof v === "string") {
73
+ var n = VARIATION_NAMES[v.toLowerCase().replace(/[\s_-]/g, "")];
74
+ if (n) return n;
75
+ }
76
+ return _variation;
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Variation definitions
81
+ // ---------------------------------------------------------------------------
82
+
83
+ var VARIATIONS = {
84
+ // V1: Centered card (Stripe-like)
85
+ 1: {
86
+ name: "Centered Card",
87
+ desc: "Classic centered modal, backdrop blur",
88
+ build: buildCenteredModal,
89
+ },
90
+ // V2: Right slide-in panel
91
+ 2: {
92
+ name: "Side Panel",
93
+ desc: "Right-anchored slide-in sheet",
94
+ build: buildSidePanel,
95
+ },
96
+ // V3: Bottom sheet (mobile-first)
97
+ 3: {
98
+ name: "Bottom Sheet",
99
+ desc: "Mobile-friendly bottom sheet",
100
+ build: buildBottomSheet,
101
+ },
102
+ // V4: Full-screen takeover
103
+ 4: {
104
+ name: "Fullscreen",
105
+ desc: "Immersive full-screen overlay",
106
+ build: buildFullscreen,
107
+ },
108
+ // V5: Floating popup (compact)
109
+ 5: {
110
+ name: "Floating Popup",
111
+ desc: "Compact bottom-right popup",
112
+ build: buildFloatingPopup,
113
+ },
114
+ };
115
+
116
+ // ---------------------------------------------------------------------------
117
+ // Shared helpers
118
+ // ---------------------------------------------------------------------------
119
+
120
+ function raf(fn) {
121
+ requestAnimationFrame(function () { requestAnimationFrame(fn); });
122
+ }
123
+
124
+ function styleIframe(iframe, css) {
125
+ iframe.style.cssText = css;
126
+ iframe.setAttribute("allow", "clipboard-write");
127
+ iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-popups allow-forms allow-clipboard-write");
128
+ }
129
+
130
+ function lockScroll() {
131
+ document.body.style.overflow = "hidden";
132
+ document.body.style.touchAction = "none";
133
+ }
134
+
135
+ function unlockScroll() {
136
+ document.body.style.overflow = "";
137
+ document.body.style.touchAction = "";
138
+ }
139
+
140
+ function buildCloseButton(onClick) {
141
+ var btn = document.createElement("button");
142
+ btn.type = "button";
143
+ btn.setAttribute("aria-label", "Close checkout");
144
+ btn.style.cssText =
145
+ "width:32px;height:32px;border-radius:10px;border:none;" +
146
+ "background:#f3f4f6;cursor:pointer;display:flex;align-items:center;" +
147
+ "justify-content:center;transition:background 0.15s ease, transform 0.15s ease;" +
148
+ "color:#6b7280;font-family:-apple-system,BlinkMacSystemFont,sans-serif;" +
149
+ "outline:none;";
150
+ btn.innerHTML =
151
+ '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">' +
152
+ '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
153
+ btn.addEventListener("mouseenter", function () { btn.style.background = "#e5e7eb"; btn.style.transform = "scale(1.05)"; });
154
+ btn.addEventListener("mouseleave", function () { btn.style.background = "#f3f4f6"; btn.style.transform = "scale(1)"; });
155
+ btn.addEventListener("mousedown", function () { btn.style.transform = "scale(0.95)"; });
156
+ btn.addEventListener("mouseup", function () { btn.style.transform = "scale(1.05)"; });
157
+ btn.addEventListener("click", function (e) { e.stopPropagation(); onClick(); });
158
+ return btn;
159
+ }
160
+
161
+ function buildBrandBadge(size) {
162
+ var s = size || "md";
163
+ var dim = s === "sm" ? 18 : s === "lg" ? 24 : 20;
164
+ var br = s === "sm" ? 4 : s === "lg" ? 6 : 5;
165
+ return '<div style="width:' + dim + 'px;height:' + dim + 'px;background:#000;border-radius:' + br + 'px;display:flex;align-items:center;justify-content:center;overflow:hidden;">' +
166
+ '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="' + dim + '" height="' + dim + '">' +
167
+ '<rect width="1024" height="1024" fill="#000"/>' +
168
+ '<path fill="#fff" d="M648.077,530.65c0,46.2-18.34,81.95-55,107.25-25.3,17.239-51.7,25.851-79.2,25.851-10.27,0-25.119-3.301-44.55-9.9-10.27-3.661-18.149-5.5-23.649-5.5-9.9,0-19.989,3.67-30.25,11-2.2,1.461-3.851,2.2-4.95,2.2-2.569,0-4.039-1.101-4.4-3.3-.369-2.2-.55-8.801-.55-19.801v-322.85c0-9.53-1.719-15.761-5.156-18.7-3.438-2.931-10.948-4.4-22.533-4.4-9.771,0-14.66-1.28-14.66-3.85s2.234-3.85,6.694-3.85c41.653,0,77.919-4.581,108.797-13.75l.559,8.8v162.25c14.618-20.161,34.899-30.25,60.853-30.25,31.427,0,57.285,11.095,77.567,33.274,20.281,22.189,30.43,50.695,30.43,85.525ZM566.127,532.851c0-71.131-14.85-106.7-44.55-106.7-16.5,0-28.6,7.7-36.3,23.1-2.939,5.87-4.675,12.745-5.225,20.625-.551,7.89-.825,30.345-.825,67.375v33.551c0,34.469,2.286,57.294,6.875,68.475,4.58,11.189,13.836,16.775,27.774,16.775,19.062,0,32.536-9.351,40.426-28.051,7.88-18.699,11.824-50.41,11.824-95.149Z"/>' +
169
+ '<path fill="#fff" d="M692.003,642.041c0,7.235-2.336,13.186-7,17.851-4.671,4.665-10.621,7-17.851,7s-13.3-2.396-18.2-7.175c-4.899-4.78-7.35-10.676-7.35-17.676,0-7.229,2.45-13.3,7.35-18.199s10.965-7.351,18.2-7.351c6.765,0,12.601,2.45,17.5,7.351,4.9,4.899,7.351,10.97,7.351,18.199Z"/>' +
170
+ '</svg></div>';
171
+ }
172
+
173
+ // ---------------------------------------------------------------------------
174
+ // V1: Centered Card — staged: backdrop fades+blurs → card rises from below
175
+ // ---------------------------------------------------------------------------
176
+
177
+ function buildCenteredModal(iframe, onClose) {
178
+ var root = document.createElement("div");
179
+ root.setAttribute("data-billing-overlay", "");
180
+ root.style.cssText =
181
+ "position:fixed;inset:0;z-index:" + Z_INDEX +
182
+ ";display:flex;align-items:center;justify-content:center;padding:16px;";
183
+
184
+ // Backdrop — starts transparent + no blur
185
+ var backdrop = document.createElement("div");
186
+ backdrop.style.cssText =
187
+ "position:absolute;inset:0;" +
188
+ "background:rgba(0,0,0,0);" +
189
+ "backdrop-filter:blur(0px);-webkit-backdrop-filter:blur(0px);" +
190
+ "transition:background 0.35s ease, backdrop-filter 0.4s ease, -webkit-backdrop-filter 0.4s ease;";
191
+ backdrop.addEventListener("click", onClose);
192
+ root.appendChild(backdrop);
193
+
194
+ // Card — starts invisible, below, scaled down
195
+ var card = document.createElement("div");
196
+ card.style.cssText =
197
+ "position:relative;width:100%;max-width:460px;max-height:680px;" +
198
+ "border-radius:20px;overflow:hidden;background:#fff;" +
199
+ "box-shadow:0 0 0 1px rgba(0,0,0,0.04);" +
200
+ "opacity:0;transform:translateY(24px) scale(0.97);" +
201
+ "transition:opacity 0.4s ease, transform 0.5s " + EASE_OUT_EXPO + ", box-shadow 0.5s ease;";
202
+
203
+ // No close button on the card itself — the checkout page header already
204
+ // occupies that corner (Secure badge). Close via backdrop click or Escape.
205
+
206
+ styleIframe(iframe, "width:100%;height:100%;min-height:600px;border:none;display:block;");
207
+ card.appendChild(iframe);
208
+ root.appendChild(card);
209
+
210
+ // Stage 1 (0ms): backdrop fades in + blur
211
+ raf(function () {
212
+ backdrop.style.background = "rgba(0,0,0,0.4)";
213
+ backdrop.style.backdropFilter = "blur(12px)";
214
+ backdrop.style.webkitBackdropFilter = "blur(12px)";
215
+
216
+ // Stage 2 (120ms delay): card rises in
217
+ setTimeout(function () {
218
+ card.style.opacity = "1";
219
+ card.style.transform = "translateY(0) scale(1)";
220
+ card.style.boxShadow = "0 25px 60px -12px rgba(0,0,0,0.25), 0 0 0 1px rgba(0,0,0,0.05)";
221
+ }, 120);
222
+ });
223
+
224
+ root._animateOut = function (cb) {
225
+ card.style.opacity = "0";
226
+ card.style.transform = "translateY(12px) scale(0.98)";
227
+ card.style.boxShadow = "0 0 0 1px rgba(0,0,0,0.04)";
228
+
229
+ setTimeout(function () {
230
+ backdrop.style.background = "rgba(0,0,0,0)";
231
+ backdrop.style.backdropFilter = "blur(0px)";
232
+ backdrop.style.webkitBackdropFilter = "blur(0px)";
233
+ }, 80);
234
+
235
+ setTimeout(cb, 380);
236
+ };
237
+
238
+ return root;
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // V2: Side Panel — staged: backdrop dims → panel slides in from right
243
+ // ---------------------------------------------------------------------------
244
+
245
+ function buildSidePanel(iframe, onClose) {
246
+ var root = document.createElement("div");
247
+ root.setAttribute("data-billing-overlay", "");
248
+ root.style.cssText = "position:fixed;inset:0;z-index:" + Z_INDEX + ";";
249
+
250
+ // Backdrop
251
+ var backdrop = document.createElement("div");
252
+ backdrop.style.cssText =
253
+ "position:absolute;inset:0;background:rgba(0,0,0,0);" +
254
+ "backdrop-filter:blur(0px);-webkit-backdrop-filter:blur(0px);" +
255
+ "transition:background 0.4s ease, backdrop-filter 0.45s ease, -webkit-backdrop-filter 0.45s ease;";
256
+ backdrop.addEventListener("click", onClose);
257
+ root.appendChild(backdrop);
258
+
259
+ // Panel
260
+ var panel = document.createElement("div");
261
+ panel.style.cssText =
262
+ "position:absolute;top:0;right:0;bottom:0;width:460px;max-width:100%;" +
263
+ "background:#fff;box-shadow:0 0 0 transparent;" +
264
+ "transform:translateX(100%);" +
265
+ "transition:transform 0.45s " + EASE_OUT_EXPO + ", box-shadow 0.45s ease;" +
266
+ "display:flex;flex-direction:column;";
267
+
268
+ // Header
269
+ var header = document.createElement("div");
270
+ header.style.cssText =
271
+ "display:flex;align-items:center;justify-content:space-between;" +
272
+ "padding:16px 20px;border-bottom:1px solid #e5e7eb;flex-shrink:0;background:#fff;" +
273
+ "opacity:0;transform:translateX(12px);" +
274
+ "transition:opacity 0.3s ease 0.25s, transform 0.4s " + EASE_OUT_EXPO + " 0.25s;";
275
+
276
+ var title = document.createElement("div");
277
+ title.style.cssText = "display:flex;align-items:center;gap:8px;";
278
+ title.innerHTML = buildBrandBadge("md") +
279
+ '<span style="font-size:14px;font-weight:600;color:#111827;font-family:-apple-system,BlinkMacSystemFont,sans-serif;">Secure Checkout</span>';
280
+ header.appendChild(title);
281
+ header.appendChild(buildCloseButton(onClose));
282
+ panel.appendChild(header);
283
+
284
+ styleIframe(iframe, "flex:1;width:100%;border:none;display:block;opacity:0;transition:opacity 0.35s ease 0.35s;");
285
+ panel.appendChild(iframe);
286
+ root.appendChild(panel);
287
+
288
+ // Stage 1: backdrop
289
+ raf(function () {
290
+ backdrop.style.background = "rgba(0,0,0,0.3)";
291
+ backdrop.style.backdropFilter = "blur(6px)";
292
+ backdrop.style.webkitBackdropFilter = "blur(6px)";
293
+
294
+ // Stage 2 (80ms): panel slides in
295
+ setTimeout(function () {
296
+ panel.style.transform = "translateX(0)";
297
+ panel.style.boxShadow = "-8px 0 40px rgba(0,0,0,0.12)";
298
+ header.style.opacity = "1";
299
+ header.style.transform = "translateX(0)";
300
+ iframe.style.opacity = "1";
301
+ }, 80);
302
+ });
303
+
304
+ root._animateOut = function (cb) {
305
+ iframe.style.opacity = "0";
306
+ iframe.style.transition = "opacity 0.15s ease";
307
+ header.style.opacity = "0";
308
+ header.style.transform = "translateX(8px)";
309
+ header.style.transition = "opacity 0.15s ease, transform 0.2s ease";
310
+
311
+ setTimeout(function () {
312
+ panel.style.transform = "translateX(100%)";
313
+ panel.style.boxShadow = "0 0 0 transparent";
314
+ backdrop.style.background = "rgba(0,0,0,0)";
315
+ backdrop.style.backdropFilter = "blur(0px)";
316
+ backdrop.style.webkitBackdropFilter = "blur(0px)";
317
+ }, 60);
318
+
319
+ setTimeout(cb, 420);
320
+ };
321
+
322
+ return root;
323
+ }
324
+
325
+ // ---------------------------------------------------------------------------
326
+ // V3: Bottom Sheet — staged: backdrop → sheet rises with spring
327
+ // ---------------------------------------------------------------------------
328
+
329
+ function buildBottomSheet(iframe, onClose) {
330
+ var root = document.createElement("div");
331
+ root.setAttribute("data-billing-overlay", "");
332
+ root.style.cssText =
333
+ "position:fixed;inset:0;z-index:" + Z_INDEX +
334
+ ";display:flex;align-items:flex-end;justify-content:center;";
335
+
336
+ // Backdrop
337
+ var backdrop = document.createElement("div");
338
+ backdrop.style.cssText =
339
+ "position:absolute;inset:0;background:rgba(0,0,0,0);" +
340
+ "backdrop-filter:blur(0px);-webkit-backdrop-filter:blur(0px);" +
341
+ "transition:background 0.35s ease, backdrop-filter 0.4s ease, -webkit-backdrop-filter 0.4s ease;";
342
+ backdrop.addEventListener("click", onClose);
343
+ root.appendChild(backdrop);
344
+
345
+ // Sheet
346
+ var sheet = document.createElement("div");
347
+ sheet.style.cssText =
348
+ "position:relative;width:100%;max-width:500px;max-height:92vh;" +
349
+ "border-radius:20px 20px 0 0;background:#fff;" +
350
+ "box-shadow:0 0 0 transparent;" +
351
+ "transform:translateY(105%);" +
352
+ "transition:transform 0.5s " + EASE_OUT_EXPO + ", box-shadow 0.5s ease;" +
353
+ "display:flex;flex-direction:column;overflow:hidden;";
354
+
355
+ // Drag handle
356
+ var handle = document.createElement("div");
357
+ handle.style.cssText =
358
+ "flex-shrink:0;display:flex;justify-content:center;padding:12px 0 4px;" +
359
+ "opacity:0;transition:opacity 0.3s ease 0.3s;";
360
+ handle.innerHTML = '<div style="width:36px;height:4px;border-radius:2px;background:#d1d5db;"></div>';
361
+ sheet.appendChild(handle);
362
+
363
+ // No close button overlaying the iframe — close via drag handle area,
364
+ // backdrop click, or Escape key. Keeps the checkout header clean.
365
+
366
+ styleIframe(iframe, "flex:1;width:100%;border:none;display:block;min-height:580px;");
367
+ sheet.appendChild(iframe);
368
+ root.appendChild(sheet);
369
+
370
+ // Stage 1: backdrop
371
+ raf(function () {
372
+ backdrop.style.background = "rgba(0,0,0,0.4)";
373
+ backdrop.style.backdropFilter = "blur(12px)";
374
+ backdrop.style.webkitBackdropFilter = "blur(12px)";
375
+
376
+ // Stage 2 (100ms): sheet springs up
377
+ setTimeout(function () {
378
+ sheet.style.transform = "translateY(0)";
379
+ sheet.style.boxShadow = "0 -10px 50px rgba(0,0,0,0.15)";
380
+ handle.style.opacity = "1";
381
+ }, 100);
382
+ });
383
+
384
+ root._animateOut = function (cb) {
385
+ handle.style.opacity = "0";
386
+ handle.style.transition = "opacity 0.1s ease";
387
+
388
+ setTimeout(function () {
389
+ sheet.style.transform = "translateY(105%)";
390
+ sheet.style.boxShadow = "0 0 0 transparent";
391
+ }, 50);
392
+
393
+ setTimeout(function () {
394
+ backdrop.style.background = "rgba(0,0,0,0)";
395
+ backdrop.style.backdropFilter = "blur(0px)";
396
+ backdrop.style.webkitBackdropFilter = "blur(0px)";
397
+ }, 150);
398
+
399
+ setTimeout(cb, 450);
400
+ };
401
+
402
+ return root;
403
+ }
404
+
405
+ // ---------------------------------------------------------------------------
406
+ // V4: Fullscreen — staged: bg wipes in → top bar drops → card fades up
407
+ // ---------------------------------------------------------------------------
408
+
409
+ function buildFullscreen(iframe, onClose) {
410
+ var root = document.createElement("div");
411
+ root.setAttribute("data-billing-overlay", "");
412
+ root.style.cssText =
413
+ "position:fixed;inset:0;z-index:" + Z_INDEX +
414
+ ";background:rgba(247,248,250,0);display:flex;flex-direction:column;" +
415
+ "transition:background 0.4s ease;";
416
+
417
+ // Top bar
418
+ var topBar = document.createElement("div");
419
+ topBar.style.cssText =
420
+ "flex-shrink:0;display:flex;align-items:center;justify-content:space-between;" +
421
+ "padding:16px 24px;background:#fff;border-bottom:1px solid #e5e7eb;" +
422
+ "opacity:0;transform:translateY(-100%);" +
423
+ "transition:opacity 0.35s ease 0.15s, transform 0.45s " + EASE_OUT_EXPO + " 0.15s;";
424
+
425
+ var brand = document.createElement("div");
426
+ brand.style.cssText = "display:flex;align-items:center;gap:10px;";
427
+ brand.innerHTML = buildBrandBadge("lg") +
428
+ '<span style="font-size:15px;font-weight:600;color:#111827;font-family:-apple-system,BlinkMacSystemFont,sans-serif;">billing.io checkout</span>';
429
+ topBar.appendChild(brand);
430
+
431
+ var rightGroup = document.createElement("div");
432
+ rightGroup.style.cssText = "display:flex;align-items:center;gap:12px;";
433
+
434
+ var secureBadge = document.createElement("div");
435
+ secureBadge.style.cssText =
436
+ "display:flex;align-items:center;gap:6px;padding:6px 12px;" +
437
+ "background:#f3f4f6;border-radius:20px;font-size:12px;color:#4b5563;" +
438
+ "font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-weight:500;";
439
+ secureBadge.innerHTML =
440
+ '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">' +
441
+ '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>' +
442
+ '<span>Secure</span>';
443
+ rightGroup.appendChild(secureBadge);
444
+ rightGroup.appendChild(buildCloseButton(onClose));
445
+ topBar.appendChild(rightGroup);
446
+ root.appendChild(topBar);
447
+
448
+ // Iframe wrapper
449
+ var wrapper = document.createElement("div");
450
+ wrapper.style.cssText =
451
+ "flex:1;display:flex;align-items:center;justify-content:center;padding:24px;overflow:auto;";
452
+
453
+ var container = document.createElement("div");
454
+ container.style.cssText =
455
+ "width:100%;max-width:460px;height:100%;max-height:680px;" +
456
+ "border-radius:20px;overflow:hidden;background:#fff;" +
457
+ "box-shadow:0 0 0 1px rgba(0,0,0,0.03);" +
458
+ "opacity:0;transform:translateY(16px) scale(0.98);" +
459
+ "transition:opacity 0.45s ease 0.25s, transform 0.55s " + EASE_OUT_EXPO + " 0.25s, box-shadow 0.5s ease 0.25s;";
460
+
461
+ styleIframe(iframe, "width:100%;height:100%;border:none;display:block;");
462
+ container.appendChild(iframe);
463
+ wrapper.appendChild(container);
464
+ root.appendChild(wrapper);
465
+
466
+ // Stage 1: background wipe
467
+ raf(function () {
468
+ root.style.background = "rgba(247,248,250,1)";
469
+
470
+ // Stage 2 (100ms): top bar drops in
471
+ setTimeout(function () {
472
+ topBar.style.opacity = "1";
473
+ topBar.style.transform = "translateY(0)";
474
+ }, 100);
475
+
476
+ // Stage 3 (200ms): card rises
477
+ setTimeout(function () {
478
+ container.style.opacity = "1";
479
+ container.style.transform = "translateY(0) scale(1)";
480
+ container.style.boxShadow = "0 10px 40px -10px rgba(0,0,0,0.1), 0 0 0 1px rgba(0,0,0,0.04)";
481
+ }, 200);
482
+ });
483
+
484
+ root._animateOut = function (cb) {
485
+ container.style.opacity = "0";
486
+ container.style.transform = "translateY(8px) scale(0.99)";
487
+ container.style.transition = "opacity 0.2s ease, transform 0.25s ease";
488
+
489
+ setTimeout(function () {
490
+ topBar.style.opacity = "0";
491
+ topBar.style.transform = "translateY(-20px)";
492
+ topBar.style.transition = "opacity 0.2s ease, transform 0.25s ease";
493
+ }, 80);
494
+
495
+ setTimeout(function () {
496
+ root.style.background = "rgba(247,248,250,0)";
497
+ }, 150);
498
+
499
+ setTimeout(cb, 400);
500
+ };
501
+
502
+ return root;
503
+ }
504
+
505
+ // ---------------------------------------------------------------------------
506
+ // V5: Floating Popup — staged: pops up from bottom-right with bounce
507
+ // ---------------------------------------------------------------------------
508
+
509
+ function buildFloatingPopup(iframe, onClose) {
510
+ var root = document.createElement("div");
511
+ root.setAttribute("data-billing-overlay", "");
512
+ root.style.cssText = "position:fixed;inset:0;z-index:" + Z_INDEX + ";pointer-events:none;";
513
+
514
+ // Light scrim
515
+ var scrim = document.createElement("div");
516
+ scrim.style.cssText =
517
+ "position:absolute;inset:0;background:rgba(0,0,0,0);pointer-events:auto;" +
518
+ "transition:background 0.35s ease;";
519
+ scrim.addEventListener("click", onClose);
520
+ root.appendChild(scrim);
521
+
522
+ // Popup
523
+ var popup = document.createElement("div");
524
+ popup.style.cssText =
525
+ "position:absolute;bottom:24px;right:24px;" +
526
+ "width:420px;height:640px;max-width:calc(100vw - 32px);max-height:calc(100vh - 32px);" +
527
+ "border-radius:16px;background:#fff;overflow:hidden;pointer-events:auto;" +
528
+ "box-shadow:0 0 0 1px rgba(0,0,0,0.04);" +
529
+ "opacity:0;transform:translateY(30px) scale(0.92);" +
530
+ "transition:opacity 0.35s ease, transform 0.5s " + EASE_OUT_EXPO + ", box-shadow 0.5s ease;" +
531
+ "display:flex;flex-direction:column;";
532
+
533
+ // Mini header
534
+ var header = document.createElement("div");
535
+ header.style.cssText =
536
+ "flex-shrink:0;display:flex;align-items:center;justify-content:space-between;" +
537
+ "padding:12px 16px;border-bottom:1px solid #f3f4f6;background:#fff;" +
538
+ "opacity:0;transition:opacity 0.25s ease 0.3s;";
539
+
540
+ var titleArea = document.createElement("div");
541
+ titleArea.style.cssText = "display:flex;align-items:center;gap:8px;";
542
+ titleArea.innerHTML = buildBrandBadge("sm") +
543
+ '<span style="font-size:13px;font-weight:600;color:#111827;font-family:-apple-system,BlinkMacSystemFont,sans-serif;">Checkout</span>';
544
+ header.appendChild(titleArea);
545
+
546
+ var closeBtn = buildCloseButton(onClose);
547
+ closeBtn.style.cssText =
548
+ "width:28px;height:28px;border-radius:8px;border:none;background:#f3f4f6;" +
549
+ "cursor:pointer;display:flex;align-items:center;justify-content:center;" +
550
+ "transition:background 0.15s ease, transform 0.15s ease;color:#6b7280;" +
551
+ "outline:none;";
552
+ closeBtn.innerHTML =
553
+ '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">' +
554
+ '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
555
+ header.appendChild(closeBtn);
556
+ popup.appendChild(header);
557
+
558
+ styleIframe(iframe, "flex:1;width:100%;border:none;display:block;");
559
+ popup.appendChild(iframe);
560
+ root.appendChild(popup);
561
+
562
+ // Stage 1: light scrim
563
+ raf(function () {
564
+ scrim.style.background = "rgba(0,0,0,0.08)";
565
+
566
+ // Stage 2 (60ms): popup springs up
567
+ setTimeout(function () {
568
+ popup.style.opacity = "1";
569
+ popup.style.transform = "translateY(0) scale(1)";
570
+ popup.style.boxShadow = "0 20px 50px -10px rgba(0,0,0,0.25), 0 0 0 1px rgba(0,0,0,0.06)";
571
+ header.style.opacity = "1";
572
+ }, 60);
573
+ });
574
+
575
+ root._animateOut = function (cb) {
576
+ header.style.opacity = "0";
577
+ header.style.transition = "opacity 0.12s ease";
578
+
579
+ setTimeout(function () {
580
+ popup.style.opacity = "0";
581
+ popup.style.transform = "translateY(20px) scale(0.95)";
582
+ popup.style.transition = "opacity 0.2s ease, transform 0.25s " + EASE_IN_EXPO;
583
+ scrim.style.background = "rgba(0,0,0,0)";
584
+ }, 50);
585
+
586
+ setTimeout(cb, 300);
587
+ };
588
+
589
+ return root;
590
+ }
591
+
592
+ // ---------------------------------------------------------------------------
593
+ // Dev-only variation switcher
594
+ // ---------------------------------------------------------------------------
595
+
596
+ function buildVariationSwitcher() {
597
+ if (typeof localStorage === "undefined") return;
598
+ var isDev =
599
+ window.location.hostname === "localhost" ||
600
+ window.location.hostname === "127.0.0.1" ||
601
+ (document.currentScript && document.currentScript.hasAttribute("data-dev"));
602
+ if (!isDev) return;
603
+
604
+ var existing = document.getElementById("billing-variation-switcher");
605
+ if (existing) existing.remove();
606
+
607
+ var switcher = document.createElement("div");
608
+ switcher.id = "billing-variation-switcher";
609
+ switcher.style.cssText =
610
+ "position:fixed;top:16px;left:16px;z-index:" + (Z_INDEX - 1) +
611
+ ";background:#fff;border-radius:14px;padding:14px 16px;" +
612
+ "box-shadow:0 4px 24px rgba(0,0,0,0.1), 0 0 0 1px rgba(0,0,0,0.06);" +
613
+ "font-family:-apple-system,BlinkMacSystemFont,Inter,sans-serif;font-size:13px;" +
614
+ "max-width:260px;";
615
+
616
+ var title = document.createElement("div");
617
+ title.style.cssText = "font-weight:600;color:#111827;margin-bottom:10px;font-size:11px;text-transform:uppercase;letter-spacing:0.6px;";
618
+ title.textContent = "Overlay Variation";
619
+ switcher.appendChild(title);
620
+
621
+ var list = document.createElement("div");
622
+ list.style.cssText = "display:flex;flex-direction:column;gap:4px;";
623
+
624
+ Object.keys(VARIATIONS).forEach(function (key) {
625
+ var v = VARIATIONS[key];
626
+ var btn = document.createElement("button");
627
+ btn.type = "button";
628
+ var isActive = parseInt(key, 10) === _variation;
629
+ btn.style.cssText =
630
+ "display:flex;align-items:center;gap:8px;padding:8px 12px;border-radius:8px;" +
631
+ "border:1.5px solid " + (isActive ? "#111827" : "#e5e7eb") + ";" +
632
+ "background:" + (isActive ? "#111827" : "#fff") + ";" +
633
+ "color:" + (isActive ? "#fff" : "#374151") + ";" +
634
+ "cursor:pointer;font-size:12px;font-family:inherit;text-align:left;" +
635
+ "transition:all 0.2s ease;width:100%;outline:none;";
636
+ btn.innerHTML =
637
+ '<span style="font-weight:700;min-width:14px;">' + key + ".</span> " +
638
+ '<span>' + v.name + '</span>' +
639
+ '<span style="margin-left:auto;font-size:10px;opacity:0.5;">' + v.desc + '</span>';
640
+ btn.addEventListener("mouseenter", function () {
641
+ if (!isActive) { btn.style.borderColor = "#9ca3af"; btn.style.background = "#f9fafb"; }
642
+ });
643
+ btn.addEventListener("mouseleave", function () {
644
+ if (!isActive) { btn.style.borderColor = "#e5e7eb"; btn.style.background = "#fff"; }
645
+ });
646
+ btn.addEventListener("click", function () {
647
+ _variation = parseInt(key, 10);
648
+ localStorage.setItem("billing_overlay_variation", key);
649
+ buildVariationSwitcher();
650
+ if (_isOpen && _currentCheckoutId) {
651
+ var cbs = Object.assign({}, _callbacks);
652
+ closeOverlay(true);
653
+ setTimeout(function () {
654
+ openCheckout(Object.assign({ checkoutId: _currentCheckoutId }, cbs));
655
+ }, 150);
656
+ }
657
+ });
658
+ list.appendChild(btn);
659
+ });
660
+
661
+ switcher.appendChild(list);
662
+
663
+ var hint = document.createElement("div");
664
+ hint.style.cssText = "margin-top:10px;font-size:10px;color:#9ca3af;line-height:1.4;";
665
+ hint.textContent = "Dev only \u2014 not shown in production";
666
+ switcher.appendChild(hint);
667
+
668
+ document.body.appendChild(switcher);
669
+ }
670
+
671
+ // ---------------------------------------------------------------------------
672
+ // PostMessage listener
673
+ // ---------------------------------------------------------------------------
674
+
675
+ function handleMessage(event) {
676
+ var data;
677
+ try {
678
+ if (typeof event.data === "string") {
679
+ if (event.data.indexOf(MSG_PREFIX) !== 0) return;
680
+ data = JSON.parse(event.data.slice(MSG_PREFIX.length));
681
+ } else if (event.data && event.data.type && typeof event.data.type === "string" && event.data.type.indexOf(MSG_PREFIX) === 0) {
682
+ data = event.data;
683
+ data.type = data.type.slice(MSG_PREFIX.length);
684
+ } else {
685
+ return;
686
+ }
687
+ } catch (e) {
688
+ return;
689
+ }
690
+
691
+ if (!data || !data.type) return;
692
+
693
+ switch (data.type) {
694
+ case "status":
695
+ _lastStatus = data.status;
696
+ if (data.status === "confirmed" || data.status === "success") {
697
+ if (_callbacks.onSuccess) {
698
+ _callbacks.onSuccess({
699
+ checkoutId: _currentCheckoutId,
700
+ txHash: data.txHash || null,
701
+ });
702
+ }
703
+ setTimeout(function () { closeOverlay(); }, 2500);
704
+ }
705
+ break;
706
+
707
+ case "error":
708
+ if (_callbacks.onError) {
709
+ _callbacks.onError({
710
+ message: data.message || "Checkout error",
711
+ code: data.code || "unknown",
712
+ });
713
+ }
714
+ break;
715
+
716
+ case "loaded":
717
+ if (_iframe) _iframe.style.opacity = "1";
718
+ break;
719
+
720
+ case "close":
721
+ closeOverlay();
722
+ break;
723
+ }
724
+ }
725
+
726
+ // ---------------------------------------------------------------------------
727
+ // Core API
728
+ // ---------------------------------------------------------------------------
729
+
730
+ function openCheckout(options) {
731
+ if (!options || !options.checkoutId) {
732
+ throw new Error("billing.openCheckout: checkoutId is required");
733
+ }
734
+
735
+ // If already open with same checkout, bring to front
736
+ if (_isOpen && _currentCheckoutId === options.checkoutId && _overlay) {
737
+ _overlay.style.display = "";
738
+ return;
739
+ }
740
+
741
+ if (_isOpen) {
742
+ closeOverlay(true);
743
+ }
744
+
745
+ _currentCheckoutId = options.checkoutId;
746
+ _callbacks = {
747
+ onSuccess: options.onSuccess || null,
748
+ onClose: options.onClose || null,
749
+ onError: options.onError || null,
750
+ };
751
+ _lastStatus = null;
752
+
753
+ // Resolve which variation to use
754
+ // Priority: options.variation > dev switcher > default
755
+ var variationNum = options.variation
756
+ ? resolveVariation(options.variation)
757
+ : _variation;
758
+
759
+ // Build iframe
760
+ var url = BILLING_HOST + "/checkout/" + encodeURIComponent(options.checkoutId);
761
+ url += (url.indexOf("?") === -1 ? "?" : "&") + "embed=1";
762
+
763
+ _iframe = document.createElement("iframe");
764
+ _iframe.src = url;
765
+ _iframe.setAttribute("title", "billing.io Secure Checkout");
766
+ _iframe.style.opacity = "0";
767
+ _iframe.style.transition = "opacity 0.4s ease";
768
+ _iframe.addEventListener("load", function () {
769
+ _iframe.style.opacity = "1";
770
+ });
771
+
772
+ // Build overlay
773
+ var variation = VARIATIONS[variationNum] || VARIATIONS[1];
774
+ _overlay = variation.build(_iframe, function () { closeOverlay(); });
775
+
776
+ document.body.appendChild(_overlay);
777
+ lockScroll();
778
+ _isOpen = true;
779
+
780
+ window.addEventListener("message", handleMessage);
781
+ document.addEventListener("keydown", handleKeydown);
782
+ }
783
+
784
+ function closeOverlay(silent) {
785
+ if (!_overlay) return;
786
+
787
+ var overlay = _overlay;
788
+ _isOpen = false;
789
+
790
+ if (overlay._animateOut) {
791
+ overlay._animateOut(function () {
792
+ if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
793
+ });
794
+ } else {
795
+ if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
796
+ }
797
+
798
+ unlockScroll();
799
+
800
+ if (!silent && _callbacks.onClose) {
801
+ _callbacks.onClose();
802
+ }
803
+
804
+ _overlay = null;
805
+ _iframe = null;
806
+ document.removeEventListener("keydown", handleKeydown);
807
+ window.removeEventListener("message", handleMessage);
808
+ }
809
+
810
+ function redirectToCheckout(options) {
811
+ if (!options || !options.checkoutId) {
812
+ throw new Error("billing.redirectToCheckout: checkoutId is required");
813
+ }
814
+ window.location.href = BILLING_HOST + "/checkout/" + encodeURIComponent(options.checkoutId);
815
+ }
816
+
817
+ function handleKeydown(e) {
818
+ if (e.key === "Escape") closeOverlay();
819
+ }
820
+
821
+ // ---------------------------------------------------------------------------
822
+ // Public API
823
+ // ---------------------------------------------------------------------------
824
+
825
+ var billingApi = {
826
+ version: VERSION,
827
+
828
+ /**
829
+ * Open the checkout overlay.
830
+ * @param {Object} options
831
+ * @param {string} options.checkoutId - The checkout ID (e.g. "co_123")
832
+ * @param {string|number} [options.variation] - Overlay style: "centered"|"panel"|"bottom"|"fullscreen"|"popup" or 1-5
833
+ * @param {Function} [options.onSuccess] - Called with { checkoutId, txHash }
834
+ * @param {Function} [options.onClose] - Called when the overlay is closed
835
+ * @param {Function} [options.onError] - Called with { message, code }
836
+ */
837
+ openCheckout: openCheckout,
838
+
839
+ /**
840
+ * Redirect the page to the hosted checkout.
841
+ * @param {Object} options
842
+ * @param {string} options.checkoutId - The checkout ID
843
+ */
844
+ redirectToCheckout: redirectToCheckout,
845
+
846
+ /**
847
+ * Close the overlay programmatically.
848
+ */
849
+ close: function () { closeOverlay(); },
850
+
851
+ /**
852
+ * Set the default overlay variation (1-5). Dev use only.
853
+ * @param {number|string} v - Variation number or name
854
+ */
855
+ setVariation: function (v) {
856
+ var n = resolveVariation(v);
857
+ if (VARIATIONS[n]) {
858
+ _variation = n;
859
+ localStorage.setItem("billing_overlay_variation", String(n));
860
+ }
861
+ },
862
+
863
+ /** Get current default variation number. */
864
+ getVariation: function () { return _variation; },
865
+ };
866
+
867
+ if (typeof window !== "undefined") {
868
+ window[NAMESPACE] = billingApi;
869
+ }
870
+
871
+ if (document.readyState === "loading") {
872
+ document.addEventListener("DOMContentLoaded", buildVariationSwitcher);
873
+ } else {
874
+ buildVariationSwitcher();
875
+ }
876
+ })();