@usero/sdk 1.1.1 → 1.1.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/dist/react.cjs CHANGED
@@ -163,7 +163,11 @@ function getGradientEnd(color) {
163
163
 
164
164
  // src/identity.ts
165
165
  var ANON_STORAGE_KEY = "usero:anonymous-id";
166
+ var SDK_SESSION_STORAGE_KEY = "usero:session-replay:sdk-session-id";
166
167
  var cachedAnonymousId = null;
168
+ var cachedSdkSessionId = null;
169
+ var currentUserId = null;
170
+ var replayStartMs = null;
167
171
  var lastIdentifyFingerprint = null;
168
172
  function generateRandomId() {
169
173
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
@@ -194,6 +198,21 @@ function safeWriteLocalStorage(key, value) {
194
198
  } catch {
195
199
  }
196
200
  }
201
+ function safeReadSessionStorage(key) {
202
+ if (typeof window === "undefined") return null;
203
+ try {
204
+ return window.sessionStorage?.getItem(key) ?? null;
205
+ } catch {
206
+ return null;
207
+ }
208
+ }
209
+ function safeWriteSessionStorage(key, value) {
210
+ if (typeof window === "undefined") return;
211
+ try {
212
+ window.sessionStorage?.setItem(key, value);
213
+ } catch {
214
+ }
215
+ }
197
216
  function getOrMintAnonymousId() {
198
217
  if (cachedAnonymousId) return cachedAnonymousId;
199
218
  const existing = safeReadLocalStorage(ANON_STORAGE_KEY);
@@ -211,8 +230,30 @@ function rotateAnonymousId() {
211
230
  cachedAnonymousId = id;
212
231
  safeWriteLocalStorage(ANON_STORAGE_KEY, id);
213
232
  lastIdentifyFingerprint = null;
233
+ currentUserId = null;
234
+ return id;
235
+ }
236
+ function getOrMintSdkSessionId() {
237
+ if (cachedSdkSessionId) return cachedSdkSessionId;
238
+ const existing = safeReadSessionStorage(SDK_SESSION_STORAGE_KEY);
239
+ if (existing && /^[a-z0-9-]{8,}$/i.test(existing)) {
240
+ cachedSdkSessionId = existing;
241
+ return existing;
242
+ }
243
+ const id = generateRandomId();
244
+ safeWriteSessionStorage(SDK_SESSION_STORAGE_KEY, id);
245
+ cachedSdkSessionId = id;
214
246
  return id;
215
247
  }
248
+ function getCurrentUserId() {
249
+ return currentUserId;
250
+ }
251
+ function publishReplayStartMs(epochMs) {
252
+ if (replayStartMs === null) replayStartMs = epochMs;
253
+ }
254
+ function getReplayStartMs() {
255
+ return replayStartMs;
256
+ }
216
257
  function fingerprintUser(anonymousId, user) {
217
258
  const traits = user.traits ?? {};
218
259
  const keys = Object.keys(traits).sort();
@@ -221,6 +262,7 @@ function fingerprintUser(anonymousId, user) {
221
262
  }
222
263
  async function identifyIfChanged(transport, user) {
223
264
  const anonymousId = getOrMintAnonymousId();
265
+ currentUserId = user.id;
224
266
  const fp = fingerprintUser(anonymousId, user);
225
267
  if (fp === lastIdentifyFingerprint) return false;
226
268
  const url = `${transport.apiUrl.replace(/\/$/, "")}/api/identify`;
@@ -909,7 +951,15 @@ function initUseroFeedbackWidget(props) {
909
951
  } else {
910
952
  resolveAndApplyGetUser();
911
953
  }
912
- }
954
+ },
955
+ // Core-owned cross-cutting identity. Every plugin reads the same
956
+ // source of truth in identity.ts, so user-test and session-replay
957
+ // agree on the per-tab sdkSessionId without importing each other.
958
+ getSdkSessionId: () => getOrMintSdkSessionId(),
959
+ getAnonymousId: () => getOrMintAnonymousId(),
960
+ getUserId: () => getCurrentUserId(),
961
+ getReplayStartMs: () => getReplayStartMs(),
962
+ publishReplayStartMs: (epochMs) => publishReplayStartMs(epochMs)
913
963
  };
914
964
  pluginContexts.set(plugin.name, ctx);
915
965
  if (plugin.onInit) {
@@ -991,21 +1041,22 @@ function initUseroFeedbackWidget(props) {
991
1041
  screenshotError = null;
992
1042
  if (!file.type.startsWith("image/")) {
993
1043
  screenshotError = "Image files only";
994
- render();
1044
+ updateUploadExtras();
995
1045
  return;
996
1046
  }
997
1047
  if (file.size > MAX_SCREENSHOT_BYTES) {
998
1048
  screenshotError = "Max 10MB";
999
- render();
1049
+ updateUploadExtras();
1000
1050
  return;
1001
1051
  }
1002
1052
  if (screenshots.length >= MAX_SCREENSHOTS) {
1003
1053
  screenshotError = `Max ${MAX_SCREENSHOTS} screenshots`;
1004
- render();
1054
+ updateUploadExtras();
1005
1055
  return;
1006
1056
  }
1007
1057
  isUploadingScreenshot = true;
1008
- render();
1058
+ updateUploadButton();
1059
+ updateUploadExtras();
1009
1060
  try {
1010
1061
  const uploaded = await apiClient.uploadScreenshot(file, clientId);
1011
1062
  screenshots = [...screenshots, uploaded];
@@ -1013,12 +1064,14 @@ function initUseroFeedbackWidget(props) {
1013
1064
  screenshotError = err instanceof Error ? err.message : "Upload failed";
1014
1065
  } finally {
1015
1066
  isUploadingScreenshot = false;
1016
- render();
1067
+ updateUploadButton();
1068
+ updateUploadExtras();
1017
1069
  }
1018
1070
  }
1019
1071
  function removeScreenshot(index) {
1020
1072
  screenshots = screenshots.filter((_, i) => i !== index);
1021
- render();
1073
+ updateUploadButton();
1074
+ updateUploadExtras();
1022
1075
  }
1023
1076
  function close() {
1024
1077
  if (!isOpen) return;
@@ -1026,6 +1079,57 @@ function initUseroFeedbackWidget(props) {
1026
1079
  onClose?.();
1027
1080
  render();
1028
1081
  }
1082
+ function buildUploadButtonInner() {
1083
+ return isUploadingScreenshot ? '<span class="fb-ups"></span> Uploading...' : "\u{1F4F7} Add screenshot";
1084
+ }
1085
+ function buildUploadButtonHtml() {
1086
+ const atMax = screenshots.length >= MAX_SCREENSHOTS;
1087
+ const btnDisabled = isUploadingScreenshot || atMax;
1088
+ return `
1089
+ <input type="file" accept="image/*" data-role="screenshot-input" style="display:none;" aria-label="Choose screenshot" />
1090
+ <button type="button" class="fb-upb ${btnDisabled ? "fb-upb--dis" : ""}" data-role="screenshot-pick" ${btnDisabled ? "disabled" : ""} style="border:1px solid ${theme.border};color:${theme.text};">
1091
+ ${buildUploadButtonInner()}
1092
+ </button>
1093
+ `;
1094
+ }
1095
+ function buildUploadExtrasHtml() {
1096
+ const atMax = screenshots.length >= MAX_SCREENSHOTS;
1097
+ const previewsHtml = screenshots.map(
1098
+ (shot, i) => `
1099
+ <div class="fb-sp">
1100
+ <img src="${escapeHtml(shot.url)}" alt="Screenshot ${i + 1}" class="fb-si" />
1101
+ <button type="button" class="fb-sr" data-role="screenshot-remove" data-index="${i}" aria-label="Remove screenshot">\u2715</button>
1102
+ </div>
1103
+ `
1104
+ ).join("");
1105
+ const errorHtml = screenshotError ? `<div class="fb-upe">\u26A0 ${escapeHtml(screenshotError)}</div>` : "";
1106
+ const limitHtml = atMax ? `<div class="fb-sl">Max ${MAX_SCREENSHOTS}</div>` : "";
1107
+ return screenshotError || screenshots.length > 0 || atMax ? `<div class="fb-up-extras">${errorHtml}${screenshots.length > 0 ? `<div class="fb-ss">${previewsHtml}</div>` : ""}${limitHtml}</div>` : "";
1108
+ }
1109
+ function updateUploadButton() {
1110
+ if (!showScreenshotOption) return;
1111
+ const pickBtn = panelEl.querySelector(
1112
+ 'button[data-role="screenshot-pick"]'
1113
+ );
1114
+ if (!pickBtn) return;
1115
+ const atMax = screenshots.length >= MAX_SCREENSHOTS;
1116
+ const btnDisabled = isUploadingScreenshot || atMax;
1117
+ pickBtn.disabled = btnDisabled;
1118
+ pickBtn.classList.toggle("fb-upb--dis", btnDisabled);
1119
+ pickBtn.innerHTML = buildUploadButtonInner();
1120
+ }
1121
+ function updateUploadExtras() {
1122
+ if (!showScreenshotOption) return;
1123
+ const container = panelEl.querySelector(".fb-up");
1124
+ if (!container) return;
1125
+ container.innerHTML = buildUploadExtrasHtml();
1126
+ container.querySelectorAll('button[data-role="screenshot-remove"]').forEach((btn) => {
1127
+ btn.addEventListener("click", () => {
1128
+ const idx = Number(btn.dataset.index);
1129
+ if (Number.isInteger(idx)) removeScreenshot(idx);
1130
+ });
1131
+ });
1132
+ }
1029
1133
  async function submitForm() {
1030
1134
  if (isSubmitting) return;
1031
1135
  isSubmitting = true;
@@ -1143,30 +1247,8 @@ function initUseroFeedbackWidget(props) {
1143
1247
  `;
1144
1248
  }).join("");
1145
1249
  const messageHtml = submitMessage ? `<div class="fb-msg fb-msg--header ${submitMessage.type === "success" ? "fb-msg--ok" : "fb-msg--err"}">${submitMessage.type === "success" ? "\u2713" : "\u26A0"} ${escapeHtml(submitMessage.text)}</div>` : "";
1146
- const uploadBtnHtml = showScreenshotOption ? (() => {
1147
- const atMax = screenshots.length >= MAX_SCREENSHOTS;
1148
- const btnDisabled = isUploadingScreenshot || atMax;
1149
- return `
1150
- <input type="file" accept="image/*" data-role="screenshot-input" style="display:none;" aria-label="Choose screenshot" />
1151
- <button type="button" class="fb-upb ${btnDisabled ? "fb-upb--dis" : ""}" data-role="screenshot-pick" ${btnDisabled ? "disabled" : ""} style="border:1px solid ${theme.border};color:${theme.text};">
1152
- ${isUploadingScreenshot ? '<span class="fb-ups"></span> Uploading...' : "\u{1F4F7} Add screenshot"}
1153
- </button>
1154
- `;
1155
- })() : "";
1156
- const uploadExtrasHtml = showScreenshotOption ? (() => {
1157
- const atMax = screenshots.length >= MAX_SCREENSHOTS;
1158
- const previewsHtml = screenshots.map(
1159
- (shot, i) => `
1160
- <div class="fb-sp">
1161
- <img src="${escapeHtml(shot.url)}" alt="Screenshot ${i + 1}" class="fb-si" />
1162
- <button type="button" class="fb-sr" data-role="screenshot-remove" data-index="${i}" aria-label="Remove screenshot">\u2715</button>
1163
- </div>
1164
- `
1165
- ).join("");
1166
- const errorHtml = screenshotError ? `<div class="fb-upe">\u26A0 ${escapeHtml(screenshotError)}</div>` : "";
1167
- const limitHtml = atMax ? `<div class="fb-sl">Max ${MAX_SCREENSHOTS}</div>` : "";
1168
- return screenshotError || screenshots.length > 0 || atMax ? `<div class="fb-up-extras">${errorHtml}${screenshots.length > 0 ? `<div class="fb-ss">${previewsHtml}</div>` : ""}${limitHtml}</div>` : "";
1169
- })() : "";
1250
+ const uploadBtnHtml = showScreenshotOption ? buildUploadButtonHtml() : "";
1251
+ const uploadExtrasHtml = showScreenshotOption ? buildUploadExtrasHtml() : "";
1170
1252
  const emailBlockHtml = showEmailOption ? `
1171
1253
  <div class="fb-email">
1172
1254
  <label class="fb-email-lbl" style="color:${theme.text}">
@@ -1192,7 +1274,7 @@ function initUseroFeedbackWidget(props) {
1192
1274
  ${uploadBtnHtml}
1193
1275
  <div class="fb-charcount${lowChars ? " fb-charcount--low" : ""}" data-role="charcount" style="color:${lowChars ? "#dc2626" : theme.text};opacity:${lowChars ? 1 : 0.6};">${remaining} chars remaining</div>
1194
1276
  </div>
1195
- ${uploadExtrasHtml ? `<div class="fb-up">${uploadExtrasHtml}</div>` : ""}
1277
+ ${showScreenshotOption ? `<div class="fb-up">${uploadExtrasHtml}</div>` : ""}
1196
1278
  ${emailBlockHtml}
1197
1279
  <button class="fb-sub ${submitDisabled ? "fb-sub--dis" : ""}" type="submit" aria-label="Submit" ${submitDisabled ? "disabled" : ""} style="${submitStyle}">
1198
1280
  ${isSubmitting ? '<span class="fb-spin"></span>' : ""}
@@ -1289,10 +1371,16 @@ function initUseroFeedbackWidget(props) {
1289
1371
  if (isOpen) close();
1290
1372
  else open();
1291
1373
  });
1292
- backdropEl.addEventListener("click", close);
1374
+ backdropEl.addEventListener("click", () => {
1375
+ if (isUploadingScreenshot || isSubmitting) return;
1376
+ close();
1377
+ });
1293
1378
  const onKeyDown = (e) => {
1294
1379
  if (!isOpen) return;
1295
- if (e.key === "Escape") close();
1380
+ if (e.key === "Escape") {
1381
+ if (isUploadingScreenshot || isSubmitting) return;
1382
+ close();
1383
+ }
1296
1384
  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
1297
1385
  e.preventDefault();
1298
1386
  void submitForm();