@maytes/checkout-button 1.1.1 → 1.2.1

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +93 -74
  3. package/dist/checkout-button.299a037b.js +188 -0
  4. package/dist/checkout-button.299a037b.js.map +1 -0
  5. package/dist/{checkout-button.2aad12c3.cjs → checkout-button.332d26cc.cjs} +314 -156
  6. package/dist/checkout-button.332d26cc.cjs.map +1 -0
  7. package/dist/{checkout-button.aed57a66.mjs → checkout-button.67fdb2e1.mjs} +314 -156
  8. package/dist/checkout-button.67fdb2e1.mjs.map +1 -0
  9. package/dist/checkout-button.cjs +314 -156
  10. package/dist/checkout-button.cjs.map +1 -1
  11. package/dist/checkout-button.d.cts +40 -4
  12. package/dist/checkout-button.d.ts +40 -4
  13. package/dist/checkout-button.element.afe71cd5.js +187 -0
  14. package/dist/checkout-button.element.afe71cd5.js.map +1 -0
  15. package/dist/checkout-button.element.js +187 -0
  16. package/dist/checkout-button.element.js.map +1 -0
  17. package/dist/checkout-button.js +58 -21
  18. package/dist/checkout-button.js.map +1 -1
  19. package/dist/checkout-button.mjs +314 -156
  20. package/dist/checkout-button.mjs.map +1 -1
  21. package/dist/element.cjs +201 -0
  22. package/dist/element.cjs.map +1 -0
  23. package/dist/element.d.cts +44 -0
  24. package/dist/element.d.ts +44 -0
  25. package/dist/element.mjs +180 -0
  26. package/dist/element.mjs.map +1 -0
  27. package/dist/integrity.json +37 -26
  28. package/dist/react.cjs +139 -0
  29. package/dist/react.cjs.map +1 -0
  30. package/dist/react.d.cts +30 -0
  31. package/dist/react.d.ts +30 -0
  32. package/dist/react.mjs +117 -0
  33. package/dist/react.mjs.map +1 -0
  34. package/docs/guides/angular.md +56 -0
  35. package/docs/guides/api.md +108 -0
  36. package/docs/guides/events.md +48 -0
  37. package/docs/guides/launch-behaviour.md +41 -0
  38. package/docs/guides/nextjs.md +49 -0
  39. package/docs/guides/own-button.md +41 -0
  40. package/docs/guides/quickstart-html.md +31 -0
  41. package/docs/guides/quickstart-npm.md +34 -0
  42. package/docs/guides/react.md +88 -0
  43. package/docs/guides/security-csp.md +32 -0
  44. package/docs/guides/server.md +76 -0
  45. package/docs/guides/solid.md +44 -0
  46. package/docs/guides/stripe-payment-element.md +14 -0
  47. package/docs/guides/svelte.md +46 -0
  48. package/docs/guides/troubleshooting.md +17 -0
  49. package/docs/guides/vue-nuxt.md +64 -0
  50. package/docs/guides/web-component.md +109 -0
  51. package/package.json +40 -3
  52. package/dist/checkout-button.2aad12c3.cjs.map +0 -1
  53. package/dist/checkout-button.4b780d52.js +0 -151
  54. package/dist/checkout-button.4b780d52.js.map +0 -1
  55. package/dist/checkout-button.aed57a66.mjs.map +0 -1
@@ -27,6 +27,57 @@ __export(src_exports, {
27
27
  });
28
28
  module.exports = __toCommonJS(src_exports);
29
29
 
