@maytes/checkout-button 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +93 -74
  3. package/dist/checkout-button.299a037b.js +188 -0
  4. package/dist/checkout-button.299a037b.js.map +1 -0
  5. package/dist/{checkout-button.2aad12c3.cjs → checkout-button.332d26cc.cjs} +314 -156
  6. package/dist/checkout-button.332d26cc.cjs.map +1 -0
  7. package/dist/{checkout-button.aed57a66.mjs → checkout-button.67fdb2e1.mjs} +314 -156
  8. package/dist/checkout-button.67fdb2e1.mjs.map +1 -0
  9. package/dist/checkout-button.cjs +314 -156
  10. package/dist/checkout-button.cjs.map +1 -1
  11. package/dist/checkout-button.d.cts +40 -4
  12. package/dist/checkout-button.d.ts +40 -4
  13. package/dist/checkout-button.element.afe71cd5.js +187 -0
  14. package/dist/checkout-button.element.afe71cd5.js.map +1 -0
  15. package/dist/checkout-button.element.js +187 -0
  16. package/dist/checkout-button.element.js.map +1 -0
  17. package/dist/checkout-button.js +58 -21
  18. package/dist/checkout-button.js.map +1 -1
  19. package/dist/checkout-button.mjs +314 -156
  20. package/dist/checkout-button.mjs.map +1 -1
  21. package/dist/element.cjs +201 -0
  22. package/dist/element.cjs.map +1 -0
  23. package/dist/element.d.cts +44 -0
  24. package/dist/element.d.ts +44 -0
  25. package/dist/element.mjs +180 -0
  26. package/dist/element.mjs.map +1 -0
  27. package/dist/integrity.json +37 -26
  28. package/dist/react.cjs +139 -0
  29. package/dist/react.cjs.map +1 -0
  30. package/dist/react.d.cts +30 -0
  31. package/dist/react.d.ts +30 -0
  32. package/dist/react.mjs +117 -0
  33. package/dist/react.mjs.map +1 -0
  34. package/docs/guides/angular.md +56 -0
  35. package/docs/guides/api.md +108 -0
  36. package/docs/guides/events.md +48 -0
  37. package/docs/guides/launch-behaviour.md +41 -0
  38. package/docs/guides/nextjs.md +49 -0
  39. package/docs/guides/own-button.md +41 -0
  40. package/docs/guides/quickstart-html.md +31 -0
  41. package/docs/guides/quickstart-npm.md +34 -0
  42. package/docs/guides/react.md +88 -0
  43. package/docs/guides/security-csp.md +32 -0
  44. package/docs/guides/server.md +76 -0
  45. package/docs/guides/solid.md +44 -0
  46. package/docs/guides/stripe-payment-element.md +14 -0
  47. package/docs/guides/svelte.md +46 -0
  48. package/docs/guides/troubleshooting.md +17 -0
  49. package/docs/guides/vue-nuxt.md +64 -0
  50. package/docs/guides/web-component.md +109 -0
  51. package/package.json +40 -3
  52. package/dist/checkout-button.2aad12c3.cjs.map +0 -1
  53. package/dist/checkout-button.4b780d52.js +0 -151
  54. package/dist/checkout-button.4b780d52.js.map +0 -1
  55. package/dist/checkout-button.aed57a66.mjs.map +0 -1
@@ -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.1";
320
361
 
321
362
  // src/styles.ts
322
363
  var STYLE_ATTR = "data-maytes-checkout-button";
@@ -329,28 +370,51 @@ 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
- border: none;
334
- border-radius: 999px;
335
- background: ${foundation.brand.primary};
336
- color: ${foundation.text.primaryInverse};
375
+ border-radius: var(--maytes-button-radius, 999px);
337
376
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
338
377
  font-size: var(--maytes-button-font-size, 14px);
339
378
  font-weight: 600;
340
379
  line-height: 1;
341
- cursor: pointer;
342
380
  user-select: none;
381
+ }
382
+ .maytes-checkout-button.maytes-checkout-button,
383
+ .maytes-checkout-button.maytes-checkout-button:hover,
384
+ .maytes-checkout-button.maytes-checkout-button:focus,
385
+ .maytes-checkout-button.maytes-checkout-button:active {
386
+ background: ${foundation.brand.primary};
387
+ border: none;
388
+ color: ${foundation.text.primaryInverse};
389
+ box-shadow: none;
390
+ text-decoration: none;
391
+ text-shadow: none;
392
+ filter: none;
393
+ transform: none;
394
+ opacity: 1;
395
+ cursor: pointer;
343
396
  transition: background-color 120ms ease, opacity 120ms ease;
344
397
  }
