@benji-money/connect-sdk 1.1.0-beta.2 → 1.1.0-beta.3

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.
package/README.md CHANGED
@@ -86,6 +86,8 @@ The SDK accepts the following configuration options:
86
86
 
87
87
  - `environment` (required): One of `local`, `development`, `sandbox`, or `production`
88
88
  - `token` (required): Your API connect token
89
+ - `mode` (optional): `BenjiConnectMode` — `CONNECT` (default), `TRANSFER`, `REDEEM`, or `LOGIN`
90
+ - `authType` (optional): `PartnerIntegrationType` — LOGIN only; omit or set `DUMMY` for the visible modal iframe; set to your partner type (1–6) for popup-only auth
89
91
  - `onSuccess` (optional): Callback function called when connect completed successfully
90
92
  - `onError` (optional): Callback function called when error occurs in the connect flow
91
93
  - `onExit` (optional): Callback function called when the user exits the connect flow
@@ -94,6 +96,14 @@ The SDK accepts the following configuration options:
94
96
  - `placement` (optional): Placement relative to `anchor` — see [Placement](#placement) below
95
97
  - `offset` (optional): Pixel gap after placement — `{ top?: number; left?: number }` (default `{ top: 0, left: 0 }`)
96
98
 
99
+ ### Modes
100
+
101
+ | Mode | Visible UX | Notes |
102
+ | ---- | ---------- | ----- |
103
+ | **CONNECT** (default) | Centered or anchored modal iframe | No `authType`; standard connect / verify flow |
104
+ | **LOGIN + DUMMY / no `authType`** | Same visible modal iframe | Partner auth runs in a nested iframe inside Benji Connect |
105
+ | **LOGIN + non-DUMMY** | Partner popup only | Pass `authType`; Benji Connect iframe is headless — see [LOGIN popup example](#login-popup-example) below |
106
+
97
107
  ### Placement
98
108
 
99
109
  Placement uses [Floating UI](https://floating-ui.com/docs/computePosition#placement) / CSS logical naming (`start` / `end`). In **LTR** layouts:
@@ -104,6 +114,11 @@ Placement uses [Floating UI](https://floating-ui.com/docs/computePosition#placem
104
114
  | `bottom-end` | Below the anchor, right edge aligned with anchor's right edge |
105
115
  | `bottom-start` | Below the anchor, left edge aligned with anchor's left edge |
106
116
 
117
+ **What `anchor` positions:**
118
+
119
+ - **CONNECT, TRANSFER, REDEEM, and LOGIN + DUMMY:** the modal iframe
120
+ - **LOGIN + non-DUMMY:** the partner-auth popup window (modal iframe stays headless)
121
+
107
122
  **Defaults:**
108
123
 
109
124
  - No `anchor` → centered modal (unchanged behavior for existing integrations)
@@ -129,30 +144,35 @@ Prefer selector, ref, or getter when the anchor can re-render (e.g. disabled/loa
129
144
  - **Anchored** popovers use the full design size when width and height checks pass; otherwise they fall back to a scaled centered modal
130
145
  - Scroll, resize, and visual viewport changes while open recompute placement and size
131
146
 
132
- **LOGIN popover example** (dropdown under a top-right button):
147
+ #### LOGIN popup example
148
+
149
+ Popup below a top-right login button; pass `authType` for non-DUMMY partners:
133
150
 
134
151
  ```typescript
135
152
  import {
136
153
  ConnectSDK,
137
154
  BenjiConnectMode,
138
155
  BenjiConnectPlacement,
156
+ PartnerIntegrationType,
139
157
  } from "@benji-money/connect-sdk";
140
158
 
141
- // Prefer a live binding (selector or ref) when the button may re-render before open()
142
159
  const sdk = new ConnectSDK({
143
160
  environment: "sandbox",
144
161
  token: connectToken,
145
162
  mode: BenjiConnectMode.LOGIN,
163
+ authType: PartnerIntegrationType.OAUTH_PKCE,
146
164
  anchor: "#login-btn",
147
165
  placement: BenjiConnectPlacement.BOTTOM_END,
148
166
  offset: { top: 8 },
149
167
  onSuccess: (token) => { /* ... */ },
168
+ onError: (error) => { /* popup blocked, etc. */ },
169
+ onExit: (metadata) => { /* POPUP_CLOSED when user dismisses popup */ },
150
170
  });
151
171
 
152
172
  await sdk.open();
153
173
  ```
154
174
 
155
- > **`bottom-end`** opens below the anchor with the modal's right edge aligned to the anchor's right edge — matching a dropdown under a top-right LOGIN button.
175
+ > **`bottom-end`** positions the **partner popup** below the anchor with its right edge aligned to the anchor's right edge. The Benji Connect iframe stays headless (not visible).
156
176
 
157
177
  ### Environments
158
178
 
@@ -110,7 +110,7 @@ var ConnectSDK = (() => {
110
110
  // src/utils/config.ts
111
111
  var buildContext = () => ({
112
112
  namespace: "benji-connect-sdk" ? "benji-connect-sdk" : "benji-connect-sdk",
113
- version: "1.1.0-beta.2" ? String("1.1.0-beta.2") : "0.0.0"
113
+ version: "1.1.0-beta.3" ? String("1.1.0-beta.3") : "0.0.0"
114
114
  });
115
115
  function isBenjiConnectEnvironment(environment) {
116
116
  return environment === "local" /* LOCAL */ || environment === "development" /* DEVELOPMENT */ || environment === "sandbox" /* SANDBOX */ || environment === "production" /* PRODUCTION */;
@@ -133,6 +133,8 @@ var ConnectSDK = (() => {
133
133
  // src/utils/placement.ts
134
134
  var IFRAME_WIDTH = 400;
135
135
  var IFRAME_HEIGHT = 645;
136
+ var POPUP_WIDTH = 420;
137
+ var POPUP_HEIGHT = 640;
136
138
  var IFRAME_STATIC_STYLE = {
137
139
  border: "none",
138
140
  borderRadius: "20px",
@@ -389,6 +391,165 @@ var ConnectSDK = (() => {
389
391
  left: `${state.layout.left + viewportOffset.left}px`
390
392
  });
391
393
  }
394
+ function popupFitsBelow(anchorBottom, offsetTop, viewportHeight) {
395
+ const top = anchorBottom + offsetTop;
396
+ const bottom = top + POPUP_HEIGHT;
397
+ return top >= VIEWPORT_PADDING && bottom <= viewportHeight - VIEWPORT_PADDING;
398
+ }
399
+ function popupFitsHorizontally(viewportWidth) {
400
+ return viewportWidth >= POPUP_WIDTH + 2 * VIEWPORT_PADDING;
401
+ }
402
+ function clampPopupHorizontal(left, viewportWidth) {
403
+ const min = VIEWPORT_PADDING;
404
+ const max = viewportWidth - POPUP_WIDTH - VIEWPORT_PADDING;
405
+ if (max < min) {
406
+ return min;
407
+ }
408
+ return Math.min(Math.max(left, min), max);
409
+ }
410
+ function centerPopupViewportRect(viewport) {
411
+ const left = Math.max(
412
+ VIEWPORT_PADDING,
413
+ (viewport.width - POPUP_WIDTH) / 2
414
+ );
415
+ const top = Math.max(
416
+ VIEWPORT_PADDING,
417
+ (viewport.height - POPUP_HEIGHT) / 2
418
+ );
419
+ return {
420
+ top,
421
+ left,
422
+ width: POPUP_WIDTH,
423
+ height: POPUP_HEIGHT,
424
+ placement: "center" /* CENTER */
425
+ };
426
+ }
427
+ function computePopupViewportRect(anchorElement, placement, offset, viewport, options = {}) {
428
+ const { warnOnInvalidAnchor = false } = options;
429
+ const resolvedOffset = defaultOffset(offset);
430
+ if (placement === "center" /* CENTER */) {
431
+ return centerPopupViewportRect(viewport);
432
+ }
433
+ if (!isAnchorVisible(anchorElement)) {
434
+ if (isPreservableAnchoredLayout(options.preserveAnchoredLayout)) {
435
+ return clampPreservedAnchoredLayout(
436
+ options.preserveAnchoredLayout,
437
+ viewport
438
+ );
439
+ }
440
+ if (warnOnInvalidAnchor) {
441
+ warnInvalidAnchor(
442
+ "anchor is hidden or detached; falling back to centered popup"
443
+ );
444
+ }
445
+ return centerPopupViewportRect(viewport);
446
+ }
447
+ const rect = anchorElement.getBoundingClientRect();
448
+ if (!popupFitsBelow(rect.bottom, resolvedOffset.top, viewport.height)) {
449
+ return centerPopupViewportRect(viewport);
450
+ }
451
+ if (!popupFitsHorizontally(viewport.width)) {
452
+ return centerPopupViewportRect(viewport);
453
+ }
454
+ const top = rect.bottom + resolvedOffset.top;
455
+ const idealLeft = placement === "bottom-end" /* BOTTOM_END */ ? rect.right - POPUP_WIDTH + resolvedOffset.left : rect.left + resolvedOffset.left;
456
+ if (idealLeft < VIEWPORT_PADDING) {
457
+ if (warnOnInvalidAnchor) {
458
+ warnInvalidAnchor(
459
+ "anchor rect is unusable for popup placement; falling back to centered popup"
460
+ );
461
+ }
462
+ return centerPopupViewportRect(viewport);
463
+ }
464
+ const left = clampPopupHorizontal(idealLeft, viewport.width);
465
+ return {
466
+ top,
467
+ left,
468
+ width: POPUP_WIDTH,
469
+ height: POPUP_HEIGHT,
470
+ placement
471
+ };
472
+ }
473
+ function resolvePopupViewportLayout(config, viewport = {
474
+ width: window.innerWidth,
475
+ height: window.innerHeight
476
+ }, options = {}) {
477
+ const { warnOnInvalidAnchor = true } = options;
478
+ const placement = resolveEffectivePlacement(config);
479
+ const anchorElement = resolveAnchor(config.anchor);
480
+ if (warnOnInvalidAnchor && placement !== "center" /* CENTER */ && !anchorElement) {
481
+ warnInvalidAnchor("anchor not found; falling back to centered popup");
482
+ if (isPreservableAnchoredLayout(options.preserveAnchoredLayout)) {
483
+ return clampPreservedAnchoredLayout(
484
+ options.preserveAnchoredLayout,
485
+ viewport
486
+ );
487
+ }
488
+ return centerPopupViewportRect(viewport);
489
+ }
490
+ if (placement === "center" /* CENTER */ || !anchorElement) {
491
+ return centerPopupViewportRect(viewport);
492
+ }
493
+ return computePopupViewportRect(
494
+ anchorElement,
495
+ placement,
496
+ config.offset,
497
+ viewport,
498
+ options
499
+ );
500
+ }
501
+ function computePopupScreenPosition(config, state = { lastValidAnchoredLayout: null }, options = {}) {
502
+ const layout = resolvePopupViewportLayout(config, void 0, {
503
+ ...options,
504
+ preserveAnchoredLayout: state.lastValidAnchoredLayout
505
+ });
506
+ if (layout.placement !== "center" /* CENTER */) {
507
+ state.lastValidAnchoredLayout = layout;
508
+ }
509
+ const viewportOffset = getVisualViewportOffset();
510
+ return {
511
+ left: window.screenX + layout.left + viewportOffset.left,
512
+ top: window.screenY + layout.top + viewportOffset.top,
513
+ width: layout.width,
514
+ height: layout.height
515
+ };
516
+ }
517
+ function buildLoginPopupFeatures(position) {
518
+ return [
519
+ `width=${position.width}`,
520
+ `height=${position.height}`,
521
+ `left=${Math.round(position.left)}`,
522
+ `top=${Math.round(position.top)}`,
523
+ "scrollbars=yes",
524
+ "resizable=yes"
525
+ ].join(",");
526
+ }
527
+ function applyHeadlessContainerStyle(container) {
528
+ Object.assign(container.style, {
529
+ position: "fixed",
530
+ top: "0",
531
+ left: "-9999px",
532
+ width: "1px",
533
+ height: "1px",
534
+ overflow: "hidden",
535
+ pointerEvents: "none",
536
+ backgroundColor: "transparent",
537
+ opacity: "0",
538
+ zIndex: "-1"
539
+ });
540
+ }
541
+ function applyHeadlessIframeStyle(iframe) {
542
+ Object.assign(iframe.style, {
543
+ position: "absolute",
544
+ top: "0",
545
+ left: "0",
546
+ width: "1px",
547
+ height: "1px",
548
+ border: "none",
549
+ opacity: "0",
550
+ pointerEvents: "none"
551
+ });
552
+ }
392
553
  function attachRepositionListeners(config, onReposition) {
393
554
  const repositionHandler = onReposition;
394
555
  const repositionScrollTargets = getRepositionScrollTargets(config.anchor);
@@ -418,6 +579,7 @@ var ConnectSDK = (() => {
418
579
  BenjiConnectEventType2["FLOW_SUCCESS"] = "FLOW_SUCCESS";
419
580
  BenjiConnectEventType2["EVENT"] = "EVENT";
420
581
  BenjiConnectEventType2["ERROR"] = "ERROR";
582
+ BenjiConnectEventType2["PARTNER_LOGIN_OPEN"] = "PARTNER_LOGIN_OPEN";
421
583
  return BenjiConnectEventType2;
422
584
  })(BenjiConnectEventType || {});
423
585
 
@@ -549,6 +711,8 @@ var ConnectSDK = (() => {
549
711
  this.onExit = config.onExit;
550
712
  this.onEvent = config.onEvent;
551
713
  this.close = config.close;
714
+ this.closeLoginPopup = config.closeLoginPopup;
715
+ this.navigateLoginPopup = config.navigateLoginPopup;
552
716
  }
553
717
  static setMode(mode) {
554
718
  this.mode = mode;
@@ -586,7 +750,7 @@ var ConnectSDK = (() => {
586
750
  }
587
751
  }
588
752
  static handleMessage(event) {
589
- var _a, _b, _c, _d, _e, _f, _g;
753
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
590
754
  if (!isConnectMessageType(event.data)) return;
591
755
  const expectedOrigin = new URL(Endpoints.benji_connect_auth_url).origin;
592
756
  if (!event || event.origin !== expectedOrigin) {
@@ -634,6 +798,16 @@ var ConnectSDK = (() => {
634
798
  (_g = this.onEvent) == null ? void 0 : _g.call(this, callbackData.type, callbackData.metadata);
635
799
  break;
636
800
  }
801
+ case "PARTNER_LOGIN_OPEN" /* PARTNER_LOGIN_OPEN */: {
802
+ const connectMessage = message;
803
+ const url = (_h = connectMessage.data) == null ? void 0 : _h.url;
804
+ if (url) {
805
+ (_i = this.navigateLoginPopup) == null ? void 0 : _i.call(this, url);
806
+ } else {
807
+ (_j = this.closeLoginPopup) == null ? void 0 : _j.call(this);
808
+ }
809
+ break;
810
+ }
637
811
  }
638
812
  } finally {
639
813
  }
@@ -650,6 +824,7 @@ var ConnectSDK = (() => {
650
824
  var MessageRouter = _MessageRouter;
651
825
 
652
826
  // src/connect-sdk.ts
827
+ var POPUP_CLOSED_POLL_MS = 500;
653
828
  var ConnectSDK = class {
654
829
  constructor(config) {
655
830
  this.iframe = null;
@@ -664,7 +839,20 @@ var ConnectSDK = (() => {
664
839
  },
665
840
  lastValidAnchoredLayout: null
666
841
  };
842
+ this.popupLayoutState = {
843
+ lastValidAnchoredLayout: null
844
+ };
667
845
  this.repositionListeners = null;
846
+ this.loginPopup = null;
847
+ this.popupClosedPollId = null;
848
+ this.isClosing = false;
849
+ /**
850
+ * Aborts a pending open() when close() runs before the iframe mounts.
851
+ * Non-DUMMY LOGIN opens the partner popup synchronously, then awaits initialize()
852
+ * before mounting a headless iframe. Without this, close() can tear down the popup
853
+ * while the in-flight open() still registers listeners and mounts the iframe.
854
+ */
855
+ this.openAbortController = null;
668
856
  this.sdkConfig = config;
669
857
  if (!this.sdkConfig.environment) throw new Error("Environment is required");
670
858
  if (!this.sdkConfig.token) throw new Error("Connect token is required");
@@ -672,26 +860,125 @@ var ConnectSDK = (() => {
672
860
  this.sdkConfig.environment = environment;
673
861
  configureConfig(environment);
674
862
  }
863
+ isLoginPopupOnly() {
864
+ var _a;
865
+ const mode = (_a = this.sdkConfig.mode) != null ? _a : 1 /* CONNECT */;
866
+ return mode === 5 /* LOGIN */ && this.sdkConfig.authType != null && this.sdkConfig.authType !== 0 /* DUMMY */;
867
+ }
868
+ isSessionActive() {
869
+ return this.iframe != null || this.loginPopup != null;
870
+ }
871
+ /** Cancels any open() still running between popup pre-open and iframe mount. */
872
+ abortInflightOpen() {
873
+ var _a;
874
+ (_a = this.openAbortController) == null ? void 0 : _a.abort();
875
+ this.openAbortController = null;
876
+ }
675
877
  async initialize() {
676
878
  MessageRouter.configureMessageRouter({
677
879
  onSuccess: this.sdkConfig.onSuccess,
678
880
  onError: this.sdkConfig.onError,
679
881
  onExit: this.sdkConfig.onExit,
680
882
  onEvent: this.sdkConfig.onEvent,
681
- close: () => this.close()
883
+ close: () => this.close(),
884
+ closeLoginPopup: () => this.closeLoginPopup(),
885
+ navigateLoginPopup: (url) => this.navigateLoginPopup(url)
682
886
  });
683
887
  }
888
+ openLoginPopup() {
889
+ var _a, _b;
890
+ const position = computePopupScreenPosition(
891
+ this.sdkConfig,
892
+ this.popupLayoutState,
893
+ { warnOnInvalidAnchor: true }
894
+ );
895
+ this.loginPopup = window.open(
896
+ "about:blank",
897
+ "benji_partner_auth",
898
+ buildLoginPopupFeatures(position)
899
+ );
900
+ if (!this.loginPopup) {
901
+ console.warn("[Connect SDK] Failed to pre-open LOGIN partner auth popup");
902
+ (_b = (_a = this.sdkConfig).onError) == null ? void 0 : _b.call(
903
+ _a,
904
+ new Error("Popup blocked. Allow popups for this site and try again."),
905
+ "400",
906
+ { context: buildContext() }
907
+ );
908
+ return false;
909
+ }
910
+ this.startPopupClosedPoll();
911
+ return true;
912
+ }
913
+ startPopupClosedPoll() {
914
+ this.stopPopupClosedPoll();
915
+ this.popupClosedPollId = setInterval(() => {
916
+ var _a, _b, _c;
917
+ if (this.isClosing) return;
918
+ if ((_a = this.loginPopup) == null ? void 0 : _a.closed) {
919
+ (_c = (_b = this.sdkConfig).onExit) == null ? void 0 : _c.call(_b, {
920
+ context: buildContext(),
921
+ trigger: "POPUP_CLOSED" /* POPUP_CLOSED */
922
+ });
923
+ this.close();
924
+ }
925
+ }, POPUP_CLOSED_POLL_MS);
926
+ }
927
+ stopPopupClosedPoll() {
928
+ if (this.popupClosedPollId != null) {
929
+ clearInterval(this.popupClosedPollId);
930
+ this.popupClosedPollId = null;
931
+ }
932
+ }
933
+ repositionLoginPopup() {
934
+ if (!this.loginPopup || this.loginPopup.closed) return;
935
+ const position = computePopupScreenPosition(
936
+ this.sdkConfig,
937
+ this.popupLayoutState
938
+ );
939
+ try {
940
+ this.loginPopup.moveTo(
941
+ Math.round(position.left),
942
+ Math.round(position.top)
943
+ );
944
+ this.loginPopup.resizeTo(position.width, position.height);
945
+ } catch (error) {
946
+ console.warn("[Connect SDK] Failed to reposition LOGIN popup", error);
947
+ }
948
+ }
684
949
  async open() {
685
950
  var _a;
951
+ if (this.isSessionActive()) return;
952
+ const mode = (_a = this.sdkConfig.mode) != null ? _a : 1 /* CONNECT */;
953
+ const abortController = new AbortController();
954
+ this.openAbortController = abortController;
955
+ const { signal } = abortController;
956
+ if (this.isLoginPopupOnly()) {
957
+ const opened = this.openLoginPopup();
958
+ if (!opened) {
959
+ this.abortInflightOpen();
960
+ return;
961
+ }
962
+ }
686
963
  await this.initialize();
687
- this.openUI();
688
- MessageRouter.setMode((_a = this.sdkConfig.mode) != null ? _a : 1 /* CONNECT */);
964
+ if (signal.aborted) return;
965
+ MessageRouter.setMode(mode);
689
966
  MessageRouter.addEventListeners();
967
+ if (signal.aborted) {
968
+ MessageRouter.removeEventListeners();
969
+ return;
970
+ }
971
+ this.openUI();
690
972
  }
691
973
  openUI() {
692
974
  if (this.iframe) return;
975
+ const headless = this.isLoginPopupOnly();
693
976
  this.container = document.createElement("div");
694
- applyOverlayBaseStyle(this.container);
977
+ if (headless) {
978
+ applyHeadlessContainerStyle(this.container);
979
+ } else {
980
+ applyOverlayBaseStyle(this.container);
981
+ }
695
982
  console.debug("[Connect SDK] endpoints", Endpoints);
696
983
  console.log("[Connect SDK] opeining URL", Endpoints.benji_connect_auth_url);
697
984
  const url = new URL(Endpoints.benji_connect_auth_url);
@@ -705,48 +992,88 @@ var ConnectSDK = (() => {
705
992
  this.iframe.src = url.toString();
706
993
  this.container.appendChild(this.iframe);
707
994
  document.body.appendChild(this.container);
708
- applyIframeLayoutToElements(
709
- this.sdkConfig,
710
- this.container,
711
- this.iframe,
712
- this.layoutState,
713
- { warnOnInvalidAnchor: true }
714
- );
715
- this.repositionListeners = attachRepositionListeners(
716
- this.sdkConfig,
717
- () => {
718
- if (!this.iframe || !this.container) return;
719
- applyIframeLayoutToElements(
720
- this.sdkConfig,
721
- this.container,
722
- this.iframe,
723
- this.layoutState
724
- );
725
- }
726
- );
727
- this.container.addEventListener("click", (e) => {
728
- var _a, _b;
729
- if (e.target === this.container) {
730
- (_b = (_a = this.sdkConfig).onExit) == null ? void 0 : _b.call(_a, {
731
- context: buildContext(),
732
- trigger: "TAPPED_OUT_OF_BOUNDS" /* TAPPED_OUT_OF_BOUNDS */
733
- });
734
- this.close();
995
+ if (headless) {
996
+ applyHeadlessIframeStyle(this.iframe);
997
+ this.repositionListeners = attachRepositionListeners(
998
+ this.sdkConfig,
999
+ () => this.repositionLoginPopup()
1000
+ );
1001
+ } else {
1002
+ applyIframeLayoutToElements(
1003
+ this.sdkConfig,
1004
+ this.container,
1005
+ this.iframe,
1006
+ this.layoutState,
1007
+ { warnOnInvalidAnchor: true }
1008
+ );
1009
+ this.repositionListeners = attachRepositionListeners(
1010
+ this.sdkConfig,
1011
+ () => {
1012
+ if (!this.iframe || !this.container) return;
1013
+ applyIframeLayoutToElements(
1014
+ this.sdkConfig,
1015
+ this.container,
1016
+ this.iframe,
1017
+ this.layoutState
1018
+ );
1019
+ }
1020
+ );
1021
+ this.container.addEventListener("click", (e) => {
1022
+ var _a, _b;
1023
+ if (e.target === this.container) {
1024
+ (_b = (_a = this.sdkConfig).onExit) == null ? void 0 : _b.call(_a, {
1025
+ context: buildContext(),
1026
+ trigger: "TAPPED_OUT_OF_BOUNDS" /* TAPPED_OUT_OF_BOUNDS */
1027
+ });
1028
+ this.close();
1029
+ }
1030
+ });
1031
+ }
1032
+ }
1033
+ navigateLoginPopup(url) {
1034
+ if (!this.loginPopup || this.loginPopup.closed) {
1035
+ console.warn("[Connect SDK] Cannot navigate LOGIN popup \u2014 window unavailable");
1036
+ return;
1037
+ }
1038
+ try {
1039
+ this.loginPopup.location.href = url;
1040
+ } catch (error) {
1041
+ console.warn("[Connect SDK] Failed to navigate LOGIN popup", error);
1042
+ }
1043
+ }
1044
+ closeLoginPopup() {
1045
+ if (!this.loginPopup) return;
1046
+ try {
1047
+ if (!this.loginPopup.closed) {
1048
+ this.loginPopup.close();
735
1049
  }
736
- });
1050
+ } catch (error) {
1051
+ console.warn("[Connect SDK] Error closing LOGIN popup", error);
1052
+ } finally {
1053
+ this.loginPopup = null;
1054
+ }
737
1055
  }
738
1056
  close() {
739
1057
  var _a, _b;
740
- if (!this.iframe) return;
1058
+ this.abortInflightOpen();
1059
+ this.isClosing = true;
1060
+ this.stopPopupClosedPoll();
1061
+ this.closeLoginPopup();
1062
+ MessageRouter.removeEventListeners();
1063
+ if (!this.iframe) {
1064
+ this.isClosing = false;
1065
+ return;
1066
+ }
741
1067
  (_a = this.repositionListeners) == null ? void 0 : _a.remove();
742
1068
  this.repositionListeners = null;
743
1069
  this.layoutState.lastValidAnchoredLayout = null;
1070
+ this.popupLayoutState.lastValidAnchoredLayout = null;
744
1071
  if ((_b = this.container) == null ? void 0 : _b.parentNode) {
745
1072
  this.container.parentNode.removeChild(this.container);
746
1073
  }
747
- MessageRouter.removeEventListeners();
748
1074
  this.iframe = null;
749
1075
  this.container = null;
1076
+ this.isClosing = false;
750
1077
  }
751
1078
  cleanup() {
752
1079
  if (this.iframe) this.close();