@benji-money/connect-sdk 1.1.4-beta.1 → 1.1.4-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
@@ -88,7 +88,7 @@ The SDK accepts the following configuration options:
88
88
  - `token` (required): Your API connect token
89
89
  - `mode` (optional): `BenjiConnectMode` — `CONNECT` (default), `TRANSFER`, `REDEEM`, or `LOGIN`
90
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. Prefer `tokenData.authType` when using `tokenData`.
91
- - `tokenData` (optional): `BenjiConnectTokenData` — LOGIN only; pass `partnerAuthLink` (Auth Service `connect_url`) and `authType` so the SDK opens the partner auth URL synchronously in the popup on `open()` instead of `about:blank` + loading HTML
91
+ - `tokenData` (optional): `BenjiConnectTokenData` — LOGIN only; pass `partnerAuthLink` (Auth Service `connect_url`), `configToken` (mint `config_token`, used as OAuth `state` on the popup URL), and `authType` so the SDK opens the partner auth URL synchronously on `open()`
92
92
  - `onSuccess` (optional): Callback function called when connect completed successfully
93
93
  - `onError` (optional): Callback function called when error occurs in the connect flow
94
94
  - `onExit` (optional): Callback function called when the user exits the connect flow
@@ -164,6 +164,7 @@ const sdk = new ConnectSDK({
164
164
  tokenData: {
165
165
  authType: PartnerIntegrationType.OAUTH_PKCE,
166
166
  partnerAuthLink: connectUrl, // from Auth Service connect_url
167
+ configToken: configToken, // from GET /connect/token — OAuth state
167
168
  },
168
169
  anchor: "#login-btn",
169
170
  placement: BenjiConnectPlacement.BOTTOM_END,
@@ -176,7 +177,7 @@ const sdk = new ConnectSDK({
176
177
  await sdk.open();
177
178
  ```
178
179
 
179
- > **`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). When `tokenData.partnerAuthLink` is set, the popup navigates to partner auth immediately on `open()`. Otherwise the pre-opened popup shows generic loading UI (`Loading sign-in…`) until Benji Connect sends the partner auth URL.
180
+ > **`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). When `tokenData.partnerAuthLink` is set, the popup opens that URL on `open()` (with `state` from `configToken` when provided) after LOGIN listeners and popup context are registered. The SDK skips `PARTNER_LOGIN_OPEN` navigation when the popup already has a partner URL. Popup→iframe auth messages are queued until the Connect iframe `load` event. Otherwise the pre-opened popup shows generic loading UI (`Loading sign-in…`) until Benji Connect sends the partner auth URL.
180
181
 
181
182
  ### Environments
182
183
 
@@ -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.4-beta.1" ? String("1.1.4-beta.1") : "0.0.0"
113
+ version: "1.1.4-beta.3" ? String("1.1.4-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 */;
@@ -217,10 +217,34 @@ var ConnectSDK = (() => {
217
217
  }
218
218
 
219
219
  // src/utils/partnerAuthLink.ts
220
+ var LOGIN_POPUP_QUERY_FLAG = "benji_login_popup";
221
+ function ensureAuthLinkStateNonce(url, nonce) {
222
+ try {
223
+ const parsed = new URL(url);
224
+ if (!parsed.searchParams.get("state")) {
225
+ parsed.searchParams.set("state", nonce);
226
+ }
227
+ return parsed.toString();
228
+ } catch {
229
+ if (url.includes("state=")) {
230
+ return url;
231
+ }
232
+ const separator = url.includes("?") ? "&" : "?";
233
+ return `${url}${separator}state=${encodeURIComponent(nonce)}`;
234
+ }
235
+ }
220
236
  function appendLoginPopupFlag(url) {
221
- const parsed = new URL(url);
222
- parsed.searchParams.set("benji_login_popup", "1");
223
- return parsed.toString();
237
+ try {
238
+ const parsed = new URL(url);
239
+ parsed.searchParams.set(LOGIN_POPUP_QUERY_FLAG, "1");
240
+ return parsed.toString();
241
+ } catch {
242
+ const separator = url.includes("?") ? "&" : "?";
243
+ return `${url}${separator}${LOGIN_POPUP_QUERY_FLAG}=1`;
244
+ }
245
+ }
246
+ function preparePartnerAuthUrl(url, nonce) {
247
+ return ensureAuthLinkStateNonce(url, nonce);
224
248
  }
225
249
 
226
250
  // src/utils/placement.ts
@@ -833,6 +857,12 @@ var ConnectSDK = (() => {
833
857
  this.loginPopup = context.loginPopup;
834
858
  this.mode = context.mode;
835
859
  this.connectOrigin = new URL(Endpoints.benji_connect_auth_url).origin;
860
+ if (this.isIframeReady) {
861
+ this.flushPendingPopupAuth();
862
+ }
863
+ }
864
+ static resetIframeReady() {
865
+ this.isIframeReady = false;
836
866
  }
837
867
  static setMode(mode) {
838
868
  this.mode = mode;
@@ -853,6 +883,8 @@ var ConnectSDK = (() => {
853
883
  this.loginPopup = null;
854
884
  this.mode = 1 /* CONNECT */;
855
885
  this.connectOrigin = "";
886
+ this.isIframeReady = false;
887
+ this.pendingPopupAuth = [];
856
888
  }
857
889
  static handleErrorMessage(event) {
858
890
  switch (event.type) {
@@ -875,12 +907,25 @@ var ConnectSDK = (() => {
875
907
  static relayToIframe(event) {
876
908
  var _a;
877
909
  const iframeWindow = (_a = this.iframe) == null ? void 0 : _a.contentWindow;
878
- if (!iframeWindow) {
879
- console.warn("[Benji Connect SDK] Skipping login auth relay, iframe not ready");
910
+ if (!this.isIframeReady || !iframeWindow) {
911
+ this.pendingPopupAuth.push(event);
880
912
  return;
881
913
  }
882
914
  iframeWindow.postMessage(event.data, this.connectOrigin);
883
915
  }
916
+ static markIframeReady(iframe) {
917
+ if (this.iframe !== iframe) return;
918
+ this.isIframeReady = true;
919
+ this.flushPendingPopupAuth();
920
+ }
921
+ static flushPendingPopupAuth() {
922
+ if (!this.isIframeReady || !this.pendingPopupAuth.length) return;
923
+ const queued = this.pendingPopupAuth;
924
+ this.pendingPopupAuth = [];
925
+ for (const event of queued) {
926
+ this.relayToIframe(event);
927
+ }
928
+ }
884
929
  static relayToPopup(payload) {
885
930
  if (!this.loginPopup) {
886
931
  console.warn("[Benji Connect SDK] Skipping login auth relay, no login popup");
@@ -894,13 +939,17 @@ var ConnectSDK = (() => {
894
939
  }
895
940
  static handleMessage(event) {
896
941
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
897
- if (shouldRelayMessage(this.mode, event.data) && this.loginPopup) {
942
+ if (shouldRelayMessage(this.mode, event.data)) {
898
943
  const expectedOrigin2 = this.connectOrigin || new URL(Endpoints.benji_connect_auth_url).origin;
899
944
  const iframeWindow = (_a = this.iframe) == null ? void 0 : _a.contentWindow;
900
- if (event.source === this.loginPopup) {
945
+ if (this.loginPopup && event.source === this.loginPopup) {
901
946
  this.relayToIframe(event);
902
947
  return;
903
948
  }
949
+ if (!this.loginPopup && event.source instanceof Window && event.source !== iframeWindow) {
950
+ this.pendingPopupAuth.push(event);
951
+ return;
952
+ }
904
953
  if (iframeWindow && event.source === iframeWindow) {
905
954
  if (event.origin !== expectedOrigin2) {
906
955
  console.warn(
@@ -987,6 +1036,8 @@ var ConnectSDK = (() => {
987
1036
  _MessageRouter.loginPopup = null;
988
1037
  _MessageRouter.mode = 1 /* CONNECT */;
989
1038
  _MessageRouter.connectOrigin = "";
1039
+ _MessageRouter.isIframeReady = false;
1040
+ _MessageRouter.pendingPopupAuth = [];
990
1041
  _MessageRouter.errorEventHandler = (event) => {
991
1042
  return _MessageRouter.handleErrorMessage(event);
992
1043
  };
@@ -1021,11 +1072,15 @@ var ConnectSDK = (() => {
1021
1072
  this.isClosing = false;
1022
1073
  /**
1023
1074
  * Aborts a pending open() when close() runs before the iframe mounts.
1024
- * Non-DUMMY LOGIN opens the partner popup synchronously, then awaits initialize()
1025
- * before mounting a headless iframe. Without this, close() can tear down the popup
1026
- * while the in-flight open() still registers listeners and mounts the iframe.
1075
+ * Non-DUMMY LOGIN arms listeners, then opens the partner popup synchronously,
1076
+ * then mounts a headless iframe. Without this, close() can tear down the popup
1077
+ * while the in-flight open() still mounts the iframe.
1027
1078
  */
1028
1079
  this.openAbortController = null;
1080
+ this.onIframeLoad = () => {
1081
+ if (this.isClosing || !this.iframe) return;
1082
+ MessageRouter.markIframeReady(this.iframe);
1083
+ };
1029
1084
  this.sdkConfig = config;
1030
1085
  if (!this.sdkConfig.environment) throw new Error("Environment is required");
1031
1086
  if (!this.sdkConfig.token) throw new Error("Connect token is required");
@@ -1052,7 +1107,7 @@ var ConnectSDK = (() => {
1052
1107
  (_a = this.openAbortController) == null ? void 0 : _a.abort();
1053
1108
  this.openAbortController = null;
1054
1109
  }
1055
- async initialize() {
1110
+ initialize() {
1056
1111
  MessageRouter.configureMessageRouter({
1057
1112
  onSuccess: this.sdkConfig.onSuccess,
1058
1113
  onError: this.sdkConfig.onError,
@@ -1064,14 +1119,16 @@ var ConnectSDK = (() => {
1064
1119
  });
1065
1120
  }
1066
1121
  openLoginPopup() {
1067
- var _a, _b, _c;
1122
+ var _a, _b, _c, _d;
1068
1123
  const position = computePopupScreenPosition(
1069
1124
  this.sdkConfig,
1070
1125
  this.popupLayoutState,
1071
1126
  { warnOnInvalidAnchor: true }
1072
1127
  );
1073
1128
  const partnerAuthLink = (_a = this.sdkConfig.tokenData) == null ? void 0 : _a.partnerAuthLink;
1074
- const popupUrl = partnerAuthLink ? appendLoginPopupFlag(partnerAuthLink) : "about:blank";
1129
+ const configToken = (_b = this.sdkConfig.tokenData) == null ? void 0 : _b.configToken;
1130
+ const preparedLink = partnerAuthLink && configToken ? preparePartnerAuthUrl(partnerAuthLink, configToken) : partnerAuthLink;
1131
+ const popupUrl = preparedLink ? appendLoginPopupFlag(preparedLink) : "about:blank";
1075
1132
  this.loginPopup = window.open(
1076
1133
  popupUrl,
1077
1134
  "benji_partner_auth",
@@ -1079,15 +1136,15 @@ var ConnectSDK = (() => {
1079
1136
  );
1080
1137
  if (!this.loginPopup) {
1081
1138
  console.warn("[Connect SDK] Failed to pre-open LOGIN partner auth popup");
1082
- (_c = (_b = this.sdkConfig).onError) == null ? void 0 : _c.call(
1083
- _b,
1139
+ (_d = (_c = this.sdkConfig).onError) == null ? void 0 : _d.call(
1140
+ _c,
1084
1141
  new Error("Popup blocked. Allow popups for this site and try again."),
1085
1142
  "400",
1086
1143
  { context: buildContext() }
1087
1144
  );
1088
1145
  return false;
1089
1146
  }
1090
- if (partnerAuthLink) {
1147
+ if (preparedLink) {
1091
1148
  this.hasLoginPopupUrl = true;
1092
1149
  } else {
1093
1150
  writeLoginPopupLoadingDocument(this.loginPopup);
@@ -1138,17 +1195,19 @@ var ConnectSDK = (() => {
1138
1195
  const abortController = new AbortController();
1139
1196
  this.openAbortController = abortController;
1140
1197
  const { signal } = abortController;
1198
+ this.initialize();
1199
+ if (signal.aborted) return;
1200
+ MessageRouter.setMode(mode);
1201
+ MessageRouter.addEventListeners();
1141
1202
  if (this.isLoginPopupOnly()) {
1142
1203
  const opened = this.openLoginPopup();
1143
1204
  if (!opened) {
1144
1205
  this.abortInflightOpen();
1206
+ MessageRouter.removeEventListeners();
1145
1207
  return;
1146
1208
  }
1209
+ this.syncLoginContext(mode);
1147
1210
  }
1148
- await this.initialize();
1149
- if (signal.aborted) return;
1150
- MessageRouter.setMode(mode);
1151
- MessageRouter.addEventListeners();
1152
1211
  if (signal.aborted) {
1153
1212
  MessageRouter.removeEventListeners();
1154
1213
  return;
@@ -1164,8 +1223,9 @@ var ConnectSDK = (() => {
1164
1223
  });
1165
1224
  }
1166
1225
  openUI() {
1167
- var _a;
1226
+ var _a, _b;
1168
1227
  if (this.iframe) return;
1228
+ MessageRouter.resetIframeReady();
1169
1229
  const headless = this.isLoginPopupOnly();
1170
1230
  this.container = document.createElement("div");
1171
1231
  if (headless) {
@@ -1183,6 +1243,8 @@ var ConnectSDK = (() => {
1183
1243
  url.searchParams.set("mode", String(this.sdkConfig.mode));
1184
1244
  }
1185
1245
  this.iframe = document.createElement("iframe");
1246
+ this.syncLoginContext((_a = this.sdkConfig.mode) != null ? _a : 1 /* CONNECT */);
1247
+ this.iframe.addEventListener("load", this.onIframeLoad);
1186
1248
  this.iframe.src = url.toString();
1187
1249
  this.container.appendChild(this.iframe);
1188
1250
  document.body.appendChild(this.container);
@@ -1213,9 +1275,9 @@ var ConnectSDK = (() => {
1213
1275
  }
1214
1276
  );
1215
1277
  this.container.addEventListener("click", (e) => {
1216
- var _a2, _b;
1278
+ var _a2, _b2;
1217
1279
  if (e.target === this.container) {
1218
- (_b = (_a2 = this.sdkConfig).onExit) == null ? void 0 : _b.call(_a2, {
1280
+ (_b2 = (_a2 = this.sdkConfig).onExit) == null ? void 0 : _b2.call(_a2, {
1219
1281
  context: buildContext(),
1220
1282
  trigger: "TAPPED_OUT_OF_BOUNDS" /* TAPPED_OUT_OF_BOUNDS */
1221
1283
  });
@@ -1223,7 +1285,7 @@ var ConnectSDK = (() => {
1223
1285
  }
1224
1286
  });
1225
1287
  }
1226
- this.syncLoginContext((_a = this.sdkConfig.mode) != null ? _a : 1 /* CONNECT */);
1288
+ this.syncLoginContext((_b = this.sdkConfig.mode) != null ? _b : 1 /* CONNECT */);
1227
1289
  }
1228
1290
  navigateLoginPopup(url) {
1229
1291
  if (this.hasLoginPopupUrl) return;
@@ -1266,6 +1328,7 @@ var ConnectSDK = (() => {
1266
1328
  this.repositionListeners = null;
1267
1329
  this.layoutState.lastValidAnchoredLayout = null;
1268
1330
  this.popupLayoutState.lastValidAnchoredLayout = null;
1331
+ this.iframe.removeEventListener("load", this.onIframeLoad);
1269
1332
  if ((_b = this.container) == null ? void 0 : _b.parentNode) {
1270
1333
  this.container.parentNode.removeChild(this.container);
1271
1334
  }