@maytes/checkout-button 1.1.0 → 1.2.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +97 -53
  3. package/dist/checkout-button.c640180e.js +156 -0
  4. package/dist/checkout-button.c640180e.js.map +1 -0
  5. package/dist/checkout-button.cjs +280 -145
  6. package/dist/checkout-button.cjs.map +1 -1
  7. package/dist/checkout-button.d.cts +40 -4
  8. package/dist/checkout-button.d.ts +40 -4
  9. package/dist/{checkout-button.f96c49ee.mjs → checkout-button.d6e444ee.mjs} +280 -145
  10. package/dist/checkout-button.d6e444ee.mjs.map +1 -0
  11. package/dist/{checkout-button.178d8394.cjs → checkout-button.d7bc9aea.cjs} +280 -145
  12. package/dist/checkout-button.d7bc9aea.cjs.map +1 -0
  13. package/dist/checkout-button.element.6c530c24.js +155 -0
  14. package/dist/checkout-button.element.6c530c24.js.map +1 -0
  15. package/dist/checkout-button.element.js +155 -0
  16. package/dist/checkout-button.element.js.map +1 -0
  17. package/dist/checkout-button.js +19 -14
  18. package/dist/checkout-button.js.map +1 -1
  19. package/dist/checkout-button.mjs +280 -145
  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.178d8394.cjs.map +0 -1
  53. package/dist/checkout-button.811263f9.js +0 -151
  54. package/dist/checkout-button.811263f9.js.map +0 -1
  55. package/dist/checkout-button.f96c49ee.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,13 +263,14 @@ 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);
