@benji-money/connect-sdk 1.1.4-beta.1 → 1.1.4-beta.2

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). 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.2" ? String("1.1.4-beta.2") : "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");
@@ -987,6 +1032,8 @@ var ConnectSDK = (() => {
987
1032
  _MessageRouter.loginPopup = null;
988
1033
  _MessageRouter.mode = 1 /* CONNECT */;
989
1034
  _MessageRouter.connectOrigin = "";
1035
+ _MessageRouter.isIframeReady = false;
1036
+ _MessageRouter.pendingPopupAuth = [];
990
1037
  _MessageRouter.errorEventHandler = (event) => {
991
1038
  return _MessageRouter.handleErrorMessage(event);
992
1039
  };
@@ -1026,6 +1073,10 @@ var ConnectSDK = (() => {
1026
1073
  * while the in-flight open() still registers listeners and mounts the iframe.
1027
1074
  */
1028
1075
  this.openAbortController = null;
1076
+ this.onIframeLoad = () => {
1077
+ if (this.isClosing || !this.iframe) return;
1078
+ MessageRouter.markIframeReady(this.iframe);
1079
+ };
1029
1080
  this.sdkConfig = config;
1030
1081
  if (!this.sdkConfig.environment) throw new Error("Environment is required");
1031
1082
  if (!this.sdkConfig.token) throw new Error("Connect token is required");
@@ -1064,14 +1115,16 @@ var ConnectSDK = (() => {
1064
1115
  });
1065
1116
  }
1066
1117
  openLoginPopup() {
1067
- var _a, _b, _c;
1118
+ var _a, _b, _c, _d;
1068
1119
  const position = computePopupScreenPosition(
1069
1120
  this.sdkConfig,
1070
1121
  this.popupLayoutState,
1071
1122
  { warnOnInvalidAnchor: true }
1072
1123
  );
1073
1124
  const partnerAuthLink = (_a = this.sdkConfig.tokenData) == null ? void 0 : _a.partnerAuthLink;
1074
- const popupUrl = partnerAuthLink ? appendLoginPopupFlag(partnerAuthLink) : "about:blank";
1125
+ const configToken = (_b = this.sdkConfig.tokenData) == null ? void 0 : _b.configToken;
1126
+ const preparedLink = partnerAuthLink && configToken ? preparePartnerAuthUrl(partnerAuthLink, configToken) : partnerAuthLink;
1127
+ const popupUrl = preparedLink ? appendLoginPopupFlag(preparedLink) : "about:blank";
1075
1128
  this.loginPopup = window.open(
1076
1129
  popupUrl,
1077
1130
  "benji_partner_auth",
@@ -1079,15 +1132,15 @@ var ConnectSDK = (() => {
1079
1132
  );
1080
1133
  if (!this.loginPopup) {
1081
1134
  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,
1135
+ (_d = (_c = this.sdkConfig).onError) == null ? void 0 : _d.call(
1136
+ _c,
1084
1137
  new Error("Popup blocked. Allow popups for this site and try again."),
1085
1138
  "400",
1086
1139
  { context: buildContext() }
1087
1140
  );
1088
1141
  return false;
1089
1142
  }
1090
- if (partnerAuthLink) {
1143
+ if (preparedLink) {
1091
1144
  this.hasLoginPopupUrl = true;
1092
1145
  } else {
1093
1146
  writeLoginPopupLoadingDocument(this.loginPopup);
@@ -1164,8 +1217,9 @@ var ConnectSDK = (() => {
1164
1217
  });
1165
1218
  }
1166
1219
  openUI() {
1167
- var _a;
1220
+ var _a, _b;
1168
1221
  if (this.iframe) return;
1222
+ MessageRouter.resetIframeReady();
1169
1223
  const headless = this.isLoginPopupOnly();
1170
1224
  this.container = document.createElement("div");
1171
1225
  if (headless) {
@@ -1183,6 +1237,8 @@ var ConnectSDK = (() => {
1183
1237
  url.searchParams.set("mode", String(this.sdkConfig.mode));
1184
1238
  }
1185
1239
  this.iframe = document.createElement("iframe");
1240
+ this.syncLoginContext((_a = this.sdkConfig.mode) != null ? _a : 1 /* CONNECT */);
1241
+ this.iframe.addEventListener("load", this.onIframeLoad);
1186
1242
  this.iframe.src = url.toString();
1187
1243
  this.container.appendChild(this.iframe);
1188
1244
  document.body.appendChild(this.container);
@@ -1213,9 +1269,9 @@ var ConnectSDK = (() => {
1213
1269
  }
1214
1270
  );
1215
1271
  this.container.addEventListener("click", (e) => {
1216
- var _a2, _b;
1272
+ var _a2, _b2;
1217
1273
  if (e.target === this.container) {
1218
- (_b = (_a2 = this.sdkConfig).onExit) == null ? void 0 : _b.call(_a2, {
1274
+ (_b2 = (_a2 = this.sdkConfig).onExit) == null ? void 0 : _b2.call(_a2, {
1219
1275
  context: buildContext(),
1220
1276
  trigger: "TAPPED_OUT_OF_BOUNDS" /* TAPPED_OUT_OF_BOUNDS */
1221
1277
  });
@@ -1223,7 +1279,7 @@ var ConnectSDK = (() => {
1223
1279
  }
1224
1280
  });
1225
1281
  }
1226
- this.syncLoginContext((_a = this.sdkConfig.mode) != null ? _a : 1 /* CONNECT */);
1282
+ this.syncLoginContext((_b = this.sdkConfig.mode) != null ? _b : 1 /* CONNECT */);
1227
1283
  }
1228
1284
  navigateLoginPopup(url) {
1229
1285
  if (this.hasLoginPopupUrl) return;
@@ -1266,6 +1322,7 @@ var ConnectSDK = (() => {
1266
1322
  this.repositionListeners = null;
1267
1323
  this.layoutState.lastValidAnchoredLayout = null;
1268
1324
  this.popupLayoutState.lastValidAnchoredLayout = null;
1325
+ this.iframe.removeEventListener("load", this.onIframeLoad);
1269
1326
  if ((_b = this.container) == null ? void 0 : _b.parentNode) {
1270
1327
  this.container.parentNode.removeChild(this.container);
1271
1328
  }