@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
@@ -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,13 +234,14 @@ 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);
244
+ if (host !== document) detachOverlayWhenThisPageHides(state);
203
245
  if (typeof dialog.showModal === "function") {
204
246
  try {
205
247
  dialog.showModal();
@@ -213,8 +255,15 @@ function showOverlay(state) {
213
255
  applyNativeBackdropFallback(dialog);
214
256
  }
215
257
  }
258
+ function detachOverlayWhenThisPageHides(state) {
259
+ const onPageHide = () => hideOverlay(state);
260
+ window.addEventListener("pagehide", onPageHide);
261
+ state.overlayDetach = () => window.removeEventListener("pagehide", onPageHide);
262
+ }
216
263
  function hideOverlay(state) {
217
264
  if (state.overlayEl === null) return;
265
+ state.overlayDetach?.();
266
+ state.overlayDetach = null;
218
267
  const dialog = state.overlayEl;
219
268
  const host = dialog.ownerDocument;
220
269
  if (typeof dialog.close === "function" && dialog.open) dialog.close();
@@ -308,7 +357,7 @@ function makeRedirect(environment, baseUrlOverride) {
308
357
  }
309
358
 
310
359
  // src/version.ts
311
- var SDK_VERSION = "1.1.0";
360
+ var SDK_VERSION = "1.2.0";
312
361
 
313
362
  // src/styles.ts
314
363
  var STYLE_ATTR = "data-maytes-checkout-button";
@@ -321,9 +370,10 @@ var BUTTON_CSS = `
321
370
  align-items: center;
322
371
  justify-content: center;
323
372
  gap: 0.57em;
373
+ min-height: var(--maytes-button-height, auto);
324
374
  padding: 0.71em 1.29em;
325
375
  border: none;
326
- border-radius: 999px;
376
+ border-radius: var(--maytes-button-radius, 999px);
327
377
  background: ${foundation.brand.primary};
328
378
  color: ${foundation.text.primaryInverse};
329
379
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
@@ -343,6 +393,10 @@ var BUTTON_CSS = `
343
393
  cursor: progress;
344
394
  opacity: 0.7;
345
395
  }
396
+ .maytes-checkout-button--sized {
397
+ box-sizing: border-box;
398
+ white-space: nowrap;
399
+ }
346
400
  .maytes-checkout-button--block {
347
401
  display: flex;
348
402
  width: 100%;
@@ -491,24 +545,12 @@ function releaseStyles() {
491
545
  if (el !== null) el.remove();
492
546
  }
493
547
 
494
- // src/button.ts
548
+ // src/launch.ts
495
549
  var POPUP_WIDTH = 500;
496
550
  var POPUP_HEIGHT = 800;
497
551
  var POPUP_POLL_INTERVAL_MS = 500;
498
552
  var MOBILE_MAX_WIDTH = 600;
499
553
  var SLOW_NETWORK_DELAY_MS = 8e3;
500
- function buildLogo() {
501
- const svg = buildMaytesLogo("1em", "currentColor");
502
- svg.setAttribute("class", "maytes-checkout-button__logo");
503
- svg.setAttribute("aria-hidden", "true");
504
- return svg;
505
- }
506
- function buildSpinner() {
507
- const spinner = document.createElement("span");
508
- spinner.className = "maytes-checkout-button__spinner";
509
- spinner.setAttribute("aria-hidden", "true");
510
- return spinner;
511
- }
512
554
  function isValidCheckoutResult(value) {
513
555
  if (typeof value !== "object" || value === null) return false;
514
556
  const obj = value;
@@ -607,6 +649,148 @@ function paintPopupLoadingScreen(popup, cspNonce) {
607
649
  slow.hidden = false;
608
650
  }, SLOW_NETWORK_DELAY_MS);
609
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
+ }
610
794
  function renderButton(state, container, options = {}) {
611
795
  if (state.destroyed) {
612
796
  throw new MaytesError(
@@ -617,120 +801,47 @@ function renderButton(state, container, options = {}) {
617
801
  if (!(container instanceof HTMLElement)) {
618
802
  throw new MaytesError(MaytesErrorCode.Config, "renderButton(container) requires an HTMLElement");
619
803
  }
804
+ const appearance = assertAppearance(options);
805
+ const redirectOverlay = assertRedirectOverlay(options.redirectOverlay);
620
806
  ensureStylesInjected(state.config.cspNonce);
621
807
  const label = options.label ?? "Split with";
622
808
  const block = options.block === true;
623
- const mode = options.mode ?? "popup";
624
- if (mode !== "redirect" && mode !== "popup") {
625
- throw new MaytesError(
626
- MaytesErrorCode.Config,
627
- "renderButton({ mode }) must be 'redirect' or 'popup'"
628
- );
629
- }
809
+ const mode = assertLaunchMode(options.mode ?? "popup");
630
810
  const button = document.createElement("button");
631
811
  button.type = "button";
632
812
  button.className = block ? "maytes-checkout-button maytes-checkout-button--block" : "maytes-checkout-button";
813
+ applyAppearance(button, appearance);
633
814
  button.setAttribute("aria-label", `${label} Maytes`);
634
815
  const labelNode = document.createTextNode(`${label} `);
635
- const logo = buildLogo();
816
+ let icon = buildLogo();
636
817
  button.appendChild(labelNode);
637
- button.appendChild(logo);
638
- const setBusy = (busy) => {
639
- state.busy = busy;
818
+ button.appendChild(icon);
819
+ const showBusy = (busy) => {
640
820
  if (busy) {
641
821
  button.setAttribute("aria-disabled", "true");
642
822
  button.setAttribute("aria-busy", "true");
643
- logo.replaceWith(buildSpinner());
823
+ const spinner = buildSpinner();
824
+ icon.replaceWith(spinner);
825
+ icon = spinner;
644
826
  } else {
645
827
  button.removeAttribute("aria-disabled");
646
828
  button.removeAttribute("aria-busy");
647
- const currentIndicator = button.querySelector(".maytes-checkout-button__spinner, .maytes-checkout-button__logo");
648
- if (currentIndicator !== null) currentIndicator.replaceWith(buildLogo());
649
- }
650
- };
651
- const teardownActiveCheckout = () => {
652
- hideOverlay(state);
653
- setBusy(false);
654
- };
655
- const onPopupClosed = () => {
656
- teardownActiveCheckout();
657
- document.dispatchEvent(new CustomEvent("maytes:checkout-closed"));
658
- };
659
- const dispatchFailed = (reason, cause) => {
660
- document.dispatchEvent(new CustomEvent("maytes:checkout-failed", {
661
- detail: cause === void 0 ? { reason } : { reason, cause }
662
- }));
663
- };
664
- const dispatchRedirected = (url, target) => {
665
- const detail = { url, target };
666
- document.dispatchEvent(new CustomEvent("maytes:checkout-redirected", { detail }));
667
- };
668
- const navigateAway = (url) => {
669
- const outcome = navigateTopLevel(url, false);
670
- teardownActiveCheckout();
671
- if (outcome.target === null) {
672
- console.error("[maytes/checkout-button] could not leave the embedding frame:", outcome.cause);
673
- dispatchFailed("navigation-blocked", outcome.cause);
674
- return;
829
+ const nextLogo = buildLogo();
830
+ icon.replaceWith(nextLogo);
831
+ icon = nextLogo;
675
832
  }
676
- dispatchRedirected(url, outcome.target);
677
833
  };
678
- const closeOrphanPopup = (popup) => {
679
- closePopupWindow(popup);
680
- stopPopupPoll(state);
681
- if (state.popupWindow === popup) state.popupWindow = null;
682
- teardownActiveCheckout();
834
+ let launchedHere = false;
835
+ const followOwnLaunch = (busy) => {
836
+ if (!launchedHere) return;
837
+ showBusy(busy);
838
+ if (!busy) launchedHere = false;
683
839
  };
684
- const handleClick = async () => {
840
+ state.busyViews.add(followOwnLaunch);
841
+ const handleClick = () => {
685
842
  if (state.busy || state.destroyed) return;
686
- setBusy(true);
687
- const shouldUsePopup = mode === "popup" && !isMobileViewport();
688
- const popup = shouldUsePopup ? openBlankPopup(state) : null;
689
- if (popup !== null) {
690
- state.popupWindow = popup;
691
- state.popupNavigated = false;
692
- refocusPopup(popup);
693
- paintPopupLoadingScreen(popup, state.config.cspNonce);
694
- showOverlay(state);
695
- document.dispatchEvent(new CustomEvent("maytes:checkout-opened"));
696
- startPopupPoll(state, onPopupClosed);
697
- }
698
- try {
699
- const result = await state.config.createCheckout();
700
- if (state.destroyed || popup !== null && popup.closed) {
701
- closePopupWindow(popup);
702
- return;
703
- }
704
- if (!isValidCheckoutResult(result)) {
705
- closeOrphanPopup(popup);
706
- const shapeError = new MaytesError(
707
- MaytesErrorCode.Config,
708
- "createCheckout must resolve to { checkoutId: string, checkoutUrl?: http(s) URL }"
709
- );
710
- console.error(shapeError);
711
- dispatchFailed("invalid-shape", shapeError);
712
- return;
713
- }
714
- const url = result.checkoutUrl ?? buildCheckoutUrl(
715
- state.config.environment,
716
- state.config.baseUrl,
717
- result.checkoutId
718
- );
719
- if (popup !== null) {
720
- state.popupNavigated = true;
721
- navigatePopup(popup, url);
722
- } else {
723
- if (shouldUsePopup) {
724
- console.warn("[maytes/checkout-button] popup was blocked; redirecting instead");
725
- }
726
- navigateAway(url);
727
- }
728
- } catch (err) {
729
- if (popup !== null && popup.closed) return;
730
- closeOrphanPopup(popup);
731
- console.error("[maytes/checkout-button] createCheckout failed:", err);
732
- dispatchFailed("create-checkout-rejected", err);
733
- }
843
+ launchedHere = true;
844
+ void launchCheckout(state, mode, "button", redirectOverlay);
734
845
  };
735
846
  button.addEventListener("click", handleClick);
736
847
  container.appendChild(button);
@@ -740,6 +851,7 @@ function renderButton(state, container, options = {}) {
740
851
  cleaned = true;
741
852
  button.removeEventListener("click", handleClick);
742
853
  button.remove();
854
+ state.busyViews.delete(followOwnLaunch);
743
855
  state.teardowns.delete(teardown);
744
856
  releaseStyles();
745
857
  };
@@ -748,18 +860,18 @@ function renderButton(state, container, options = {}) {
748
860
  }
749
861
 
750
862
  // src/state.ts
751
- function popupNameSuffix() {
863
+ function randomSuffix() {
752
864
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
753
865
  }
754
- function generatePopupName() {
866
+ function randomId() {
755
867
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
756
868
  try {
757
- return `maytes-checkout-${crypto.randomUUID()}`;
869
+ return crypto.randomUUID();
758
870
  } catch {
759
- return `maytes-checkout-${popupNameSuffix()}`;
871
+ return randomSuffix();
760
872
  }
761
873
  }
762
- return `maytes-checkout-${popupNameSuffix()}`;
874
+ return randomSuffix();
763
875
  }
764
876
  function createInstanceState(options, internal) {
765
877
  const config = {
@@ -771,10 +883,14 @@ function createInstanceState(options, internal) {
771
883
  return {
772
884
  config,
773
885
  busy: false,
886
+ busyViews: /* @__PURE__ */ new Set(),
887
+ instanceId: randomId(),
774
888
  overlayEl: null,
889
+ overlayDetach: null,
890
+ restoreDetach: null,
775
891
  popupWindow: null,
776
892
  popupNavigated: false,
777
- popupName: generatePopupName(),
893
+ popupName: `maytes-checkout-${randomId()}`,
778
894
  popupPollHandle: null,
779
895
  destroyed: false,
780
896
  teardowns: /* @__PURE__ */ new Set()
@@ -812,9 +928,27 @@ var Maytes = (options, internal) => {
812
928
  state.config.baseUrl
813
929
  );
814
930
  const sdk = {
931
+ instanceId: state.instanceId,
815
932
  renderButton(container, opts) {
816
933
  return renderButton(state, container, opts);
817
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
+ },
818
952
  redirectToCheckout(opts) {
819
953
  redirectToCheckout(opts);
820
954
  },
@@ -825,6 +959,7 @@ var Maytes = (options, internal) => {
825
959
  if (state.destroyed) return;
826
960
  state.destroyed = true;
827
961
  stopPopupPoll(state);
962
+ state.restoreDetach?.();
828
963
  hideOverlay(state);
829
964
  if (!state.popupNavigated) closePopupWindow(state.popupWindow);
830
965
  state.popupWindow = null;