30
+ // src/errors.ts
31
+ var MaytesErrorCode = {
32
+ Config: "CONFIG"
33
+ };
34
+ var MaytesError = class extends Error {
35
+ constructor(code, message) {
36
+ super(message);
37
+ this.code = code;
38
+ this.name = "MaytesError";
39
+ }
40
+ };
41
+
42
+ // src/appearance.ts
43
+ var MIN_BUTTON_HEIGHT = 40;
44
+ var MAX_BUTTON_HEIGHT = 55;
45
+ var MAX_BUTTON_RADIUS = 999;
46
+ function isWholeNumber(value) {
47
+ return typeof value === "number" && Number.isInteger(value);
48
+ }
49
+ function assertAppearance(options) {
50
+ const { radius, height } = options;
51
+ const appearance = {};
52
+ if (height !== void 0) {
53
+ if (!isWholeNumber(height) || height < MIN_BUTTON_HEIGHT || height > MAX_BUTTON_HEIGHT) {
54
+ throw new MaytesError(
55
+ MaytesErrorCode.Config,
56
+ `renderButton({ height }) must be a whole number of pixels from ${MIN_BUTTON_HEIGHT} to ${MAX_BUTTON_HEIGHT}`
57
+ );
58
+ }
59
+ appearance.height = height;
60
+ }
61
+ if (radius !== void 0) {
62
+ const maxRadius = height === void 0 ? MAX_BUTTON_RADIUS : Math.floor(height / 2);
63
+ if (!isWholeNumber(radius) || radius < 0 || radius > maxRadius) {
64
+ throw new MaytesError(
65
+ MaytesErrorCode.Config,
66
+ `renderButton({ radius }) must be a whole number of pixels from 0 to ${maxRadius}`
67
+ );
68
+ }
69
+ appearance.radius = radius;
70
+ }
71
+ return appearance;
72
+ }
73
+ function applyAppearance(button, appearance) {
74
+ if (appearance.radius !== void 0) button.style.setProperty("--maytes-button-radius", `${appearance.radius}px`);
75
+ if (appearance.height !== void 0) {
76
+ button.style.setProperty("--maytes-button-height", `${appearance.height}px`);
77
+ button.classList.add("maytes-checkout-button--sized");
78
+ }
79
+ }
80
+
30
81
  // src/foundation/brand.generated.ts
