@maytes/checkout-button 1.1.1 → 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 +49 -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 +271 -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.aed57a66.mjs → checkout-button.d6e444ee.mjs} +271 -145
  10. package/dist/checkout-button.d6e444ee.mjs.map +1 -0
  11. package/dist/{checkout-button.2aad12c3.cjs → checkout-button.d7bc9aea.cjs} +271 -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 +271 -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.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.0";
349
390
 
350
391
  // src/styles.ts
351
392
  var STYLE_ATTR = "data-maytes-checkout-button";
@@ -358,9 +399,10 @@ 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
404
  border: none;
363
- border-radius: 999px;
405
+ border-radius: var(--maytes-button-radius, 999px);
364
406
  background: ${foundation.brand.primary};
365
407
  color: ${foundation.text.primaryInverse};
366
408
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
@@ -380,6 +422,10 @@ var BUTTON_CSS = `
380
422
  cursor: progress;
381
423
  opacity: 0.7;
382
424
  }
425
+ .maytes-checkout-button--sized {
426
+ box-sizing: border-box;
427
+ white-space: nowrap;
428
+ }
383
429
  .maytes-checkout-button--block {
384
430
  display: flex;
385
431
  width: 100%;
@@ -528,24 +574,12 @@ function releaseStyles() {
528
574
  if (el !== null) el.remove();
529
575
  }
530
576
 
531
- // src/button.ts
577
+ // src/launch.ts
532
578
  var POPUP_WIDTH = 500;
533
579
  var POPUP_HEIGHT = 800;
534
580
  var POPUP_POLL_INTERVAL_MS = 500;
535
581
  var MOBILE_MAX_WIDTH = 600;
536
582
  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
583
  function isValidCheckoutResult(value) {
550
584
  if (typeof value !== "object" || value === null) return false;
551
585
  const obj = value;
@@ -644,6 +678,148 @@ function paintPopupLoadingScreen(popup, cspNonce) {
644
678
  slow.hidden = false;
645
679
  }, SLOW_NETWORK_DELAY_MS);
646
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
+ }
647
823
  function renderButton(state, container, options = {}) {
648
824
  if (state.destroyed) {
649
825
  throw new MaytesError(
@@ -654,120 +830,47 @@ function renderButton(state, container, options = {}) {
654
830
  if (!(container instanceof HTMLElement)) {
655
831
  throw new MaytesError(MaytesErrorCode.Config, "renderButton(container) requires an HTMLElement");
656
832
  }
833
+ const appearance = assertAppearance(options);
834
+ const redirectOverlay = assertRedirectOverlay(options.redirectOverlay);
657
835
  ensureStylesInjected(state.config.cspNonce);
658
836
  const label = options.label ?? "Split with";
659
837
  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
- }
838
+ const mode = assertLaunchMode(options.mode ?? "popup");
667
839
  const button = document.createElement("button");
668
840
  button.type = "button";
669
841
  button.className = block ? "maytes-checkout-button maytes-checkout-button--block" : "maytes-checkout-button";
842
+ applyAppearance(button, appearance);
670
843
  button.setAttribute("aria-label", `${label} Maytes`);
671
844
  const labelNode = document.createTextNode(`${label} `);
672
- const logo = buildLogo();
845
+ let icon = buildLogo();
673
846
  button.appendChild(labelNode);
674
- button.appendChild(logo);
675
- const setBusy = (busy) => {
676
- state.busy = busy;
847
+ button.appendChild(icon);
848
+ const showBusy = (busy) => {
677
849
  if (busy) {
678
850
  button.setAttribute("aria-disabled", "true");
679
851
  button.setAttribute("aria-busy", "true");
680
- logo.replaceWith(buildSpinner());
852
+ const spinner = buildSpinner();
853
+ icon.replaceWith(spinner);
854
+ icon = spinner;
681
855
  } else {
682
856
  button.removeAttribute("aria-disabled");
683
857
  button.removeAttribute("aria-busy");
684
- const currentIndicator = button.querySelector(".maytes-checkout-button__spinner, .maytes-checkout-button__logo");
685
- if (currentIndicator !== null) currentIndicator.replaceWith(buildLogo());
686
- }
687
- };
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 }));
704
- };
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;
858
+ const nextLogo = buildLogo();
859
+ icon.replaceWith(nextLogo);
860
+ icon = nextLogo;
712
861
  }
713
- dispatchRedirected(url, outcome.target);
714
862
  };
715
- const closeOrphanPopup = (popup) => {
716
- closePopupWindow(popup);
717
- stopPopupPoll(state);
718
- if (state.popupWindow === popup) state.popupWindow = null;
719
- teardownActiveCheckout();
863
+ let launchedHere = false;
864
+ const followOwnLaunch = (busy) => {
865
+ if (!launchedHere) return;
866
+ showBusy(busy);
867
+ if (!busy) launchedHere = false;
720
868
  };
721
- const handleClick = async () => {
869
+ state.busyViews.add(followOwnLaunch);
870
+ const handleClick = () => {
722
871
  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
- }
872
+ launchedHere = true;
873
+ void launchCheckout(state, mode, "button", redirectOverlay);
771
874
  };
772
875
  button.addEventListener("click", handleClick);
773
876
  container.appendChild(button);
@@ -777,6 +880,7 @@ function renderButton(state, container, options = {}) {
777
880
  cleaned = true;
778
881
  button.removeEventListener("click", handleClick);
779
882
  button.remove();
883
+ state.busyViews.delete(followOwnLaunch);
780
884
  state.teardowns.delete(teardown);
781
885
  releaseStyles();
782
886
  };
@@ -785,18 +889,18 @@ function renderButton(state, container, options = {}) {
785
889
  }
786
890
 
787
891
  // src/state.ts
788
- function popupNameSuffix() {
892
+ function randomSuffix() {
789
893
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
790
894
  }
791
- function generatePopupName() {
895
+ function randomId() {
792
896
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
793
897
  try {
794
- return `maytes-checkout-${crypto.randomUUID()}`;
898
+ return crypto.randomUUID();
795
899
  } catch {
796
- return `maytes-checkout-${popupNameSuffix()}`;
900
+ return randomSuffix();
797
901
  }
798
902
  }
799
- return `maytes-checkout-${popupNameSuffix()}`;
903
+ return randomSuffix();
800
904
  }
801
905
  function createInstanceState(options, internal) {
802
906
  const config = {
@@ -808,11 +912,14 @@ function createInstanceState(options, internal) {
808
912
  return {
809
913
  config,
810
914
  busy: false,
915
+ busyViews: /* @__PURE__ */ new Set(),
916
+ instanceId: randomId(),
811
917
  overlayEl: null,
812
918
  overlayDetach: null,
919
+ restoreDetach: null,
813
920
  popupWindow: null,
814
921
  popupNavigated: false,
815
- popupName: generatePopupName(),
922
+ popupName: `maytes-checkout-${randomId()}`,
816
923
  popupPollHandle: null,
817
924
  destroyed: false,
818
925
  teardowns: /* @__PURE__ */ new Set()
@@ -850,9 +957,27 @@ var Maytes = (options, internal) => {
850
957
  state.config.baseUrl
851
958
  );
852
959
  const sdk = {
960
+ instanceId: state.instanceId,
853
961
  renderButton(container, opts) {
854
962
  return renderButton(state, container, opts);
855
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
+ },
856
981
  redirectToCheckout(opts) {
857
982
  redirectToCheckout(opts);
858
983
  },
@@ -863,6 +988,7 @@ var Maytes = (options, internal) => {
863
988
  if (state.destroyed) return;
864
989
  state.destroyed = true;
865
990
  stopPopupPoll(state);
991
+ state.restoreDetach?.();
866
992
  hideOverlay(state);
867
993
  if (!state.popupNavigated) closePopupWindow(state.popupWindow);
868
994
  state.popupWindow = null;