@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
@@ -1,3 +1,54 @@
1
+ // src/errors.ts
2
+ var MaytesErrorCode = {
3
+ Config: "CONFIG"
4
+ };
5
+ var MaytesError = class extends Error {
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ this.name = "MaytesError";
10
+ }
11
+ };
12
+
13
+ // src/appearance.ts
14
+ var MIN_BUTTON_HEIGHT = 40;
15
+ var MAX_BUTTON_HEIGHT = 55;
16
+ var MAX_BUTTON_RADIUS = 999;
17
+ function isWholeNumber(value) {
18
+ return typeof value === "number" && Number.isInteger(value);
19
+ }
20
+ function assertAppearance(options) {
21
+ const { radius, height } = options;
22
+ const appearance = {};
23
+ if (height !== void 0) {
24
+ if (!isWholeNumber(height) || height < MIN_BUTTON_HEIGHT || height > MAX_BUTTON_HEIGHT) {
25
+ throw new MaytesError(
26
+ MaytesErrorCode.Config,
27
+ `renderButton({ height }) must be a whole number of pixels from ${MIN_BUTTON_HEIGHT} to ${MAX_BUTTON_HEIGHT}`
28
+ );
29
+ }
30
+ appearance.height = height;
31
+ }
32
+ if (radius !== void 0) {
33
+ const maxRadius = height === void 0 ? MAX_BUTTON_RADIUS : Math.floor(height / 2);
34
+ if (!isWholeNumber(radius) || radius < 0 || radius > maxRadius) {
35
+ throw new MaytesError(
36
+ MaytesErrorCode.Config,
37
+ `renderButton({ radius }) must be a whole number of pixels from 0 to ${maxRadius}`
38
+ );
39
+ }
40
+ appearance.radius = radius;
41
+ }
42
+ return appearance;
43
+ }
44
+ function applyAppearance(button, appearance) {
45
+ if (appearance.radius !== void 0) button.style.setProperty("--maytes-button-radius", `${appearance.radius}px`);
46
+ if (appearance.height !== void 0) {
47
+ button.style.setProperty("--maytes-button-height", `${appearance.height}px`);
48
+ button.classList.add("maytes-checkout-button--sized");
49
+ }
50
+ }
51
+
1
52
  // src/foundation/brand.generated.ts
