@benji-money/connect-sdk 1.1.3 → 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
@@ -87,7 +87,8 @@ The SDK accepts the following configuration options:
87
87
  - `environment` (required): One of `local`, `development`, `sandbox`, or `production`
88
88
  - `token` (required): Your API connect token
89
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
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`), `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()`
91
92
  - `onSuccess` (optional): Callback function called when connect completed successfully
92
93
  - `onError` (optional): Callback function called when error occurs in the connect flow
93
94
  - `onExit` (optional): Callback function called when the user exits the connect flow
@@ -102,7 +103,7 @@ The SDK accepts the following configuration options:
102
103
  | ---- | ---------- | ----- |
103
104
  | **CONNECT** (default) | Centered or anchored modal iframe | No `authType`; standard connect / verify flow |
104
105
  | **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 — popup shows a loading spinner until partner auth navigates — see [LOGIN popup example](#login-popup-example) below |
106
+ | **LOGIN + non-DUMMY** | Partner popup only | Pass `authType` (or `tokenData.authType`); Benji Connect iframe is headless — popup opens partner auth immediately when `tokenData.partnerAuthLink` is set, otherwise shows loading UI until Benji Connect navigates — see [LOGIN popup example](#login-popup-example) below |
106
107
 
107
108
  ### Placement
108
109
 
@@ -160,7 +161,11 @@ const sdk = new ConnectSDK({
160
161
  environment: "sandbox",
161
162
  token: connectToken,
162
163
  mode: BenjiConnectMode.LOGIN,
163
- authType: PartnerIntegrationType.OAUTH_PKCE,
164
+ tokenData: {
165
+ authType: PartnerIntegrationType.OAUTH_PKCE,
166
+ partnerAuthLink: connectUrl, // from Auth Service connect_url
167
+ configToken: configToken, // from GET /connect/token — OAuth state
168
+ },
164
169
  anchor: "#login-btn",
165
170
  placement: BenjiConnectPlacement.BOTTOM_END,
166
171
  offset: { top: 8 },
@@ -172,7 +177,7 @@ const sdk = new ConnectSDK({
172
177
  await sdk.open();
173
178
  ```
174
179
 
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). 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.
176
181
 
177
182
  ### Environments
178
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.3" ? String("1.1.3") : "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 */;
@@ -216,6 +216,37 @@ var ConnectSDK = (() => {
216
216
  }
217
217
  }
218
218
 
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
+ }
236
+ function appendLoginPopupFlag(url) {
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);
248
+ }
249
+
219
250
  // src/utils/placement.ts
220
251
  var IFRAME_WIDTH = 400;
221
252
  var IFRAME_HEIGHT = 645;
