@usero/sdk 0.3.4 → 0.4.1

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
@@ -161,6 +161,111 @@ function getGradientEnd(color) {
161
161
  return `#${[shiftedR, shiftedG, shiftedB].map((x) => x.toString(16).padStart(2, "0")).join("")}`;
162
162
  }
163
163
 
164
+ // src/identity.ts
165
+ var ANON_STORAGE_KEY = "usero:anonymous-id";
166
+ var cachedAnonymousId = null;
167
+ var lastIdentifyFingerprint = null;
168
+ function generateRandomId() {
169
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
170
+ return crypto.randomUUID();
171
+ }
172
+ const bytes = new Uint8Array(16);
173
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
174
+ crypto.getRandomValues(bytes);
175
+ } else {
176
+ for (let i = 0; i < bytes.length; i += 1) bytes[i] = Math.floor(Math.random() * 256);
177
+ }
178
+ let out = "";
179
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
180
+ return out;
181
+ }
182
+ function safeReadLocalStorage(key) {
183
+ if (typeof window === "undefined") return null;
184
+ try {
185
+ return window.localStorage?.getItem(key) ?? null;
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+ function safeWriteLocalStorage(key, value) {
191
+ if (typeof window === "undefined") return;
192
+ try {
193
+ window.localStorage?.setItem(key, value);
194
+ } catch {
195
+ }
196
+ }
197
+ function getOrMintAnonymousId() {
198
+ if (cachedAnonymousId) return cachedAnonymousId;
199
+ const existing = safeReadLocalStorage(ANON_STORAGE_KEY);
200
+ if (existing && /^[a-z0-9-]{8,}$/i.test(existing)) {
201
+ cachedAnonymousId = existing;
202
+ return existing;
203
+ }
204
+ const id = generateRandomId();
205
+ safeWriteLocalStorage(ANON_STORAGE_KEY, id);
206
+ cachedAnonymousId = id;
207
+ return id;
208
+ }
209
+ function rotateAnonymousId() {
210
+ const id = generateRandomId();
211
+ cachedAnonymousId = id;
212
+ safeWriteLocalStorage(ANON_STORAGE_KEY, id);
213
+ lastIdentifyFingerprint = null;
214
+ return id;
215
+ }
216
+ function fingerprintUser(anonymousId, user) {
217
+ const traits = user.traits ?? {};
218
+ const keys = Object.keys(traits).sort();
219
+ const canonical = keys.map((k) => [k, traits[k] ?? null]);
220
+ return JSON.stringify([anonymousId, user.id, user.email ?? null, user.displayName ?? null, canonical]);
221
+ }
222
+ async function identifyIfChanged(transport, user) {
223
+ const anonymousId = getOrMintAnonymousId();
224
+ const fp = fingerprintUser(anonymousId, user);
225
+ if (fp === lastIdentifyFingerprint) return false;
226
+ const url = `${transport.apiUrl.replace(/\/$/, "")}/api/identify`;
227
+ const body = JSON.stringify({
228
+ clientId: transport.clientId,
229
+ anonymousId,
230
+ externalUserId: user.id,
231
+ email: user.email,
232
+ displayName: user.displayName,
233
+ traits: user.traits
234
+ });
235
+ if (typeof document !== "undefined" && document.visibilityState === "hidden" && typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
236
+ try {
237
+ const blob = new Blob([body], { type: "application/json" });
238
+ if (navigator.sendBeacon(url, blob)) {
239
+ lastIdentifyFingerprint = fp;
240
+ return true;
241
+ }
242
+ } catch {
243
+ }
244
+ }
245
+ try {
246
+ const res = await fetch(url, {
247
+ method: "POST",
248
+ headers: { "Content-Type": "application/json" },
249
+ body,
250
+ // keepalive lets the request survive a tab-close mid-flight on
251
+ // browsers that support it; sendBeacon above is the primary path.
252
+ keepalive: true
253
+ });
254
+ if (!res.ok) return true;
255
+ try {
256
+ const json = await res.json();
257
+ if (json && json.accepted === true) lastIdentifyFingerprint = fp;
258
+ } catch {
259
+ }
260
+ return true;
261
+ } catch {
262
+ return false;
263
+ }
264
+ }
265
+ function handleLogout() {
266
+ rotateAnonymousId();
267
+ }
268
+
164
269
  // src/plugin.ts
165
270
  function createPluginLogger(name) {
166
271
  const prefix = `[usero:${name}]`;
@@ -706,7 +811,9 @@ function initUseroFeedbackWidget(props) {
706
811
  },
707
812
  update: () => {
708
813
  },
709
- whenReady: () => Promise.resolve()
814
+ whenReady: () => Promise.resolve(),
815
+ identify: () => {
816
+ }
710
817
  };
711
818
  }
712
819
  const { clientId, baseUrl } = props;
@@ -722,7 +829,9 @@ function initUseroFeedbackWidget(props) {
722
829
  },
723
830
  update: () => {
724
831
  },
725
- whenReady: () => Promise.resolve()
832
+ whenReady: () => Promise.resolve(),
833
+ identify: () => {
834
+ }
726
835
  };
727
836
  }
728
837
  let position = props.position ?? "right";
