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