@@ -826,6 +857,12 @@ var ConnectSDK = (() => {
826
857
  this.loginPopup = context.loginPopup;
827
858
  this.mode = context.mode;
828
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;
829
866
  }
830
867
  static setMode(mode) {
831
868
  this.mode = mode;
@@ -846,6 +883,8 @@ var ConnectSDK = (() => {
846
883
  this.loginPopup = null;
847
884
  this.mode = 1 /* CONNECT */;
848
885
  this.connectOrigin = "";
886
+ this.isIframeReady = false;
887
+ this.pendingPopupAuth = [];
849
888
  }
850
889
  static handleErrorMessage(event) {
851
890
  switch (event.type) {
@@ -868,12 +907,25 @@ var ConnectSDK = (() => {
868
907
  static relayToIframe(event) {
869
908
  var _a;
870
909
  const iframeWindow = (_a = this.iframe) == null ? void 0 : _a.contentWindow;
871
- if (!iframeWindow) {
872
- console.warn("[Benji Connect SDK] Skipping login auth relay, iframe not ready");
910
+ if (!this.isIframeReady || !iframeWindow) {
911
+ this.pendingPopupAuth.push(event);
873
912
  return;
874
913
  }
875
914
  iframeWindow.postMessage(event.data, this.connectOrigin);
876
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
+ }
877
929
  static relayToPopup(payload) {
878
930
  if (!this.loginPopup) {
879
931
  console.warn("[Benji Connect SDK] Skipping login auth relay, no login popup");
@@ -980,6 +1032,8 @@ var ConnectSDK = (() => {
980
1032
  _MessageRouter.loginPopup = null;
981
1033
  _MessageRouter.mode = 1 /* CONNECT */;
982
1034
  _MessageRouter.connectOrigin = "";
1035
+ _MessageRouter.isIframeReady = false;
1036
+ _MessageRouter.pendingPopupAuth = [];
983
1037
  _MessageRouter.errorEventHandler = (event) => {
984
1038
  return _MessageRouter.handleErrorMessage(event);
985
1039
  };
@@ -1009,6 +1063,7 @@ var ConnectSDK = (() => {
1009
1063
  };
1010
1064
  this.repositionListeners = null;
1011
1065
  this.loginPopup = null;
1066
+ this.hasLoginPopupUrl = false;
1012
1067
  this.popupClosedPollId = null;
1013
1068
  this.isClosing = false;
1014
1069
  /**
@@ -1018,6 +1073,10 @@ var ConnectSDK = (() => {
1018
1073
  * while the in-flight open() still registers listeners and mounts the iframe.
1019
1074
  */
1020
1075
  this.openAbortController = null;
1076
+ this.onIframeLoad = () => {
1077
+ if (this.isClosing || !this.iframe) return;
1078
+ MessageRouter.markIframeReady(this.iframe);
1079
+ };
1021
1080
  this.sdkConfig = config;
1022
1081
  if (!this.sdkConfig.environment) throw new Error("Environment is required");
1023
1082
  if (!this.sdkConfig.token) throw new Error("Connect token is required");
@@ -1025,10 +1084,15 @@ var ConnectSDK = (() => {
1025
1084
  this.sdkConfig.environment = environment;
1026
1085
  configureConfig(environment);
1027
1086
  }
1087
+ resolveAuthType() {
1088
+ var _a, _b;
1089
+ return (_b = (_a = this.sdkConfig.tokenData) == null ? void 0 : _a.authType) != null ? _b : this.sdkConfig.authType;
1090
+ }
1028
1091
  isLoginPopupOnly() {
1029
1092
  var _a;
1030
1093
  const mode = (_a = this.sdkConfig.mode) != null ? _a : 1 /* CONNECT */;
1031
- return mode === 5 /* LOGIN */ && this.sdkConfig.authType != null && this.sdkConfig.authType !== 0 /* DUMMY */;
1094
+ const authType = this.resolveAuthType();
1095
+ return mode === 5 /* LOGIN */ && authType != null && authType !== 0 /* DUMMY */;
1032
1096
  }
1033
1097
  isSessionActive() {
1034
1098
  return this.iframe != null || this.loginPopup != null;
@@ -1051,28 +1115,36 @@ var ConnectSDK = (() => {
1051
1115
  });
1052
1116
  }
1053
1117
  openLoginPopup() {
1054
- var _a, _b;
1118
+ var _a, _b, _c, _d;
1055
1119
  const position = computePopupScreenPosition(
1056
1120
  this.sdkConfig,
1057
1121
  this.popupLayoutState,
1058
1122
  { warnOnInvalidAnchor: true }
1059
1123
  );
1124
+ const partnerAuthLink = (_a = this.sdkConfig.tokenData) == null ? void 0 : _a.partnerAuthLink;
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";
1060
1128
  this.loginPopup = window.open(
1061
- "about:blank",
1129
+ popupUrl,
1062
1130
  "benji_partner_auth",
1063
1131
  buildLoginPopupFeatures(position)
1064
1132
  );
1065
1133
  if (!this.loginPopup) {
1066
1134
  console.warn("[Connect SDK] Failed to pre-open LOGIN partner auth popup");
1067
- (_b = (_a = this.sdkConfig).onError) == null ? void 0 : _b.call(
1068
- _a,
1135
+ (_d = (_c = this.sdkConfig).onError) == null ? void 0 : _d.call(
1136
+ _c,
1069
1137
  new Error("Popup blocked. Allow popups for this site and try again."),
1070
1138
  "400",
1071
1139
  { context: buildContext() }
1072
1140
  );
1073
1141
  return false;
1074
1142
  }
1075
- writeLoginPopupLoadingDocument(this.loginPopup);
1143
+ if (preparedLink) {
1144
+ this.hasLoginPopupUrl = true;
1145
+ } else {
1146
+ writeLoginPopupLoadingDocument(this.loginPopup);
1147
+ }
1076
1148
  this.startPopupClosedPoll();
1077
1149
  return true;
1078
1150
  }
@@ -1145,8 +1217,9 @@ var ConnectSDK = (() => {
1145
1217
  });
1146
1218
  }
1147
1219
  openUI() {
1148
- var _a;
1220
+ var _a, _b;
1149
1221
  if (this.iframe) return;
1222
+ MessageRouter.resetIframeReady();
1150
1223
  const headless = this.isLoginPopupOnly();
1151
1224
  this.container = document.createElement("div");
1152
1225
  if (headless) {
@@ -1164,6 +1237,8 @@ var ConnectSDK = (() => {
1164
1237
  url.searchParams.set("mode", String(this.sdkConfig.mode));
1165
1238
  }
1166
1239
  this.iframe = document.createElement("iframe");
1240
+ this.syncLoginContext((_a = this.sdkConfig.mode) != null ? _a : 1 /* CONNECT */);
1241
+ this.iframe.addEventListener("load", this.onIframeLoad);
1167
1242
  this.iframe.src = url.toString();
1168
1243
  this.container.appendChild(this.iframe);
1169
1244
  document.body.appendChild(this.container);
@@ -1194,9 +1269,9 @@ var ConnectSDK = (() => {
1194
1269
  }
1195
1270
  );
1196
1271
  this.container.addEventListener("click", (e) => {
1197
- var _a2, _b;
1272
+ var _a2, _b2;
1198
1273
  if (e.target === this.container) {
1199
- (_b = (_a2 = this.sdkConfig).onExit) == null ? void 0 : _b.call(_a2, {
1274
+ (_b2 = (_a2 = this.sdkConfig).onExit) == null ? void 0 : _b2.call(_a2, {
1200
1275
  context: buildContext(),
1201
1276
  trigger: "TAPPED_OUT_OF_BOUNDS" /* TAPPED_OUT_OF_BOUNDS */
1202
1277
  });
@@ -1204,15 +1279,17 @@ var ConnectSDK = (() => {
1204
1279
  }
1205
1280
  });
1206
1281
  }
1207
- this.syncLoginContext((_a = this.sdkConfig.mode) != null ? _a : 1 /* CONNECT */);
1282
+ this.syncLoginContext((_b = this.sdkConfig.mode) != null ? _b : 1 /* CONNECT */);
1208
1283
  }
1209
1284
  navigateLoginPopup(url) {
1285
+ if (this.hasLoginPopupUrl) return;
1210
1286
  if (!this.loginPopup || this.loginPopup.closed) {
1211
1287
  console.warn("[Connect SDK] Cannot navigate LOGIN popup \u2014 window unavailable");
1212
1288
  return;
1213
1289
  }
1214
1290
  try {
1215
1291
  this.loginPopup.location.href = url;
1292
+ this.hasLoginPopupUrl = true;
1216
1293
  } catch (error) {
1217
1294
  console.warn("[Connect SDK] Failed to navigate LOGIN popup", error);
1218
1295
  }
@@ -1227,6 +1304,7 @@ var ConnectSDK = (() => {
1227
1304
  console.warn("[Connect SDK] Error closing LOGIN popup", error);
1228
1305
  } finally {
1229
1306
  this.loginPopup = null;
1307
+ this.hasLoginPopupUrl = false;
1230
1308
  }
1231
1309
  }
1232
1310
  close() {
@@ -1244,6 +1322,7 @@ var ConnectSDK = (() => {
1244
1322
  this.repositionListeners = null;
1245
1323
  this.layoutState.lastValidAnchoredLayout = null;
1246
1324
  this.popupLayoutState.lastValidAnchoredLayout = null;
1325
+ this.iframe.removeEventListener("load", this.onIframeLoad);
1247
1326
  if ((_b = this.container) == null ? void 0 : _b.parentNode) {
1248
1327
  this.container.parentNode.removeChild(this.container);
1249
1328
  }