@@ -738,7 +847,44 @@ function initUseroFeedbackWidget(props) {
738
847
  let onError = props.onError;
739
848
  let onOpen = props.onOpen;
740
849
  let onClose = props.onClose;
850
+ let getUserFn = props.getUser;
851
+ let currentUserProp = props.user;
741
852
  const apiClient = new FeedbackApiClient(baseUrl);
853
+ const identifyTransport = { apiUrl: baseUrl ?? DEFAULT_API_URL, clientId };
854
+ let lastUserId = null;
855
+ let lastTraitsRef;
856
+ let lastEmail;
857
+ let lastDisplayName;
858
+ function applyResolvedUser(user) {
859
+ const next = user ?? null;
860
+ if (next) {
861
+ const unchanged = next.id === lastUserId && next.traits === lastTraitsRef && next.email === lastEmail && next.displayName === lastDisplayName;
862
+ if (unchanged) return;
863
+ void identifyIfChanged(identifyTransport, next);
864
+ lastUserId = next.id;
865
+ lastTraitsRef = next.traits;
866
+ lastEmail = next.email;
867
+ lastDisplayName = next.displayName;
868
+ } else if (lastUserId !== null) {
869
+ handleLogout();
870
+ lastUserId = null;
871
+ lastTraitsRef = void 0;
872
+ lastEmail = void 0;
873
+ lastDisplayName = void 0;
874
+ }
875
+ }
876
+ function resolveAndApplyGetUser() {
877
+ if (!getUserFn) return;
878
+ try {
879
+ applyResolvedUser(getUserFn() ?? null);
880
+ } catch {
881
+ }
882
+ }
883
+ if (props.user !== void 0) {
884
+ applyResolvedUser(props.user);
885
+ } else if (getUserFn) {
886
+ resolveAndApplyGetUser();
887
+ }
742
888
  const pluginList = props.plugins ?? [];
743
889
  const pluginStores = /* @__PURE__ */ new Map();
744
890
  const pluginContexts = /* @__PURE__ */ new Map();
@@ -751,6 +897,18 @@ function initUseroFeedbackWidget(props) {
751
897
  getStore: () => pluginStores.get(plugin.name),
752
898
  setStore: (value) => {
753
899
  pluginStores.set(plugin.name, value);
900
+ },
901
+ // Expose the same user-resolution path the widget uses, so plugins
902
+ // (e.g. session-replay for replay-only installs that never open the
903
+ // widget) can re-poll user state at their own boundaries. Prefers
904
+ // the imperative `user` prop when set, falls back to `getUser`.
905
+ resolveUser: () => {
906
+ if (destroyed) return;
907
+ if (currentUserProp !== void 0) {
908
+ applyResolvedUser(currentUserProp);
909
+ } else {
910
+ resolveAndApplyGetUser();
911
+ }
754
912
  }
755
913
  };
756
914
  pluginContexts.set(plugin.name, ctx);
@@ -784,6 +942,9 @@ function initUseroFeedbackWidget(props) {
784
942
  host.style.cssText = "all: initial;";
785
943
  document.body.appendChild(host);
786
944
  const root = host.attachShadow({ mode: "open" });
945
+ function pollGetUser() {
946
+ resolveAndApplyGetUser();
947
+ }
787
948
  function notifyShadowUpdate(reason) {
788
949
  try {
789
950
  window.dispatchEvent(
@@ -819,6 +980,7 @@ function initUseroFeedbackWidget(props) {
819
980
  screenshotError = null;
820
981
  isUploadingScreenshot = false;
821
982
  apiClient.ping();
983
+ pollGetUser();
822
984
  onOpen?.();
823
985
  render();
824
986
  notifyShadowUpdate("panel-open");
@@ -1176,6 +1338,11 @@ function initUseroFeedbackWidget(props) {
1176
1338
  open,
1177
1339
  close,
1178
1340
  whenReady: () => readyPromise,
1341
+ identify: (user) => {
1342
+ if (destroyed) return;
1343
+ currentUserProp = user;
1344
+ applyResolvedUser(user);
1345
+ },
1179
1346
  update: (next) => {
1180
1347
  if (destroyed) return;
1181
1348
  let needsRender = false;
@@ -1212,6 +1379,11 @@ function initUseroFeedbackWidget(props) {
1212
1379
  if ("onError" in next) onError = next.onError;
1213
1380
  if ("onOpen" in next) onOpen = next.onOpen;
1214
1381
  if ("onClose" in next) onClose = next.onClose;
1382
+ if ("getUser" in next) getUserFn = next.getUser;
1383
+ if ("user" in next) {
1384
+ currentUserProp = next.user;
1385
+ applyResolvedUser(next.user);
1386
+ }
1215
1387
  if (needsRender) render();
1216
1388
  }
1217
1389
  };
@@ -1276,6 +1448,16 @@ function UseroFeedbackWidget(props) {
1276
1448
  props.environment,
1277
1449
  metadataJson
1278
1450
  ]);
1451
+ const userId = props.user?.id ?? null;
1452
+ const userEmail = props.user?.email ?? null;
1453
+ const userDisplayName = props.user?.displayName ?? null;
1454
+ const userTraitsJson = JSON.stringify(props.user?.traits ?? null);
1455
+ const userIsNull = props.user === null;
1456
+ react.useEffect(() => {
1457
+ const handle = handleRef.current;
1458
+ if (!handle) return;
1459
+ if (props.user !== void 0) handle.update({ user: props.user });
1460
+ }, [userId, userEmail, userDisplayName, userTraitsJson, userIsNull]);
1279
1461
  return null;
1280
1462
  }
1281
1463