31
82
  var foundation = {
32
83
  brand: {
@@ -74,18 +125,6 @@ function buildMaytesLogo(height, color) {
74
125
  return svg;
75
126
  }
76
127
 
77
- // src/errors.ts
78
- var MaytesErrorCode = {
79
- Config: "CONFIG"
80
- };
81
- var MaytesError = class extends Error {
82
- constructor(code, message) {
83
- super(message);
84
- this.code = code;
85
- this.name = "MaytesError";
86
- }
87
- };
88
-
89
128
  // src/framing.ts
90
129
  function isSecurityError(err) {
91
130
  return typeof err === "object" && err !== null && err.name === "SecurityError";
@@ -175,7 +214,7 @@ function ensureOverlayStylesInTopDocument(host) {
175
214
  copy.textContent = localStyle.textContent ?? "";
176
215
  host.head.appendChild(copy);
177
216
  }
178
- function buildOverlay(host, state) {
217
+ function buildOverlay(host, state, variant) {
179
218
  const dialog = host.createElement("dialog");
180
219
  dialog.className = "maytes-checkout-overlay";
181
220
  dialog.setAttribute("aria-labelledby", TITLE_ID);
@@ -195,22 +234,24 @@ function buildOverlay(host, state) {
195
234
  const text = host.createElement("p");
196
235
  text.id = TITLE_ID;
197
236
  text.className = "maytes-checkout-overlay__text";
198
- text.textContent = "Completing checkout with Maytes\u2026";
237
+ text.textContent = variant === "popup" ? "Completing checkout with Maytes\u2026" : "Taking you to Maytes\u2026";
199
238
  content.appendChild(text);
200
- const link = host.createElement("button");
201
- link.className = "maytes-checkout-overlay__link";
202
- link.type = "button";
203
- link.textContent = "Return to Maytes";
204
- link.addEventListener("click", () => {
205
- if (state.popupWindow !== null && !state.popupWindow.closed) {
206
- try {
207
- state.popupWindow.focus();
208
- } catch (err) {
209
- if (!isSecurityError(err)) throw err;
239
+ if (variant === "popup") {
240
+ const link = host.createElement("button");
241
+ link.className = "maytes-checkout-overlay__link";
242
+ link.type = "button";
243
+ link.textContent = "Return to Maytes";
244
+ link.addEventListener("click", () => {
245
+ if (state.popupWindow !== null && !state.popupWindow.closed) {
246
+ try {
247
+ state.popupWindow.focus();
248
+ } catch (err) {
249
+ if (!isSecurityError(err)) throw err;
250
+ }
210
251
  }
211
- }
212
- });
213
- content.appendChild(link);
252
+ });
253
+ content.appendChild(link);
254
+ }
214
255
  dialog.appendChild(content);
215
256
  dialog.addEventListener("cancel", (event) => {
216
257
  event.preventDefault();
@@ -222,11 +263,11 @@ function buildOverlay(host, state) {
222
263
  function applyNativeBackdropFallback(dialog) {
223
264
  dialog.style.background = NATIVE_BACKDROP_FALLBACK;
224
265
  }
225
- function showOverlay(state) {
266
+ function showOverlay(state, variant = "popup") {
226
267
  if (state.overlayEl !== null) return;
227
268
  const host = resolveOverlayHost();
228
269
  ensureOverlayStylesInTopDocument(host);
229
- const dialog = buildOverlay(host, state);
270
+ const dialog = buildOverlay(host, state, variant);
230
271
  state.overlayEl = dialog;
231
272
  host.body.appendChild(dialog);
232
273
  if (host !== document) detachOverlayWhenThisPageHides(state);
@@ -345,7 +386,7 @@ function makeRedirect(environment, baseUrlOverride) {
345
386
  }
346
387
 
347
388
  // src/version.ts
348
- var SDK_VERSION = "1.1.1";
389
+ var SDK_VERSION = "1.2.1";
349
390
 
350
391
  // src/styles.ts
351
392
  var STYLE_ATTR = "data-maytes-checkout-button";
@@ -358,28 +399,51 @@ var BUTTON_CSS = `
358
399
  align-items: center;
359
400
  justify-content: center;
360
401
  gap: 0.57em;
402
+ min-height: var(--maytes-button-height, auto);
361
403
  padding: 0.71em 1.29em;
362
- border: none;
363
- border-radius: 999px;
364
- background: ${foundation.brand.primary};
365
- color: ${foundation.text.primaryInverse};
404
+ border-radius: var(--maytes-button-radius, 999px);
366
405
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
367
406
  font-size: var(--maytes-button-font-size, 14px);
368
407
  font-weight: 600;
369
408
  line-height: 1;
370
- cursor: pointer;
371
409
  user-select: none;
410
+ }
411
+ .maytes-checkout-button.maytes-checkout-button,
412
+ .maytes-checkout-button.maytes-checkout-button:hover,
413
+ .maytes-checkout-button.maytes-checkout-button:focus,
414
+ .maytes-checkout-button.maytes-checkout-button:active {
415
+ background: ${foundation.brand.primary};
416
+ border: none;
417
+ color: ${foundation.text.primaryInverse};
418
+ box-shadow: none;
419
+ text-decoration: none;
420
+ text-shadow: none;
421
+ filter: none;
422
+ transform: none;
423
+ opacity: 1;
424
+ cursor: pointer;
372
425
  transition: background-color 120ms ease, opacity 120ms ease;
373
426
  }
374
- .maytes-checkout-button:hover { background: ${foundation.palette.burgundy[700]}; }
375
- .maytes-checkout-button:focus-visible {
427
+ .maytes-checkout-button.maytes-checkout-button:hover { background: ${foundation.palette.burgundy[700]}; }
428
+ .maytes-checkout-button.maytes-checkout-button:focus:not(:focus-visible) { outline: none; }
429
+ .maytes-checkout-button.maytes-checkout-button:focus-visible {
376
430
  outline: 2px solid ${FOCUS_RING};
377
431
  outline-offset: 2px;
378
432
  }
379
- .maytes-checkout-button[aria-disabled='true'] {
433
+ .maytes-checkout-button.maytes-checkout-button[aria-disabled='true'] {
380
434
  cursor: progress;
381
435
  opacity: 0.7;
382
436
  }
437
+ .maytes-checkout-button.maytes-checkout-button * { transition: none; }
438
+ .maytes-checkout-button.maytes-checkout-button .maytes-checkout-button__logo,
439
+ .maytes-checkout-button.maytes-checkout-button .maytes-checkout-button__logo * {
440
+ animation: none;
441
+ fill: currentColor;
442
+ }
443
+ .maytes-checkout-button--sized {
444
+ box-sizing: border-box;
445
+ white-space: nowrap;
446
+ }
383
447
  .maytes-checkout-button--block {
384
448
  display: flex;
385
449
  width: 100%;
@@ -441,17 +505,31 @@ var BUTTON_CSS = `
441
505
  margin: 0;
442
506
  }
443
507
  .maytes-checkout-overlay__link {
444
- color: rgba(255, 255, 255, 0.7);
445
508
  font-size: 0.875rem;
446
- text-decoration: underline;
447
509
  text-underline-offset: 4px;
510
+ font-family: inherit;
511
+ }
512
+ .maytes-checkout-overlay__link.maytes-checkout-overlay__link,
513
+ .maytes-checkout-overlay__link.maytes-checkout-overlay__link:hover,
514
+ .maytes-checkout-overlay__link.maytes-checkout-overlay__link:focus,
515
+ .maytes-checkout-overlay__link.maytes-checkout-overlay__link:active {
448
516
  background: none;
449
517
  border: none;
518
+ color: rgba(255, 255, 255, 0.7);
519
+ box-shadow: none;
520
+ text-decoration: underline;
521
+ text-shadow: none;
522
+ transform: none;
523
+ opacity: 1;
450
524
  cursor: pointer;
451
- font-family: inherit;
452
525
  transition: color 0.15s;
453
526
  }
454
- .maytes-checkout-overlay__link:hover { color: ${foundation.text.primaryInverse}; }
527
+ .maytes-checkout-overlay__link.maytes-checkout-overlay__link:hover { color: ${foundation.text.primaryInverse}; }
528
+ .maytes-checkout-overlay__link.maytes-checkout-overlay__link:focus:not(:focus-visible) { outline: none; }
529
+ .maytes-checkout-overlay__link.maytes-checkout-overlay__link:focus-visible {
530
+ outline: 2px solid ${FOCUS_RING};
531
+ outline-offset: 2px;
532
+ }
455
533
  @media (prefers-reduced-motion: reduce) {
456
534
  .maytes-checkout-overlay__spinner,
457
535
  .maytes-checkout-button__spinner { animation: none; }
@@ -528,24 +606,12 @@ function releaseStyles() {
528
606
  if (el !== null) el.remove();
529
607
  }
530
608
 
531
- // src/button.ts
609
+ // src/launch.ts
532
610
  var POPUP_WIDTH = 500;
533
611
  var POPUP_HEIGHT = 800;
534
612
  var POPUP_POLL_INTERVAL_MS = 500;
535
613
  var MOBILE_MAX_WIDTH = 600;
536
614
  var SLOW_NETWORK_DELAY_MS = 8e3;
537
- function buildLogo() {
538
- const svg = buildMaytesLogo("1em", "currentColor");
539
- svg.setAttribute("class", "maytes-checkout-button__logo");
540
- svg.setAttribute("aria-hidden", "true");
541
- return svg;
542
- }
543
- function buildSpinner() {
544
- const spinner = document.createElement("span");
545
- spinner.className = "maytes-checkout-button__spinner";
546
- spinner.setAttribute("aria-hidden", "true");
547
- return spinner;
548
- }
549
615
  function isValidCheckoutResult(value) {
550
616
  if (typeof value !== "object" || value === null) return false;
551
617
  const obj = value;
@@ -644,6 +710,148 @@ function paintPopupLoadingScreen(popup, cspNonce) {
644
710
  slow.hidden = false;
645
711
  }, SLOW_NETWORK_DELAY_MS);
646
712
  }
713
+ function eventDetail(state, source) {
714
+ return { instanceId: state.instanceId, source };
715
+ }
716
+ function dispatchFailed(origin, reason, cause) {
717
+ const detail = cause === void 0 ? { ...origin, reason } : { ...origin, reason, cause };
718
+ document.dispatchEvent(new CustomEvent("maytes:checkout-failed", { detail }));
719
+ }
720
+ function dispatchRedirected(origin, url, target) {
721
+ const detail = { ...origin, url, target };
722
+ document.dispatchEvent(new CustomEvent("maytes:checkout-redirected", { detail }));
723
+ }
724
+ function assertLaunchMode(mode, caller = "renderButton") {
725
+ if (mode !== "redirect" && mode !== "popup") {
726
+ throw new MaytesError(MaytesErrorCode.Config, `${caller}({ mode }) must be 'redirect' or 'popup'`);
727
+ }
728
+ return mode;
729
+ }
730
+ function assertRedirectOverlay(value, caller = "renderButton") {
731
+ if (value === void 0) return false;
732
+ if (typeof value !== "boolean") {
733
+ throw new MaytesError(MaytesErrorCode.Config, `${caller}({ redirectOverlay }) must be true or false`);
734
+ }
735
+ return value;
736
+ }
737
+ function setInstanceBusy(state, busy) {
738
+ state.busy = busy;
739
+ for (const view of [...state.busyViews]) view(busy);
740
+ }
741
+ function endLaunch(state) {
742
+ hideOverlay(state);
743
+ setInstanceBusy(state, false);
744
+ }
745
+ function closeOrphanPopup(state, popup) {
746
+ closePopupWindow(popup);
747
+ stopPopupPoll(state);
748
+ if (state.popupWindow === popup) state.popupWindow = null;
749
+ endLaunch(state);
750
+ }
751
+ function clearOverlayOnRestore(state) {
752
+ if (state.restoreDetach !== null) return;
753
+ const onPageShow = (event) => {
754
+ if (!event.persisted) return;
755
+ detach();
756
+ endLaunch(state);
757
+ };
758
+ const detach = () => {
759
+ window.removeEventListener("pageshow", onPageShow);
760
+ state.restoreDetach = null;
761
+ };
762
+ window.addEventListener("pageshow", onPageShow);
763
+ state.restoreDetach = detach;
764
+ }
765
+ function navigateAway(state, origin, url, keepOverlay) {
766
+ const navigation = navigateTopLevel(url, false);
767
+ if (keepOverlay && (navigation.target === "self" || navigation.target === "top")) {
768
+ setInstanceBusy(state, false);
769
+ clearOverlayOnRestore(state);
770
+ } else {
771
+ endLaunch(state);
772
+ }
773
+ if (navigation.target === null) {
774
+ console.error("[maytes/checkout-button] could not leave the embedding frame:", navigation.cause);
775
+ dispatchFailed(origin, "navigation-blocked", navigation.cause);
776
+ return { outcome: "failed", reason: "navigation-blocked" };
777
+ }
778
+ dispatchRedirected(origin, url, navigation.target);
779
+ return { outcome: "redirected", target: navigation.target };
780
+ }
781
+ function openPopupLoader(state, origin) {
782
+ const popup = openBlankPopup(state);
783
+ if (popup === null) return null;
784
+ state.popupWindow = popup;
785
+ state.popupNavigated = false;
786
+ refocusPopup(popup);
787
+ paintPopupLoadingScreen(popup, state.config.cspNonce);
788
+ showOverlay(state);
789
+ document.dispatchEvent(new CustomEvent("maytes:checkout-opened", { detail: origin }));
790
+ startPopupPoll(state, () => {
791
+ endLaunch(state);
792
+ document.dispatchEvent(new CustomEvent("maytes:checkout-closed", { detail: origin }));
793
+ });
794
+ return popup;
795
+ }
796
+ async function launchCheckout(state, mode, source, redirectOverlay = false) {
797
+ if (state.busy || state.destroyed) return { outcome: "ignored" };
798
+ setInstanceBusy(state, true);
799
+ const origin = eventDetail(state, source);
800
+ const shouldUsePopup = mode === "popup" && !isMobileViewport();
801
+ const popup = shouldUsePopup ? openPopupLoader(state, origin) : null;
802
+ if (popup === null && redirectOverlay) showOverlay(state, "redirect");
803
+ try {
804
+ const result = await state.config.createCheckout();
805
+ if (state.destroyed || popup !== null && popup.closed) {
806
+ closePopupWindow(popup);
807
+ return { outcome: "closed" };
808
+ }
809
+ if (!isValidCheckoutResult(result)) {
810
+ closeOrphanPopup(state, popup);
811
+ const shapeError = new MaytesError(
812
+ MaytesErrorCode.Config,
813
+ "createCheckout must resolve to { checkoutId: string, checkoutUrl?: http(s) URL }"
814
+ );
815
+ console.error(shapeError);
816
+ dispatchFailed(origin, "invalid-shape", shapeError);
817
+ return { outcome: "failed", reason: "invalid-shape" };
818
+ }
819
+ const url = result.checkoutUrl ?? buildCheckoutUrl(
820
+ state.config.environment,
821
+ state.config.baseUrl,
822
+ result.checkoutId
823
+ );
824
+ if (popup !== null) {
825
+ state.popupNavigated = true;
826
+ navigatePopup(popup, url);
827
+ return { outcome: "popup" };
828
+ }
829
+ if (shouldUsePopup) {
830
+ console.warn("[maytes/checkout-button] popup was blocked; redirecting instead");
831
+ }
832
+ return navigateAway(state, origin, url, redirectOverlay);
833
+ } catch (err) {
834
+ if (popup !== null && popup.closed) return { outcome: "closed" };
835
+ closeOrphanPopup(state, popup);
836
+ console.error("[maytes/checkout-button] createCheckout failed:", err);
837
+ dispatchFailed(origin, "create-checkout-rejected", err);
838
+ return { outcome: "failed", reason: "create-checkout-rejected" };
839
+ }
840
+ }
841
+
842
+ // src/button.ts
843
+ function buildLogo() {
844
+ const svg = buildMaytesLogo("1em", "currentColor");
845
+ svg.setAttribute("class", "maytes-checkout-button__logo");
846
+ svg.setAttribute("aria-hidden", "true");
847
+ return svg;
848
+ }
849
+ function buildSpinner() {
850
+ const spinner = document.createElement("span");
851
+ spinner.className = "maytes-checkout-button__spinner";
852
+ spinner.setAttribute("aria-hidden", "true");
853
+ return spinner;
854
+ }
647
855
  function renderButton(state, container, options = {}) {
648
856
  if (state.destroyed) {
649
857
  throw new MaytesError(
@@ -654,120 +862,47 @@ function renderButton(state, container, options = {}) {
654
862
  if (!(container instanceof HTMLElement)) {
655
863
  throw new MaytesError(MaytesErrorCode.Config, "renderButton(container) requires an HTMLElement");
656
864
  }
865
+ const appearance = assertAppearance(options);
866
+ const redirectOverlay = assertRedirectOverlay(options.redirectOverlay);
657
867
  ensureStylesInjected(state.config.cspNonce);
658
868
  const label = options.label ?? "Split with";
659
869
  const block = options.block === true;
660
- const mode = options.mode ?? "popup";
661
- if (mode !== "redirect" && mode !== "popup") {
662
- throw new MaytesError(
663
- MaytesErrorCode.Config,
664
- "renderButton({ mode }) must be 'redirect' or 'popup'"
665
- );
666
- }
870
+ const mode = assertLaunchMode(options.mode ?? "popup");
667
871
  const button = document.createElement("button");
668
872
  button.type = "button";
669
873
  button.className = block ? "maytes-checkout-button maytes-checkout-button--block" : "maytes-checkout-button";
874
+ applyAppearance(button, appearance);
670
875
  button.setAttribute("aria-label", `${label} Maytes`);
671
876
  const labelNode = document.createTextNode(`${label} `);
672
- const logo = buildLogo();
877
+ let icon = buildLogo();
673
878
  button.appendChild(labelNode);
674
- button.appendChild(logo);
675
- const setBusy = (busy) => {
676
- state.busy = busy;
879
+ button.appendChild(icon);
880
+ const showBusy = (busy) => {
677
881
  if (busy) {
678
882
  button.setAttribute("aria-disabled", "true");
679
883
  button.setAttribute("aria-busy", "true");
680
- logo.replaceWith(buildSpinner());
884
+ const spinner = buildSpinner();
885
+ icon.replaceWith(spinner);
886
+ icon = spinner;
681
887
  } else {
682
888
  button.removeAttribute("aria-disabled");
683
889
  button.removeAttribute("aria-busy");
684
- const currentIndicator = button.querySelector(".maytes-checkout-button__spinner, .maytes-checkout-button__logo");
685
- if (currentIndicator !== null) currentIndicator.replaceWith(buildLogo());
890
+ const nextLogo = buildLogo();
891
+ icon.replaceWith(nextLogo);
892
+ icon = nextLogo;
686
893
  }
687
894
  };
688
- const teardownActiveCheckout = () => {
689
- hideOverlay(state);
690
- setBusy(false);
691
- };
692
- const onPopupClosed = () => {
693
- teardownActiveCheckout();
694
- document.dispatchEvent(new CustomEvent("maytes:checkout-closed"));
695
- };
696
- const dispatchFailed = (reason, cause) => {
697
- document.dispatchEvent(new CustomEvent("maytes:checkout-failed", {
698
- detail: cause === void 0 ? { reason } : { reason, cause }
699
- }));
700
- };
701
- const dispatchRedirected = (url, target) => {
702
- const detail = { url, target };
703
- document.dispatchEvent(new CustomEvent("maytes:checkout-redirected", { detail }));
895
+ let launchedHere = false;
896
+ const followOwnLaunch = (busy) => {
897
+ if (!launchedHere) return;
898
+ showBusy(busy);
899
+ if (!busy) launchedHere = false;
704
900
  };
705
- const navigateAway = (url) => {
706
- const outcome = navigateTopLevel(url, false);
707
- teardownActiveCheckout();
708
- if (outcome.target === null) {
709
- console.error("[maytes/checkout-button] could not leave the embedding frame:", outcome.cause);
710
- dispatchFailed("navigation-blocked", outcome.cause);
711
- return;
712
- }
713
- dispatchRedirected(url, outcome.target);
714
- };
715
- const closeOrphanPopup = (popup) => {
716
- closePopupWindow(popup);
717
- stopPopupPoll(state);
718
- if (state.popupWindow === popup) state.popupWindow = null;
719
- teardownActiveCheckout();
720
- };
721
- const handleClick = async () => {
901
+ state.busyViews.add(followOwnLaunch);
902
+ const handleClick = () => {
722
903
  if (state.busy || state.destroyed) return;
723
- setBusy(true);
724
- const shouldUsePopup = mode === "popup" && !isMobileViewport();
725
- const popup = shouldUsePopup ? openBlankPopup(state) : null;
726
- if (popup !== null) {
727
- state.popupWindow = popup;
728
- state.popupNavigated = false;
729
- refocusPopup(popup);
730
- paintPopupLoadingScreen(popup, state.config.cspNonce);
731
- showOverlay(state);
732
- document.dispatchEvent(new CustomEvent("maytes:checkout-opened"));
733
- startPopupPoll(state, onPopupClosed);
734
- }
735
- try {
736
- const result = await state.config.createCheckout();
737
- if (state.destroyed || popup !== null && popup.closed) {
738
- closePopupWindow(popup);
739
- return;
740
- }
741
- if (!isValidCheckoutResult(result)) {
742
- closeOrphanPopup(popup);
743
- const shapeError = new MaytesError(
744
- MaytesErrorCode.Config,
745
- "createCheckout must resolve to { checkoutId: string, checkoutUrl?: http(s) URL }"
746
- );
747
- console.error(shapeError);
748
- dispatchFailed("invalid-shape", shapeError);
749
- return;
750
- }
751
- const url = result.checkoutUrl ?? buildCheckoutUrl(
752
- state.config.environment,
753
- state.config.baseUrl,
754
- result.checkoutId
755
- );
756
- if (popup !== null) {
757
- state.popupNavigated = true;
758
- navigatePopup(popup, url);
759
- } else {
760
- if (shouldUsePopup) {
761
- console.warn("[maytes/checkout-button] popup was blocked; redirecting instead");
762
- }
763
- navigateAway(url);
764
- }
765
- } catch (err) {
766
- if (popup !== null && popup.closed) return;
767
- closeOrphanPopup(popup);
768
- console.error("[maytes/checkout-button] createCheckout failed:", err);
769
- dispatchFailed("create-checkout-rejected", err);
770
- }
904
+ launchedHere = true;
905
+ void launchCheckout(state, mode, "button", redirectOverlay);
771
906
  };
772
907
  button.addEventListener("click", handleClick);
773
908
  container.appendChild(button);
@@ -777,6 +912,7 @@ function renderButton(state, container, options = {}) {
777
912
  cleaned = true;
778
913
  button.removeEventListener("click", handleClick);
779
914
  button.remove();
915
+ state.busyViews.delete(followOwnLaunch);
780
916
  state.teardowns.delete(teardown);
781
917
  releaseStyles();
782
918
  };
@@ -785,18 +921,18 @@ function renderButton(state, container, options = {}) {
785
921
  }
786
922
 
787
923
  // src/state.ts
788
- function popupNameSuffix() {
924
+ function randomSuffix() {
789
925
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
790
926
  }
791
- function generatePopupName() {
927
+ function randomId() {
792
928
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
793
929
  try {
794
- return `maytes-checkout-${crypto.randomUUID()}`;
930
+ return crypto.randomUUID();
795
931
  } catch {
796
- return `maytes-checkout-${popupNameSuffix()}`;
932
+ return randomSuffix();
797
933
  }
798
934
  }
799
- return `maytes-checkout-${popupNameSuffix()}`;
935
+ return randomSuffix();
800
936
  }
801
937
  function createInstanceState(options, internal) {
802
938
  const config = {
@@ -808,11 +944,14 @@ function createInstanceState(options, internal) {
808
944
  return {
809
945
  config,
810
946
  busy: false,
947
+ busyViews: /* @__PURE__ */ new Set(),
948
+ instanceId: randomId(),
811
949
  overlayEl: null,
812
950
  overlayDetach: null,
951
+ restoreDetach: null,
813
952
  popupWindow: null,
814
953
  popupNavigated: false,
815
- popupName: generatePopupName(),
954
+ popupName: `maytes-checkout-${randomId()}`,
816
955
  popupPollHandle: null,
817
956
  destroyed: false,
818
957
  teardowns: /* @__PURE__ */ new Set()
@@ -850,9 +989,27 @@ var Maytes = (options, internal) => {
850
989
  state.config.baseUrl
851
990
  );
852
991
  const sdk = {
992
+ instanceId: state.instanceId,
853
993
  renderButton(container, opts) {
854
994
  return renderButton(state, container, opts);
855
995
  },
996
+ openCheckout(opts) {
997
+ if (state.destroyed) {
998
+ throw new MaytesError(MaytesErrorCode.Config, "openCheckout() called on a destroyed Maytes instance");
999
+ }
1000
+ const mode = assertLaunchMode(opts?.mode ?? "popup", "openCheckout");
1001
+ const redirectOverlay = assertRedirectOverlay(opts?.redirectOverlay, "openCheckout");
1002
+ return launchCheckout(state, mode, "api", redirectOverlay);
1003
+ },
1004
+ onBusyChange(listener) {
1005
+ if (state.destroyed) {
1006
+ throw new MaytesError(MaytesErrorCode.Config, "onBusyChange() called on a destroyed Maytes instance");
1007
+ }
1008
+ state.busyViews.add(listener);
1009
+ return () => {
1010
+ state.busyViews.delete(listener);
1011
+ };
1012
+ },
856
1013
  redirectToCheckout(opts) {
857
1014
  redirectToCheckout(opts);
858
1015
  },
@@ -863,6 +1020,7 @@ var Maytes = (options, internal) => {
863
1020
  if (state.destroyed) return;
864
1021
  state.destroyed = true;
865
1022
  stopPopupPoll(state);
1023
+ state.restoreDetach?.();
866
1024
  hideOverlay(state);
867
1025
  if (!state.popupNavigated) closePopupWindow(state.popupWindow);
868
1026
  state.popupWindow = null;