273
+ if (host !== document) detachOverlayWhenThisPageHides(state);
232
274
  if (typeof dialog.showModal === "function") {
233
275
  try {
234
276
  dialog.showModal();
@@ -242,8 +284,15 @@ function showOverlay(state) {
242
284
  applyNativeBackdropFallback(dialog);
243
285
  }
244
286
  }
287
+ function detachOverlayWhenThisPageHides(state) {
288
+ const onPageHide = () => hideOverlay(state);
289
+ window.addEventListener("pagehide", onPageHide);
290
+ state.overlayDetach = () => window.removeEventListener("pagehide", onPageHide);
291
+ }
245
292
  function hideOverlay(state) {
246
293
  if (state.overlayEl === null) return;
294
+ state.overlayDetach?.();
295
+ state.overlayDetach = null;
247
296
  const dialog = state.overlayEl;
248
297
  const host = dialog.ownerDocument;
249
298
  if (typeof dialog.close === "function" && dialog.open) dialog.close();
@@ -337,7 +386,7 @@ function makeRedirect(environment, baseUrlOverride) {
337
386
  }
338
387
 
339
388
  // src/version.ts
340
- var SDK_VERSION = "1.1.0";
389
+ var SDK_VERSION = "1.2.0";
341
390
 
342
391
  // src/styles.ts
343
392
  var STYLE_ATTR = "data-maytes-checkout-button";
@@ -350,9 +399,10 @@ var BUTTON_CSS = `
350
399
  align-items: center;
351
400
  justify-content: center;
352
401
  gap: 0.57em;
402
+ min-height: var(--maytes-button-height, auto);
353
403
  padding: 0.71em 1.29em;
354
404
  border: none;
355
- border-radius: 999px;
405
+ border-radius: var(--maytes-button-radius, 999px);
356
406
  background: ${foundation.brand.primary};
357
407
  color: ${foundation.text.primaryInverse};
358
408
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
@@ -372,6 +422,10 @@ var BUTTON_CSS = `
372
422
  cursor: progress;
373
423
  opacity: 0.7;
374
424
  }
425
+ .maytes-checkout-button--sized {
426
+ box-sizing: border-box;
427
+ white-space: nowrap;
428
+ }
375
429
  .maytes-checkout-button--block {
376
430
  display: flex;
377
431
  width: 100%;
@@ -520,24 +574,12 @@ function releaseStyles() {
520
574
  if (el !== null) el.remove();
521
575
  }
522
576
 
523
- // src/button.ts
577
+ // src/launch.ts
524
578
  var POPUP_WIDTH = 500;
525
579
  var POPUP_HEIGHT = 800;
526
580
  var POPUP_POLL_INTERVAL_MS = 500;
527
581
  var MOBILE_MAX_WIDTH = 600;
528
582
  var SLOW_NETWORK_DELAY_MS = 8e3;
529
- function buildLogo() {
530
- const svg = buildMaytesLogo("1em", "currentColor");
531
- svg.setAttribute("class", "maytes-checkout-button__logo");
532
- svg.setAttribute("aria-hidden", "true");
533
- return svg;
534
- }
535
- function buildSpinner() {
536
- const spinner = document.createElement("span");
537
- spinner.className = "maytes-checkout-button__spinner";
538
- spinner.setAttribute("aria-hidden", "true");
539
- return spinner;
540
- }
541
583
  function isValidCheckoutResult(value) {
542
584
  if (typeof value !== "object" || value === null) return false;
543
585
  const obj = value;
@@ -636,6 +678,148 @@ function paintPopupLoadingScreen(popup, cspNonce) {
636
678
  slow.hidden = false;
637
679
  }, SLOW_NETWORK_DELAY_MS);
638
680
  }
681
+ function eventDetail(state, source) {
682
+ return { instanceId: state.instanceId, source };
683
+ }
684
+ function dispatchFailed(origin, reason, cause) {
685
+ const detail = cause === void 0 ? { ...origin, reason } : { ...origin, reason, cause };
686
+ document.dispatchEvent(new CustomEvent("maytes:checkout-failed", { detail }));
687
+ }
688
+ function dispatchRedirected(origin, url, target) {
689
+ const detail = { ...origin, url, target };
690
+ document.dispatchEvent(new CustomEvent("maytes:checkout-redirected", { detail }));
691
+ }
692
+ function assertLaunchMode(mode, caller = "renderButton") {
693
+ if (mode !== "redirect" && mode !== "popup") {
694
+ throw new MaytesError(MaytesErrorCode.Config, `${caller}({ mode }) must be 'redirect' or 'popup'`);
695
+ }
696
+ return mode;
697
+ }
698
+ function assertRedirectOverlay(value, caller = "renderButton") {
699
+ if (value === void 0) return false;
700
+ if (typeof value !== "boolean") {
701
+ throw new MaytesError(MaytesErrorCode.Config, `${caller}({ redirectOverlay }) must be true or false`);
702
+ }
703
+ return value;
704
+ }
705
+ function setInstanceBusy(state, busy) {
706
+ state.busy = busy;
707
+ for (const view of [...state.busyViews]) view(busy);
708
+ }
709
+ function endLaunch(state) {
710
+ hideOverlay(state);
711
+ setInstanceBusy(state, false);
712
+ }
713
+ function closeOrphanPopup(state, popup) {
714
+ closePopupWindow(popup);
715
+ stopPopupPoll(state);
716
+ if (state.popupWindow === popup) state.popupWindow = null;
717
+ endLaunch(state);
718
+ }
719
+ function clearOverlayOnRestore(state) {
720
+ if (state.restoreDetach !== null) return;
721
+ const onPageShow = (event) => {
722
+ if (!event.persisted) return;
723
+ detach();
724
+ endLaunch(state);
725
+ };
726
+ const detach = () => {
727
+ window.removeEventListener("pageshow", onPageShow);
728
+ state.restoreDetach = null;
729
+ };
730
+ window.addEventListener("pageshow", onPageShow);
731
+ state.restoreDetach = detach;
732
+ }
733
+ function navigateAway(state, origin, url, keepOverlay) {
734
+ const navigation = navigateTopLevel(url, false);
735
+ if (keepOverlay && (navigation.target === "self" || navigation.target === "top")) {
736
+ setInstanceBusy(state, false);
737
+ clearOverlayOnRestore(state);
738
+ } else {
739
+ endLaunch(state);
740
+ }
741
+ if (navigation.target === null) {
742
+ console.error("[maytes/checkout-button] could not leave the embedding frame:", navigation.cause);
743
+ dispatchFailed(origin, "navigation-blocked", navigation.cause);
744
+ return { outcome: "failed", reason: "navigation-blocked" };
745
+ }
746
+ dispatchRedirected(origin, url, navigation.target);
747
+ return { outcome: "redirected", target: navigation.target };
748
+ }
749
+ function openPopupLoader(state, origin) {
750
+ const popup = openBlankPopup(state);
751
+ if (popup === null) return null;
752
+ state.popupWindow = popup;
753
+ state.popupNavigated = false;
754
+ refocusPopup(popup);
755
+ paintPopupLoadingScreen(popup, state.config.cspNonce);
756
+ showOverlay(state);
757
+ document.dispatchEvent(new CustomEvent("maytes:checkout-opened", { detail: origin }));
758
+ startPopupPoll(state, () => {
759
+ endLaunch(state);
760
+ document.dispatchEvent(new CustomEvent("maytes:checkout-closed", { detail: origin }));
761
+ });
762
+ return popup;
763
+ }
764
+ async function launchCheckout(state, mode, source, redirectOverlay = false) {
765
+ if (state.busy || state.destroyed) return { outcome: "ignored" };
766
+ setInstanceBusy(state, true);
767
+ const origin = eventDetail(state, source);
768
+ const shouldUsePopup = mode === "popup" && !isMobileViewport();
769
+ const popup = shouldUsePopup ? openPopupLoader(state, origin) : null;
770
+ if (popup === null && redirectOverlay) showOverlay(state, "redirect");
771
+ try {
772
+ const result = await state.config.createCheckout();
773
+ if (state.destroyed || popup !== null && popup.closed) {
774
+ closePopupWindow(popup);
775
+ return { outcome: "closed" };
776
+ }
777
+ if (!isValidCheckoutResult(result)) {
778
+ closeOrphanPopup(state, popup);
779
+ const shapeError = new MaytesError(
780
+ MaytesErrorCode.Config,
781
+ "createCheckout must resolve to { checkoutId: string, checkoutUrl?: http(s) URL }"
782
+ );
783
+ console.error(shapeError);
784
+ dispatchFailed(origin, "invalid-shape", shapeError);
785
+ return { outcome: "failed", reason: "invalid-shape" };
786
+ }
787
+ const url = result.checkoutUrl ?? buildCheckoutUrl(
788
+ state.config.environment,
789
+ state.config.baseUrl,
790
+ result.checkoutId
791
+ );
792
+ if (popup !== null) {
793
+ state.popupNavigated = true;
794
+ navigatePopup(popup, url);
795
+ return { outcome: "popup" };
796
+ }
797
+ if (shouldUsePopup) {
798
+ console.warn("[maytes/checkout-button] popup was blocked; redirecting instead");
799
+ }
800
+ return navigateAway(state, origin, url, redirectOverlay);
801
+ } catch (err) {
802
+ if (popup !== null && popup.closed) return { outcome: "closed" };
803
+ closeOrphanPopup(state, popup);
804
+ console.error("[maytes/checkout-button] createCheckout failed:", err);
805
+ dispatchFailed(origin, "create-checkout-rejected", err);
806
+ return { outcome: "failed", reason: "create-checkout-rejected" };
807
+ }
808
+ }
809
+
810
+ // src/button.ts
811
+ function buildLogo() {
812
+ const svg = buildMaytesLogo("1em", "currentColor");
813
+ svg.setAttribute("class", "maytes-checkout-button__logo");
814
+ svg.setAttribute("aria-hidden", "true");
815
+ return svg;
816
+ }
817
+ function buildSpinner() {
818
+ const spinner = document.createElement("span");
819
+ spinner.className = "maytes-checkout-button__spinner";
820
+ spinner.setAttribute("aria-hidden", "true");
821
+ return spinner;
822
+ }
639
823
  function renderButton(state, container, options = {}) {
640
824
  if (state.destroyed) {
641
825
  throw new MaytesError(
@@ -646,120 +830,47 @@ function renderButton(state, container, options = {}) {
646
830
  if (!(container instanceof HTMLElement)) {
647
831
  throw new MaytesError(MaytesErrorCode.Config, "renderButton(container) requires an HTMLElement");
648
832
  }
833
+ const appearance = assertAppearance(options);
834
+ const redirectOverlay = assertRedirectOverlay(options.redirectOverlay);
649
835
  ensureStylesInjected(state.config.cspNonce);
650
836
  const label = options.label ?? "Split with";
651
837
  const block = options.block === true;
652
- const mode = options.mode ?? "popup";
653
- if (mode !== "redirect" && mode !== "popup") {
654
- throw new MaytesError(
655
- MaytesErrorCode.Config,
656
- "renderButton({ mode }) must be 'redirect' or 'popup'"
657
- );
658
- }
838
+ const mode = assertLaunchMode(options.mode ?? "popup");
659
839
  const button = document.createElement("button");
660
840
  button.type = "button";
661
841
  button.className = block ? "maytes-checkout-button maytes-checkout-button--block" : "maytes-checkout-button";
842
+ applyAppearance(button, appearance);
662
843
  button.setAttribute("aria-label", `${label} Maytes`);
663
844
  const labelNode = document.createTextNode(`${label} `);
664
- const logo = buildLogo();
845
+ let icon = buildLogo();
665
846
  button.appendChild(labelNode);
666
- button.appendChild(logo);
667
- const setBusy = (busy) => {
668
- state.busy = busy;
847
+ button.appendChild(icon);
848
+ const showBusy = (busy) => {
669
849
  if (busy) {
670
850
  button.setAttribute("aria-disabled", "true");
671
851
  button.setAttribute("aria-busy", "true");
672
- logo.replaceWith(buildSpinner());
852
+ const spinner = buildSpinner();
853
+ icon.replaceWith(spinner);
854
+ icon = spinner;
673
855
  } else {
674
856
  button.removeAttribute("aria-disabled");
675
857
  button.removeAttribute("aria-busy");
676
- const currentIndicator = button.querySelector(".maytes-checkout-button__spinner, .maytes-checkout-button__logo");
677
- if (currentIndicator !== null) currentIndicator.replaceWith(buildLogo());
678
- }
679
- };
680
- const teardownActiveCheckout = () => {
681
- hideOverlay(state);
682
- setBusy(false);
683
- };
684
- const onPopupClosed = () => {
685
- teardownActiveCheckout();
686
- document.dispatchEvent(new CustomEvent("maytes:checkout-closed"));
687
- };
688
- const dispatchFailed = (reason, cause) => {
689
- document.dispatchEvent(new CustomEvent("maytes:checkout-failed", {
690
- detail: cause === void 0 ? { reason } : { reason, cause }
691
- }));
692
- };
693
- const dispatchRedirected = (url, target) => {
694
- const detail = { url, target };
695
- document.dispatchEvent(new CustomEvent("maytes:checkout-redirected", { detail }));
696
- };
697
- const navigateAway = (url) => {
698
- const outcome = navigateTopLevel(url, false);
699
- teardownActiveCheckout();
700
- if (outcome.target === null) {
701
- console.error("[maytes/checkout-button] could not leave the embedding frame:", outcome.cause);
702
- dispatchFailed("navigation-blocked", outcome.cause);
703
- return;
858
+ const nextLogo = buildLogo();
859
+ icon.replaceWith(nextLogo);
860
+ icon = nextLogo;
704
861
  }
705
- dispatchRedirected(url, outcome.target);
706
862
  };
707
- const closeOrphanPopup = (popup) => {
708
- closePopupWindow(popup);
709
- stopPopupPoll(state);
710
- if (state.popupWindow === popup) state.popupWindow = null;
711
- teardownActiveCheckout();
863
+ let launchedHere = false;
864
+ const followOwnLaunch = (busy) => {
865
+ if (!launchedHere) return;
866
+ showBusy(busy);
867
+ if (!busy) launchedHere = false;
712
868
  };
713
- const handleClick = async () => {
869
+ state.busyViews.add(followOwnLaunch);
870
+ const handleClick = () => {
714
871
  if (state.busy || state.destroyed) return;
715
- setBusy(true);
716
- const shouldUsePopup = mode === "popup" && !isMobileViewport();
717
- const popup = shouldUsePopup ? openBlankPopup(state) : null;
718
- if (popup !== null) {
719
- state.popupWindow = popup;
720
- state.popupNavigated = false;
721
- refocusPopup(popup);
722
- paintPopupLoadingScreen(popup, state.config.cspNonce);
723
- showOverlay(state);
724
- document.dispatchEvent(new CustomEvent("maytes:checkout-opened"));
725
- startPopupPoll(state, onPopupClosed);
726
- }
727
- try {
728
- const result = await state.config.createCheckout();
729
- if (state.destroyed || popup !== null && popup.closed) {
730
- closePopupWindow(popup);
731
- return;
732
- }
733
- if (!isValidCheckoutResult(result)) {
734
- closeOrphanPopup(popup);
735
- const shapeError = new MaytesError(
736
- MaytesErrorCode.Config,
737
- "createCheckout must resolve to { checkoutId: string, checkoutUrl?: http(s) URL }"
738
- );
739
- console.error(shapeError);
740
- dispatchFailed("invalid-shape", shapeError);
741
- return;
742
- }
743
- const url = result.checkoutUrl ?? buildCheckoutUrl(
744
- state.config.environment,
745
- state.config.baseUrl,
746
- result.checkoutId
747
- );
748
- if (popup !== null) {
749
- state.popupNavigated = true;
750
- navigatePopup(popup, url);
751
- } else {
752
- if (shouldUsePopup) {
753
- console.warn("[maytes/checkout-button] popup was blocked; redirecting instead");
754
- }
755
- navigateAway(url);
756
- }
757
- } catch (err) {
758
- if (popup !== null && popup.closed) return;
759
- closeOrphanPopup(popup);
760
- console.error("[maytes/checkout-button] createCheckout failed:", err);
761
- dispatchFailed("create-checkout-rejected", err);
762
- }
872
+ launchedHere = true;
873
+ void launchCheckout(state, mode, "button", redirectOverlay);
763
874
  };
764
875
  button.addEventListener("click", handleClick);
765
876
  container.appendChild(button);
@@ -769,6 +880,7 @@ function renderButton(state, container, options = {}) {
769
880
  cleaned = true;
770
881
  button.removeEventListener("click", handleClick);
771
882
  button.remove();
883
+ state.busyViews.delete(followOwnLaunch);
772
884
  state.teardowns.delete(teardown);
773
885
  releaseStyles();
774
886
  };
@@ -777,18 +889,18 @@ function renderButton(state, container, options = {}) {
777
889
  }
778
890
 
779
891
  // src/state.ts
780
- function popupNameSuffix() {
892
+ function randomSuffix() {
781
893
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
782
894
  }
783
- function generatePopupName() {
895
+ function randomId() {
784
896
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
785
897
  try {
786
- return `maytes-checkout-${crypto.randomUUID()}`;
898
+ return crypto.randomUUID();
787
899
  } catch {
788
- return `maytes-checkout-${popupNameSuffix()}`;
900
+ return randomSuffix();
789
901
  }
790
902
  }
791
- return `maytes-checkout-${popupNameSuffix()}`;
903
+ return randomSuffix();
792
904
  }
793
905
  function createInstanceState(options, internal) {
794
906
  const config = {
@@ -800,10 +912,14 @@ function createInstanceState(options, internal) {
800
912
  return {
801
913
  config,
802
914
  busy: false,
915
+ busyViews: /* @__PURE__ */ new Set(),
916
+ instanceId: randomId(),
803
917
  overlayEl: null,
918
+ overlayDetach: null,
919
+ restoreDetach: null,
804
920
  popupWindow: null,
805
921
  popupNavigated: false,
806
- popupName: generatePopupName(),
922
+ popupName: `maytes-checkout-${randomId()}`,
807
923
  popupPollHandle: null,
808
924
  destroyed: false,
809
925
  teardowns: /* @__PURE__ */ new Set()
@@ -841,9 +957,27 @@ var Maytes = (options, internal) => {
841
957
  state.config.baseUrl
842
958
  );
843
959
  const sdk = {
960
+ instanceId: state.instanceId,
844
961
  renderButton(container, opts) {
845
962
  return renderButton(state, container, opts);
846
963
  },
964
+ openCheckout(opts) {
965
+ if (state.destroyed) {
966
+ throw new MaytesError(MaytesErrorCode.Config, "openCheckout() called on a destroyed Maytes instance");
967
+ }
968
+ const mode = assertLaunchMode(opts?.mode ?? "popup", "openCheckout");
969
+ const redirectOverlay = assertRedirectOverlay(opts?.redirectOverlay, "openCheckout");
970
+ return launchCheckout(state, mode, "api", redirectOverlay);
971
+ },
972
+ onBusyChange(listener) {
973
+ if (state.destroyed) {
974
+ throw new MaytesError(MaytesErrorCode.Config, "onBusyChange() called on a destroyed Maytes instance");
975
+ }
976
+ state.busyViews.add(listener);
977
+ return () => {
978
+ state.busyViews.delete(listener);
979
+ };
980
+ },
847
981
  redirectToCheckout(opts) {
848
982
  redirectToCheckout(opts);
849
983
  },
@@ -854,6 +988,7 @@ var Maytes = (options, internal) => {
854
988
  if (state.destroyed) return;
855
989
  state.destroyed = true;
856
990
  stopPopupPoll(state);
991
+ state.restoreDetach?.();
857
992
  hideOverlay(state);
858
993
  if (!state.popupNavigated) closePopupWindow(state.popupWindow);
859
994
  state.popupWindow = null;