345
- .maytes-checkout-button:hover { background: ${foundation.palette.burgundy[700]}; }
346
- .maytes-checkout-button:focus-visible {
398
+ .maytes-checkout-button.maytes-checkout-button:hover { background: ${foundation.palette.burgundy[700]}; }
399
+ .maytes-checkout-button.maytes-checkout-button:focus:not(:focus-visible) { outline: none; }
400
+ .maytes-checkout-button.maytes-checkout-button:focus-visible {
347
401
  outline: 2px solid ${FOCUS_RING};
348
402
  outline-offset: 2px;
349
403
  }
350
- .maytes-checkout-button[aria-disabled='true'] {
404
+ .maytes-checkout-button.maytes-checkout-button[aria-disabled='true'] {
351
405
  cursor: progress;
352
406
  opacity: 0.7;
353
407
  }
408
+ .maytes-checkout-button.maytes-checkout-button * { transition: none; }
409
+ .maytes-checkout-button.maytes-checkout-button .maytes-checkout-button__logo,
410
+ .maytes-checkout-button.maytes-checkout-button .maytes-checkout-button__logo * {
411
+ animation: none;
412
+ fill: currentColor;
413
+ }
414
+ .maytes-checkout-button--sized {
415
+ box-sizing: border-box;
416
+ white-space: nowrap;
417
+ }
354
418
  .maytes-checkout-button--block {
355
419
  display: flex;
356
420
  width: 100%;
@@ -412,17 +476,31 @@ var BUTTON_CSS = `
412
476
  margin: 0;
413
477
  }
414
478
  .maytes-checkout-overlay__link {
415
- color: rgba(255, 255, 255, 0.7);
416
479
  font-size: 0.875rem;
417
- text-decoration: underline;
418
480
  text-underline-offset: 4px;
481
+ font-family: inherit;
482
+ }
483
+ .maytes-checkout-overlay__link.maytes-checkout-overlay__link,
484
+ .maytes-checkout-overlay__link.maytes-checkout-overlay__link:hover,
485
+ .maytes-checkout-overlay__link.maytes-checkout-overlay__link:focus,
486
+ .maytes-checkout-overlay__link.maytes-checkout-overlay__link:active {
419
487
  background: none;
420
488
  border: none;
489
+ color: rgba(255, 255, 255, 0.7);
490
+ box-shadow: none;
491
+ text-decoration: underline;
492
+ text-shadow: none;
493
+ transform: none;
494
+ opacity: 1;
421
495
  cursor: pointer;
422
- font-family: inherit;
423
496
  transition: color 0.15s;
424
497
  }
425
- .maytes-checkout-overlay__link:hover { color: ${foundation.text.primaryInverse}; }
498
+ .maytes-checkout-overlay__link.maytes-checkout-overlay__link:hover { color: ${foundation.text.primaryInverse}; }
499
+ .maytes-checkout-overlay__link.maytes-checkout-overlay__link:focus:not(:focus-visible) { outline: none; }
500
+ .maytes-checkout-overlay__link.maytes-checkout-overlay__link:focus-visible {
501
+ outline: 2px solid ${FOCUS_RING};
502
+ outline-offset: 2px;
503
+ }
426
504
  @media (prefers-reduced-motion: reduce) {
427
505
  .maytes-checkout-overlay__spinner,
428
506
  .maytes-checkout-button__spinner { animation: none; }
@@ -499,24 +577,12 @@ function releaseStyles() {
499
577
  if (el !== null) el.remove();
500
578
  }
501
579
 
502
- // src/button.ts
580
+ // src/launch.ts
503
581
  var POPUP_WIDTH = 500;
504
582
  var POPUP_HEIGHT = 800;
505
583
  var POPUP_POLL_INTERVAL_MS = 500;
506
584
  var MOBILE_MAX_WIDTH = 600;
507
585
  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
586
  function isValidCheckoutResult(value) {
521
587
  if (typeof value !== "object" || value === null) return false;
522
588
  const obj = value;
@@ -615,6 +681,148 @@ function paintPopupLoadingScreen(popup, cspNonce) {
615
681
  slow.hidden = false;
616
682
  }, SLOW_NETWORK_DELAY_MS);
617
683
  }
684
+ function eventDetail(state, source) {
685
+ return { instanceId: state.instanceId, source };
686
+ }
687
+ function dispatchFailed(origin, reason, cause) {
688
+ const detail = cause === void 0 ? { ...origin, reason } : { ...origin, reason, cause };
689
+ document.dispatchEvent(new CustomEvent("maytes:checkout-failed", { detail }));
690
+ }
691
+ function dispatchRedirected(origin, url, target) {
692
+ const detail = { ...origin, url, target };
693
+ document.dispatchEvent(new CustomEvent("maytes:checkout-redirected", { detail }));
694
+ }
695
+ function assertLaunchMode(mode, caller = "renderButton") {
696
+ if (mode !== "redirect" && mode !== "popup") {
697
+ throw new MaytesError(MaytesErrorCode.Config, `${caller}({ mode }) must be 'redirect' or 'popup'`);
698
+ }
699
+ return mode;
700
+ }
701
+ function assertRedirectOverlay(value, caller = "renderButton") {
702
+ if (value === void 0) return false;
703
+ if (typeof value !== "boolean") {
704
+ throw new MaytesError(MaytesErrorCode.Config, `${caller}({ redirectOverlay }) must be true or false`);
705
+ }
706
+ return value;
707
+ }
708
+ function setInstanceBusy(state, busy) {
709
+ state.busy = busy;
710
+ for (const view of [...state.busyViews]) view(busy);
711
+ }
712
+ function endLaunch(state) {
713
+ hideOverlay(state);
714
+ setInstanceBusy(state, false);
715
+ }
716
+ function closeOrphanPopup(state, popup) {
717
+ closePopupWindow(popup);
718
+ stopPopupPoll(state);
719
+ if (state.popupWindow === popup) state.popupWindow = null;
720
+ endLaunch(state);
721
+ }
722
+ function clearOverlayOnRestore(state) {
723
+ if (state.restoreDetach !== null) return;
724
+ const onPageShow = (event) => {
725
+ if (!event.persisted) return;
726
+ detach();
727
+ endLaunch(state);
728
+ };
729
+ const detach = () => {
730
+ window.removeEventListener("pageshow", onPageShow);
731
+ state.restoreDetach = null;
732
+ };
733
+ window.addEventListener("pageshow", onPageShow);
734
+ state.restoreDetach = detach;
735
+ }
736
+ function navigateAway(state, origin, url, keepOverlay) {
737
+ const navigation = navigateTopLevel(url, false);
738
+ if (keepOverlay && (navigation.target === "self" || navigation.target === "top")) {
739
+ setInstanceBusy(state, false);
740
+ clearOverlayOnRestore(state);
741
+ } else {
742
+ endLaunch(state);
743
+ }
744
+ if (navigation.target === null) {
745
+ console.error("[maytes/checkout-button] could not leave the embedding frame:", navigation.cause);
746
+ dispatchFailed(origin, "navigation-blocked", navigation.cause);
747
+ return { outcome: "failed", reason: "navigation-blocked" };
748
+ }
749
+ dispatchRedirected(origin, url, navigation.target);
750
+ return { outcome: "redirected", target: navigation.target };
751
+ }
752
+ function openPopupLoader(state, origin) {
753
+ const popup = openBlankPopup(state);
754
+ if (popup === null) return null;
755
+ state.popupWindow = popup;
756
+ state.popupNavigated = false;
757
+ refocusPopup(popup);
758
+ paintPopupLoadingScreen(popup, state.config.cspNonce);
759
+ showOverlay(state);
760
+ document.dispatchEvent(new CustomEvent("maytes:checkout-opened", { detail: origin }));
761
+ startPopupPoll(state, () => {
762
+ endLaunch(state);
763
+ document.dispatchEvent(new CustomEvent("maytes:checkout-closed", { detail: origin }));
764
+ });
765
+ return popup;
766
+ }
767
+ async function launchCheckout(state, mode, source, redirectOverlay = false) {
768
+ if (state.busy || state.destroyed) return { outcome: "ignored" };
769
+ setInstanceBusy(state, true);
770
+ const origin = eventDetail(state, source);
771
+ const shouldUsePopup = mode === "popup" && !isMobileViewport();
772
+ const popup = shouldUsePopup ? openPopupLoader(state, origin) : null;
773
+ if (popup === null && redirectOverlay) showOverlay(state, "redirect");
774
+ try {
775
+ const result = await state.config.createCheckout();
776
+ if (state.destroyed || popup !== null && popup.closed) {
777
+ closePopupWindow(popup);
778
+ return { outcome: "closed" };
779
+ }
780
+ if (!isValidCheckoutResult(result)) {
781
+ closeOrphanPopup(state, popup);
782
+ const shapeError = new MaytesError(
783
+ MaytesErrorCode.Config,
784
+ "createCheckout must resolve to { checkoutId: string, checkoutUrl?: http(s) URL }"
785
+ );
786
+ console.error(shapeError);
787
+ dispatchFailed(origin, "invalid-shape", shapeError);
788
+ return { outcome: "failed", reason: "invalid-shape" };
789
+ }
790
+ const url = result.checkoutUrl ?? buildCheckoutUrl(
791
+ state.config.environment,
792
+ state.config.baseUrl,
793
+ result.checkoutId
794
+ );
795
+ if (popup !== null) {
796
+ state.popupNavigated = true;
797
+ navigatePopup(popup, url);
798
+ return { outcome: "popup" };
799
+ }
800
+ if (shouldUsePopup) {
801
+ console.warn("[maytes/checkout-button] popup was blocked; redirecting instead");
802
+ }
803
+ return navigateAway(state, origin, url, redirectOverlay);
804
+ } catch (err) {
805
+ if (popup !== null && popup.closed) return { outcome: "closed" };
806
+ closeOrphanPopup(state, popup);
807
+ console.error("[maytes/checkout-button] createCheckout failed:", err);
808
+ dispatchFailed(origin, "create-checkout-rejected", err);
809
+ return { outcome: "failed", reason: "create-checkout-rejected" };
810
+ }
811
+ }
812
+
813
+ // src/button.ts
814
+ function buildLogo() {
815
+ const svg = buildMaytesLogo("1em", "currentColor");
816
+ svg.setAttribute("class", "maytes-checkout-button__logo");
817
+ svg.setAttribute("aria-hidden", "true");
818
+ return svg;
819
+ }
820
+ function buildSpinner() {
821
+ const spinner = document.createElement("span");
822
+ spinner.className = "maytes-checkout-button__spinner";
823
+ spinner.setAttribute("aria-hidden", "true");
824
+ return spinner;
825
+ }
618
826
  function renderButton(state, container, options = {}) {
619
827
  if (state.destroyed) {
620
828
  throw new MaytesError(
@@ -625,120 +833,47 @@ function renderButton(state, container, options = {}) {
625
833
  if (!(container instanceof HTMLElement)) {
626
834
  throw new MaytesError(MaytesErrorCode.Config, "renderButton(container) requires an HTMLElement");
627
835
  }
836
+ const appearance = assertAppearance(options);
837
+ const redirectOverlay = assertRedirectOverlay(options.redirectOverlay);
628
838
  ensureStylesInjected(state.config.cspNonce);
629
839
  const label = options.label ?? "Split with";
630
840
  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
- }
841
+ const mode = assertLaunchMode(options.mode ?? "popup");
638
842
  const button = document.createElement("button");
639
843
  button.type = "button";
640
844
  button.className = block ? "maytes-checkout-button maytes-checkout-button--block" : "maytes-checkout-button";
845
+ applyAppearance(button, appearance);
641
846
  button.setAttribute("aria-label", `${label} Maytes`);
642
847
  const labelNode = document.createTextNode(`${label} `);
643
- const logo = buildLogo();
848
+ let icon = buildLogo();
644
849
  button.appendChild(labelNode);
645
- button.appendChild(logo);
646
- const setBusy = (busy) => {
647
- state.busy = busy;
850
+ button.appendChild(icon);
851
+ const showBusy = (busy) => {
648
852
  if (busy) {
649
853
  button.setAttribute("aria-disabled", "true");
650
854
  button.setAttribute("aria-busy", "true");
651
- logo.replaceWith(buildSpinner());
855
+ const spinner = buildSpinner();
856
+ icon.replaceWith(spinner);
857
+ icon = spinner;
652
858
  } else {
653
859
  button.removeAttribute("aria-disabled");
654
860
  button.removeAttribute("aria-busy");
655
- const currentIndicator = button.querySelector(".maytes-checkout-button__spinner, .maytes-checkout-button__logo");
656
- if (currentIndicator !== null) currentIndicator.replaceWith(buildLogo());
861
+ const nextLogo = buildLogo();
862
+ icon.replaceWith(nextLogo);
863
+ icon = nextLogo;
657
864
  }
658
865
  };
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 }));
866
+ let launchedHere = false;
867
+ const followOwnLaunch = (busy) => {
868
+ if (!launchedHere) return;
869
+ showBusy(busy);
870
+ if (!busy) launchedHere = false;
675
871
  };
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;
683
- }
684
- dispatchRedirected(url, outcome.target);
685
- };
686
- const closeOrphanPopup = (popup) => {
687
- closePopupWindow(popup);
688
- stopPopupPoll(state);
689
- if (state.popupWindow === popup) state.popupWindow = null;
690
- teardownActiveCheckout();
691
- };
692
- const handleClick = async () => {
872
+ state.busyViews.add(followOwnLaunch);
873
+ const handleClick = () => {
693
874
  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
- }
875
+ launchedHere = true;
876
+ void launchCheckout(state, mode, "button", redirectOverlay);
742
877
  };
743
878
  button.addEventListener("click", handleClick);
744
879
  container.appendChild(button);
@@ -748,6 +883,7 @@ function renderButton(state, container, options = {}) {
748
883
  cleaned = true;
749
884
  button.removeEventListener("click", handleClick);
750
885
  button.remove();
886
+ state.busyViews.delete(followOwnLaunch);
751
887
  state.teardowns.delete(teardown);
752
888
  releaseStyles();
753
889
  };
@@ -756,18 +892,18 @@ function renderButton(state, container, options = {}) {
756
892
  }
757
893
 
758
894
  // src/state.ts
759
- function popupNameSuffix() {
895
+ function randomSuffix() {
760
896
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
761
897
  }
762
- function generatePopupName() {
898
+ function randomId() {
763
899
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
764
900
  try {
765
- return `maytes-checkout-${crypto.randomUUID()}`;
901
+ return crypto.randomUUID();
766
902
  } catch {
767
- return `maytes-checkout-${popupNameSuffix()}`;
903
+ return randomSuffix();
768
904
  }
769
905
  }
770
- return `maytes-checkout-${popupNameSuffix()}`;
906
+ return randomSuffix();
771
907
  }
772
908
  function createInstanceState(options, internal) {
773
909
  const config = {
@@ -779,11 +915,14 @@ function createInstanceState(options, internal) {
779
915
  return {
780
916
  config,
781
917
  busy: false,
918
+ busyViews: /* @__PURE__ */ new Set(),
919
+ instanceId: randomId(),
782
920
  overlayEl: null,
783
921
  overlayDetach: null,
922
+ restoreDetach: null,
784
923
  popupWindow: null,
785
924
  popupNavigated: false,
786
- popupName: generatePopupName(),
925
+ popupName: `maytes-checkout-${randomId()}`,
787
926
  popupPollHandle: null,
788
927
  destroyed: false,
789
928
  teardowns: /* @__PURE__ */ new Set()
@@ -821,9 +960,27 @@ var Maytes = (options, internal) => {
821
960
  state.config.baseUrl
822
961
  );
823
962
  const sdk = {
963
+ instanceId: state.instanceId,
824
964
  renderButton(container, opts) {
825
965
  return renderButton(state, container, opts);
826
966
  },
967
+ openCheckout(opts) {
968
+ if (state.destroyed) {
969
+ throw new MaytesError(MaytesErrorCode.Config, "openCheckout() called on a destroyed Maytes instance");
970
+ }
971
+ const mode = assertLaunchMode(opts?.mode ?? "popup", "openCheckout");
972
+ const redirectOverlay = assertRedirectOverlay(opts?.redirectOverlay, "openCheckout");
973
+ return launchCheckout(state, mode, "api", redirectOverlay);
974
+ },
975
+ onBusyChange(listener) {
976
+ if (state.destroyed) {
977
+ throw new MaytesError(MaytesErrorCode.Config, "onBusyChange() called on a destroyed Maytes instance");
978
+ }
979
+ state.busyViews.add(listener);
980
+ return () => {
981
+ state.busyViews.delete(listener);
982
+ };
983
+ },
827
984
  redirectToCheckout(opts) {
828
985
  redirectToCheckout(opts);
829
986
  },
@@ -834,6 +991,7 @@ var Maytes = (options, internal) => {
834
991
  if (state.destroyed) return;
835
992
  state.destroyed = true;
836
993
  stopPopupPoll(state);
994
+ state.restoreDetach?.();
837
995
  hideOverlay(state);
838
996
  if (!state.popupNavigated) closePopupWindow(state.popupWindow);
839
997
  state.popupWindow = null;