2
53
  var foundation = {
3
54
  brand: {
@@ -45,18 +96,6 @@ function buildMaytesLogo(height, color) {
45
96
  return svg;
46
97
  }
47
98
 
48
- // src/errors.ts
49
- var MaytesErrorCode = {
50
- Config: "CONFIG"
51
- };
52
- var MaytesError = class extends Error {
53
- constructor(code, message) {
54
- super(message);
55
- this.code = code;
56
- this.name = "MaytesError";
57
- }
58
- };
59
-
60
99
  // src/framing.ts
61
100
  function isSecurityError(err) {
62
101
  return typeof err === "object" && err !== null && err.name === "SecurityError";
@@ -146,7 +185,7 @@ function ensureOverlayStylesInTopDocument(host) {
146
185
  copy.textContent = localStyle.textContent ?? "";
147
186
  host.head.appendChild(copy);
148
187
  }
149
- function buildOverlay(host, state) {
188
+ function buildOverlay(host, state, variant) {
150
189
  const dialog = host.createElement("dialog");
151
190
  dialog.className = "maytes-checkout-overlay";
152
191
  dialog.setAttribute("aria-labelledby", TITLE_ID);
@@ -166,22 +205,24 @@ function buildOverlay(host, state) {
166
205
  const text = host.createElement("p");
167
206
  text.id = TITLE_ID;
168
207
  text.className = "maytes-checkout-overlay__text";
169
- text.textContent = "Completing checkout with Maytes\u2026";
208
+ text.textContent = variant === "popup" ? "Completing checkout with Maytes\u2026" : "Taking you to Maytes\u2026";
170
209
  content.appendChild(text);
171
- const link = host.createElement("button");
172
- link.className = "maytes-checkout-overlay__link";
173
- link.type = "button";
174
- link.textContent = "Return to Maytes";
175
- link.addEventListener("click", () => {
176
- if (state.popupWindow !== null && !state.popupWindow.closed) {
177
- try {
178
- state.popupWindow.focus();
179
- } catch (err) {
180
- if (!isSecurityError(err)) throw err;
210
+ if (variant === "popup") {
211
+ const link = host.createElement("button");
212
+ link.className = "maytes-checkout-overlay__link";
213
+ link.type = "button";
214
+ link.textContent = "Return to Maytes";
215
+ link.addEventListener("click", () => {
216
+ if (state.popupWindow !== null && !state.popupWindow.closed) {
217
+ try {
218
+ state.popupWindow.focus();
219
+ } catch (err) {
220
+ if (!isSecurityError(err)) throw err;
221
+ }
181
222
  }
182
- }
183
- });
184
- content.appendChild(link);
223
+ });
224
+ content.appendChild(link);
225
+ }
185
226
  dialog.appendChild(content);
186
227
  dialog.addEventListener("cancel", (event) => {
187
228
  event.preventDefault();
@@ -193,11 +234,11 @@ function buildOverlay(host, state) {
193
234
  function applyNativeBackdropFallback(dialog) {
194
235
  dialog.style.background = NATIVE_BACKDROP_FALLBACK;
195
236
  }
196
- function showOverlay(state) {
237
+ function showOverlay(state, variant = "popup") {
197
238
  if (state.overlayEl !== null) return;
198
239
  const host = resolveOverlayHost();
199
240
  ensureOverlayStylesInTopDocument(host);
200
- const dialog = buildOverlay(host, state);
241
+ const dialog = buildOverlay(host, state, variant);
201
242
  state.overlayEl = dialog;
202
243
  host.body.appendChild(dialog);
203
244
  if (host !== document) detachOverlayWhenThisPageHides(state);
@@ -316,7 +357,7 @@ function makeRedirect(environment, baseUrlOverride) {
316
357
  }
317
358
 
318
359
  // src/version.ts
319
- var SDK_VERSION = "1.1.1";
360
+ var SDK_VERSION = "1.2.0";
320
361
 
321
362
  // src/styles.ts
322
363
  var STYLE_ATTR = "data-maytes-checkout-button";
@@ -329,9 +370,10 @@ var BUTTON_CSS = `
329
370
  align-items: center;
330
371
  justify-content: center;
331
372
  gap: 0.57em;
373
+ min-height: var(--maytes-button-height, auto);
332
374
  padding: 0.71em 1.29em;
333
375
  border: none;
334
- border-radius: 999px;
376
+ border-radius: var(--maytes-button-radius, 999px);
335
377
  background: ${foundation.brand.primary};
336
378
  color: ${foundation.text.primaryInverse};
337
379
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
@@ -351,6 +393,10 @@ var BUTTON_CSS = `
351
393
  cursor: progress;
352
394
  opacity: 0.7;
353
395
  }
396
+ .maytes-checkout-button--sized {
397
+ box-sizing: border-box;
398
+ white-space: nowrap;
399
+ }
354
400
  .maytes-checkout-button--block {
355
401
  display: flex;
356
402
  width: 100%;
@@ -499,24 +545,12 @@ function releaseStyles() {
499
545
  if (el !== null) el.remove();
500
546
  }
501
547
 
502
- // src/button.ts
548
+ // src/launch.ts
503
549
  var POPUP_WIDTH = 500;
504
550
  var POPUP_HEIGHT = 800;
505
551
  var POPUP_POLL_INTERVAL_MS = 500;
506
552
  var MOBILE_MAX_WIDTH = 600;
507
553
  var SLOW_NETWORK_DELAY_MS = 8e3;
508
- function buildLogo() {
509
- const svg = buildMaytesLogo("1em", "currentColor");
510
- svg.setAttribute("class", "maytes-checkout-button__logo");
511
- svg.setAttribute("aria-hidden", "true");
512
- return svg;
513
- }
514
- function buildSpinner() {
515
- const spinner = document.createElement("span");
516
- spinner.className = "maytes-checkout-button__spinner";
517
- spinner.setAttribute("aria-hidden", "true");
518
- return spinner;
519
- }
520
554
  function isValidCheckoutResult(value) {
521
555
  if (typeof value !== "object" || value === null) return false;
522
556
  const obj = value;
@@ -615,6 +649,148 @@ function paintPopupLoadingScreen(popup, cspNonce) {
615
649
  slow.hidden = false;
616
650
  }, SLOW_NETWORK_DELAY_MS);
617
651
  }
652
+ function eventDetail(state, source) {
653
+ return { instanceId: state.instanceId, source };
654
+ }
655
+ function dispatchFailed(origin, reason, cause) {
656
+ const detail = cause === void 0 ? { ...origin, reason } : { ...origin, reason, cause };
657
+ document.dispatchEvent(new CustomEvent("maytes:checkout-failed", { detail }));
658
+ }
659
+ function dispatchRedirected(origin, url, target) {
660
+ const detail = { ...origin, url, target };
661
+ document.dispatchEvent(new CustomEvent("maytes:checkout-redirected", { detail }));
662
+ }
663
+ function assertLaunchMode(mode, caller = "renderButton") {
664
+ if (mode !== "redirect" && mode !== "popup") {
665
+ throw new MaytesError(MaytesErrorCode.Config, `${caller}({ mode }) must be 'redirect' or 'popup'`);
666
+ }
667
+ return mode;
668
+ }
669
+ function assertRedirectOverlay(value, caller = "renderButton") {
670
+ if (value === void 0) return false;
671
+ if (typeof value !== "boolean") {
672
+ throw new MaytesError(MaytesErrorCode.Config, `${caller}({ redirectOverlay }) must be true or false`);
673
+ }
674
+ return value;
675
+ }
676
+ function setInstanceBusy(state, busy) {
677
+ state.busy = busy;
678
+ for (const view of [...state.busyViews]) view(busy);
679
+ }
680
+ function endLaunch(state) {
681
+ hideOverlay(state);
682
+ setInstanceBusy(state, false);
683
+ }
684
+ function closeOrphanPopup(state, popup) {
685
+ closePopupWindow(popup);
686
+ stopPopupPoll(state);
687
+ if (state.popupWindow === popup) state.popupWindow = null;
688
+ endLaunch(state);
689
+ }
690
+ function clearOverlayOnRestore(state) {
691
+ if (state.restoreDetach !== null) return;
692
+ const onPageShow = (event) => {
693
+ if (!event.persisted) return;
694
+ detach();
695
+ endLaunch(state);
696
+ };
697
+ const detach = () => {
698
+ window.removeEventListener("pageshow", onPageShow);
699
+ state.restoreDetach = null;
700
+ };
701
+ window.addEventListener("pageshow", onPageShow);
702
+ state.restoreDetach = detach;
703
+ }
704
+ function navigateAway(state, origin, url, keepOverlay) {
705
+ const navigation = navigateTopLevel(url, false);
706
+ if (keepOverlay && (navigation.target === "self" || navigation.target === "top")) {
707
+ setInstanceBusy(state, false);
708
+ clearOverlayOnRestore(state);
709
+ } else {
710
+ endLaunch(state);
711
+ }
712
+ if (navigation.target === null) {
713
+ console.error("[maytes/checkout-button] could not leave the embedding frame:", navigation.cause);
714
+ dispatchFailed(origin, "navigation-blocked", navigation.cause);
715
+ return { outcome: "failed", reason: "navigation-blocked" };
716
+ }
717
+ dispatchRedirected(origin, url, navigation.target);
718
+ return { outcome: "redirected", target: navigation.target };
719
+ }
720
+ function openPopupLoader(state, origin) {
721
+ const popup = openBlankPopup(state);
722
+ if (popup === null) return null;
723
+ state.popupWindow = popup;
724
+ state.popupNavigated = false;
725
+ refocusPopup(popup);
726
+ paintPopupLoadingScreen(popup, state.config.cspNonce);
727
+ showOverlay(state);
728
+ document.dispatchEvent(new CustomEvent("maytes:checkout-opened", { detail: origin }));
729
+ startPopupPoll(state, () => {
730
+ endLaunch(state);
731
+ document.dispatchEvent(new CustomEvent("maytes:checkout-closed", { detail: origin }));
732
+ });
733
+ return popup;
734
+ }
735
+ async function launchCheckout(state, mode, source, redirectOverlay = false) {
736
+ if (state.busy || state.destroyed) return { outcome: "ignored" };
737
+ setInstanceBusy(state, true);
738
+ const origin = eventDetail(state, source);
739
+ const shouldUsePopup = mode === "popup" && !isMobileViewport();
740
+ const popup = shouldUsePopup ? openPopupLoader(state, origin) : null;
741
+ if (popup === null && redirectOverlay) showOverlay(state, "redirect");
742
+ try {
743
+ const result = await state.config.createCheckout();
744
+ if (state.destroyed || popup !== null && popup.closed) {
745
+ closePopupWindow(popup);
746
+ return { outcome: "closed" };
747
+ }
748
+ if (!isValidCheckoutResult(result)) {
749
+ closeOrphanPopup(state, popup);
750
+ const shapeError = new MaytesError(
751
+ MaytesErrorCode.Config,
752
+ "createCheckout must resolve to { checkoutId: string, checkoutUrl?: http(s) URL }"
753
+ );
754
+ console.error(shapeError);
755
+ dispatchFailed(origin, "invalid-shape", shapeError);
756
+ return { outcome: "failed", reason: "invalid-shape" };
757
+ }
758
+ const url = result.checkoutUrl ?? buildCheckoutUrl(
759
+ state.config.environment,
760
+ state.config.baseUrl,
761
+ result.checkoutId
762
+ );
763
+ if (popup !== null) {
764
+ state.popupNavigated = true;
765
+ navigatePopup(popup, url);
766
+ return { outcome: "popup" };
767
+ }
768
+ if (shouldUsePopup) {
769
+ console.warn("[maytes/checkout-button] popup was blocked; redirecting instead");
770
+ }
771
+ return navigateAway(state, origin, url, redirectOverlay);
772
+ } catch (err) {
773
+ if (popup !== null && popup.closed) return { outcome: "closed" };
774
+ closeOrphanPopup(state, popup);
775
+ console.error("[maytes/checkout-button] createCheckout failed:", err);
776
+ dispatchFailed(origin, "create-checkout-rejected", err);
777
+ return { outcome: "failed", reason: "create-checkout-rejected" };
778
+ }
779
+ }
780
+
781
+ // src/button.ts
782
+ function buildLogo() {
783
+ const svg = buildMaytesLogo("1em", "currentColor");
784
+ svg.setAttribute("class", "maytes-checkout-button__logo");
785
+ svg.setAttribute("aria-hidden", "true");
786
+ return svg;
787
+ }
788
+ function buildSpinner() {
789
+ const spinner = document.createElement("span");
790
+ spinner.className = "maytes-checkout-button__spinner";
791
+ spinner.setAttribute("aria-hidden", "true");
792
+ return spinner;
793
+ }
618
794
  function renderButton(state, container, options = {}) {
619
795
  if (state.destroyed) {
620
796
  throw new MaytesError(
@@ -625,120 +801,47 @@ function renderButton(state, container, options = {}) {
625
801
  if (!(container instanceof HTMLElement)) {
626
802
  throw new MaytesError(MaytesErrorCode.Config, "renderButton(container) requires an HTMLElement");
627
803
  }
804
+ const appearance = assertAppearance(options);
805
+ const redirectOverlay = assertRedirectOverlay(options.redirectOverlay);
628
806
  ensureStylesInjected(state.config.cspNonce);
629
807
  const label = options.label ?? "Split with";
630
808
  const block = options.block === true;
631
- const mode = options.mode ?? "popup";
632
- if (mode !== "redirect" && mode !== "popup") {
633
- throw new MaytesError(
634
- MaytesErrorCode.Config,
635
- "renderButton({ mode }) must be 'redirect' or 'popup'"
636
- );
637
- }
809
+ const mode = assertLaunchMode(options.mode ?? "popup");
638
810
  const button = document.createElement("button");
639
811
  button.type = "button";
640
812
  button.className = block ? "maytes-checkout-button maytes-checkout-button--block" : "maytes-checkout-button";
813
+ applyAppearance(button, appearance);
641
814
  button.setAttribute("aria-label", `${label} Maytes`);
642
815
  const labelNode = document.createTextNode(`${label} `);
643
- const logo = buildLogo();
816
+ let icon = buildLogo();
644
817
  button.appendChild(labelNode);
645
- button.appendChild(logo);
646
- const setBusy = (busy) => {
647
- state.busy = busy;
818
+ button.appendChild(icon);
819
+ const showBusy = (busy) => {
648
820
  if (busy) {
649
821
  button.setAttribute("aria-disabled", "true");
650
822
  button.setAttribute("aria-busy", "true");
651
- logo.replaceWith(buildSpinner());
823
+ const spinner = buildSpinner();
824
+ icon.replaceWith(spinner);
825
+ icon = spinner;
652
826
  } else {
653
827
  button.removeAttribute("aria-disabled");
654
828
  button.removeAttribute("aria-busy");
655
- const currentIndicator = button.querySelector(".maytes-checkout-button__spinner, .maytes-checkout-button__logo");
656
- if (currentIndicator !== null) currentIndicator.replaceWith(buildLogo());
657
- }
658
- };
659
- const teardownActiveCheckout = () => {
660
- hideOverlay(state);
661
- setBusy(false);
662
- };
663
- const onPopupClosed = () => {
664
- teardownActiveCheckout();
665
- document.dispatchEvent(new CustomEvent("maytes:checkout-closed"));
666
- };
667
- const dispatchFailed = (reason, cause) => {
668
- document.dispatchEvent(new CustomEvent("maytes:checkout-failed", {
669
- detail: cause === void 0 ? { reason } : { reason, cause }
670
- }));
671
- };
672
- const dispatchRedirected = (url, target) => {
673
- const detail = { url, target };
674
- document.dispatchEvent(new CustomEvent("maytes:checkout-redirected", { detail }));
675
- };
676
- const navigateAway = (url) => {
677
- const outcome = navigateTopLevel(url, false);
678
- teardownActiveCheckout();
679
- if (outcome.target === null) {
680
- console.error("[maytes/checkout-button] could not leave the embedding frame:", outcome.cause);
681
- dispatchFailed("navigation-blocked", outcome.cause);
682
- return;
829
+ const nextLogo = buildLogo();
830
+ icon.replaceWith(nextLogo);
831
+ icon = nextLogo;
683
832
  }
684
- dispatchRedirected(url, outcome.target);
685
833
  };
686
- const closeOrphanPopup = (popup) => {
687
- closePopupWindow(popup);
688
- stopPopupPoll(state);
689
- if (state.popupWindow === popup) state.popupWindow = null;
690
- teardownActiveCheckout();
834
+ let launchedHere = false;
835
+ const followOwnLaunch = (busy) => {
836
+ if (!launchedHere) return;
837
+ showBusy(busy);
838
+ if (!busy) launchedHere = false;
691
839
  };
692
- const handleClick = async () => {
840
+ state.busyViews.add(followOwnLaunch);
841
+ const handleClick = () => {
693
842
  if (state.busy || state.destroyed) return;
694
- setBusy(true);
695
- const shouldUsePopup = mode === "popup" && !isMobileViewport();
696
- const popup = shouldUsePopup ? openBlankPopup(state) : null;
697
- if (popup !== null) {
698
- state.popupWindow = popup;
699
- state.popupNavigated = false;
700
- refocusPopup(popup);
701
- paintPopupLoadingScreen(popup, state.config.cspNonce);
702
- showOverlay(state);
703
- document.dispatchEvent(new CustomEvent("maytes:checkout-opened"));
704
- startPopupPoll(state, onPopupClosed);
705
- }
706
- try {
707
- const result = await state.config.createCheckout();
708
- if (state.destroyed || popup !== null && popup.closed) {
709
- closePopupWindow(popup);
710
- return;
711
- }
712
- if (!isValidCheckoutResult(result)) {
713
- closeOrphanPopup(popup);
714
- const shapeError = new MaytesError(
715
- MaytesErrorCode.Config,
716
- "createCheckout must resolve to { checkoutId: string, checkoutUrl?: http(s) URL }"
717
- );
718
- console.error(shapeError);
719
- dispatchFailed("invalid-shape", shapeError);
720
- return;
721
- }
722
- const url = result.checkoutUrl ?? buildCheckoutUrl(
723
- state.config.environment,
724
- state.config.baseUrl,
725
- result.checkoutId
726
- );
727
- if (popup !== null) {
728
- state.popupNavigated = true;
729
- navigatePopup(popup, url);
730
- } else {
731
- if (shouldUsePopup) {
732
- console.warn("[maytes/checkout-button] popup was blocked; redirecting instead");
733
- }
734
- navigateAway(url);
735
- }
736
- } catch (err) {
737
- if (popup !== null && popup.closed) return;
738
- closeOrphanPopup(popup);
739
- console.error("[maytes/checkout-button] createCheckout failed:", err);
740
- dispatchFailed("create-checkout-rejected", err);
741
- }
843
+ launchedHere = true;
844
+ void launchCheckout(state, mode, "button", redirectOverlay);
742
845
  };
743
846
  button.addEventListener("click", handleClick);
744
847
  container.appendChild(button);
@@ -748,6 +851,7 @@ function renderButton(state, container, options = {}) {
748
851
  cleaned = true;
749
852
  button.removeEventListener("click", handleClick);
750
853
  button.remove();
854
+ state.busyViews.delete(followOwnLaunch);
751
855
  state.teardowns.delete(teardown);
752
856
  releaseStyles();
753
857
  };
@@ -756,18 +860,18 @@ function renderButton(state, container, options = {}) {
756
860
  }
757
861
 
758
862
  // src/state.ts
759
- function popupNameSuffix() {
863
+ function randomSuffix() {
760
864
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
761
865
  }
762
- function generatePopupName() {
866
+ function randomId() {
763
867
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
764
868
  try {
765
- return `maytes-checkout-${crypto.randomUUID()}`;
869
+ return crypto.randomUUID();
766
870
  } catch {
767
- return `maytes-checkout-${popupNameSuffix()}`;
871
+ return randomSuffix();
768
872
  }
769
873
  }
770
- return `maytes-checkout-${popupNameSuffix()}`;
874
+ return randomSuffix();
771
875
  }
772
876
  function createInstanceState(options, internal) {
773
877
  const config = {
@@ -779,11 +883,14 @@ function createInstanceState(options, internal) {
779
883
  return {
780
884
  config,
781
885
  busy: false,
886
+ busyViews: /* @__PURE__ */ new Set(),
887
+ instanceId: randomId(),
782
888
  overlayEl: null,
783
889
  overlayDetach: null,
890
+ restoreDetach: null,
784
891
  popupWindow: null,
785
892
  popupNavigated: false,
786
- popupName: generatePopupName(),
893
+ popupName: `maytes-checkout-${randomId()}`,
787
894
  popupPollHandle: null,
788
895
  destroyed: false,
789
896
  teardowns: /* @__PURE__ */ new Set()
@@ -821,9 +928,27 @@ var Maytes = (options, internal) => {
821
928
  state.config.baseUrl
822
929
  );
823
930
  const sdk = {
931
+ instanceId: state.instanceId,
824
932
  renderButton(container, opts) {
825
933
  return renderButton(state, container, opts);
826
934
  },
935
+ openCheckout(opts) {
936
+ if (state.destroyed) {
937
+ throw new MaytesError(MaytesErrorCode.Config, "openCheckout() called on a destroyed Maytes instance");
938
+ }
939
+ const mode = assertLaunchMode(opts?.mode ?? "popup", "openCheckout");
940
+ const redirectOverlay = assertRedirectOverlay(opts?.redirectOverlay, "openCheckout");
941
+ return launchCheckout(state, mode, "api", redirectOverlay);
942
+ },
943
+ onBusyChange(listener) {
944
+ if (state.destroyed) {
945
+ throw new MaytesError(MaytesErrorCode.Config, "onBusyChange() called on a destroyed Maytes instance");
946
+ }
947
+ state.busyViews.add(listener);
948
+ return () => {
949
+ state.busyViews.delete(listener);
950
+ };
951
+ },
827
952
  redirectToCheckout(opts) {
828
953
  redirectToCheckout(opts);
829
954
  },
@@ -834,6 +959,7 @@ var Maytes = (options, internal) => {
834
959
  if (state.destroyed) return;
835
960
  state.destroyed = true;
836
961
  stopPopupPoll(state);
962
+ state.restoreDetach?.();
837
963
  hideOverlay(state);
838
964
  if (!state.popupNavigated) closePopupWindow(state.popupWindow);
839
965
  state.popupWindow = null;