@usero/sdk 0.3.4 → 0.4.0

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