@cimplify/sdk 0.92.0 → 0.94.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.
Files changed (59) hide show
  1. package/dist/advanced.d.mts +3 -3
  2. package/dist/advanced.d.ts +3 -3
  3. package/dist/advanced.js +20 -20
  4. package/dist/advanced.mjs +1 -1
  5. package/dist/{chunk-OAHSF542.js → chunk-6Y7KC5YZ.js} +11 -0
  6. package/dist/{chunk-3MBMIEWT.js → chunk-AWNWP5J3.js} +4 -4
  7. package/dist/{chunk-3XD2FVP4.mjs → chunk-FKARGZK3.mjs} +195 -60
  8. package/dist/{chunk-DCRN67NO.js → chunk-QLGZIBRO.js} +237 -102
  9. package/dist/{chunk-54H2YYIW.mjs → chunk-QXWX7CD2.mjs} +2 -2
  10. package/dist/{chunk-IVJEUGWF.js → chunk-VEOAMBEX.js} +1 -3
  11. package/dist/{chunk-T7LDBZZZ.mjs → chunk-VUSHG3HA.mjs} +1 -3
  12. package/dist/{chunk-PYBFTMHI.mjs → chunk-X36FZZT6.mjs} +11 -0
  13. package/dist/{client-CeW3acve.d.ts → client-BTL9d_nN.d.mts} +29 -2
  14. package/dist/{client-BQbSKlHS.d.mts → client-BX30FAWZ.d.ts} +29 -2
  15. package/dist/{client-Bw4HtLP1.d.mts → client-Bhe2TnFW.d.mts} +2 -2
  16. package/dist/{client-Otzv_rgP.d.ts → client-DrkuEK_I.d.ts} +2 -2
  17. package/dist/{index-CmIboQjl.d.mts → index-BuNjyfaV.d.mts} +3 -3
  18. package/dist/{index-DgoRwz8l.d.ts → index-D-w5h82t.d.ts} +3 -3
  19. package/dist/index.d.mts +7 -7
  20. package/dist/index.d.ts +7 -7
  21. package/dist/index.js +69 -69
  22. package/dist/index.mjs +3 -3
  23. package/dist/mock/cli.mjs +11 -0
  24. package/dist/mock/library.js +11 -0
  25. package/dist/mock/library.mjs +11 -0
  26. package/dist/mock/msw.js +11 -0
  27. package/dist/mock/msw.mjs +11 -0
  28. package/dist/{payment-Cq8FT1B6.d.ts → payment-By0JidTe.d.ts} +1 -1
  29. package/dist/{payment-BFtLqiG2.d.mts → payment-pKPpaORI.d.mts} +1 -1
  30. package/dist/{price-DaFXT-Lr.d.ts → price-23RNXftz.d.ts} +1 -1
  31. package/dist/{price-DleDWk_s.d.mts → price-BvRtp6ak.d.mts} +1 -1
  32. package/dist/{product-bZ8G5Z8D.d.mts → product-O8-wcHSe.d.mts} +11 -0
  33. package/dist/{product-bZ8G5Z8D.d.ts → product-O8-wcHSe.d.ts} +11 -0
  34. package/dist/react.d.mts +20 -6
  35. package/dist/react.d.ts +20 -6
  36. package/dist/react.js +208 -76
  37. package/dist/react.mjs +190 -59
  38. package/dist/{server-D-iK-bNt.d.mts → server-BN9ZCAKT.d.mts} +5 -1
  39. package/dist/{server-DwsY9hlX.d.ts → server-DDGbfW_S.d.ts} +5 -1
  40. package/dist/server.d.mts +4 -4
  41. package/dist/server.d.ts +4 -4
  42. package/dist/server.js +7 -7
  43. package/dist/server.mjs +2 -2
  44. package/dist/testing/msw.d.mts +2 -2
  45. package/dist/testing/msw.d.ts +2 -2
  46. package/dist/testing/msw.js +2 -2
  47. package/dist/testing/msw.mjs +1 -1
  48. package/dist/testing/suite.d.mts +6 -6
  49. package/dist/testing/suite.d.ts +6 -6
  50. package/dist/testing/suite.js +23 -23
  51. package/dist/testing/suite.mjs +4 -4
  52. package/dist/testing.d.mts +5 -5
  53. package/dist/testing.d.ts +5 -5
  54. package/dist/testing.js +79 -79
  55. package/dist/testing.mjs +5 -5
  56. package/dist/utils.d.mts +4 -4
  57. package/dist/utils.d.ts +4 -4
  58. package/package.json +1 -1
  59. package/registry/chat-widget.json +1 -1
package/dist/react.js CHANGED
@@ -3,8 +3,8 @@
3
3
 
4
4
  var chunkHKN3YG76_js = require('./chunk-HKN3YG76.js');
5
5
  var chunkXREIZ56K_js = require('./chunk-XREIZ56K.js');
6
- var chunkDCRN67NO_js = require('./chunk-DCRN67NO.js');
7
- var chunkIVJEUGWF_js = require('./chunk-IVJEUGWF.js');
6
+ var chunkQLGZIBRO_js = require('./chunk-QLGZIBRO.js');
7
+ var chunkVEOAMBEX_js = require('./chunk-VEOAMBEX.js');
8
8
  var chunkPTPOQVXS_js = require('./chunk-PTPOQVXS.js');
9
9
  var chunkAETVRBKI_js = require('./chunk-AETVRBKI.js');
10
10
  require('./chunk-OWW5GUSB.js');
@@ -806,7 +806,7 @@ async function syncFromServer(entry) {
806
806
  return next;
807
807
  }
808
808
  function emitMutationError(entry, operation, opId, idempotencyKey, error) {
809
- entry.client.events.emit(chunkDCRN67NO_js.MUTATION_ERROR, {
809
+ entry.client.events.emit(chunkQLGZIBRO_js.MUTATION_ERROR, {
810
810
  service: "cart",
811
811
  operation,
812
812
  opId,
@@ -846,10 +846,10 @@ function getOrCreateStore(params) {
846
846
  syncTail: Promise.resolve(),
847
847
  failedOpRemovers: /* @__PURE__ */ new Set()
848
848
  };
849
- client.events.on(chunkDCRN67NO_js.SESSION_CLAIMED, () => {
849
+ client.events.on(chunkQLGZIBRO_js.SESSION_CLAIMED, () => {
850
850
  if (entry.initialized) void syncFromServer(entry);
851
851
  });
852
- client.events.on(chunkDCRN67NO_js.SESSION_CLEARED, () => {
852
+ client.events.on(chunkQLGZIBRO_js.SESSION_CLEARED, () => {
853
853
  if (entry.initialized) void syncFromServer(entry);
854
854
  });
855
855
  cartStores.set(storeKey, entry);
@@ -1137,6 +1137,37 @@ function preconnectOrigin(url, crossOrigin) {
1137
1137
  if (crossOrigin) link.crossOrigin = crossOrigin;
1138
1138
  document.head.appendChild(link);
1139
1139
  }
1140
+ function usePageViewTracking(client, enabled) {
1141
+ React14.useEffect(() => {
1142
+ if (!enabled || typeof window === "undefined") return;
1143
+ let lastPath = null;
1144
+ const record = () => {
1145
+ const path = window.location.pathname;
1146
+ if (path === lastPath) return;
1147
+ lastPath = path;
1148
+ try {
1149
+ client.activity?.trackPageView({ pagePath: path });
1150
+ } catch {
1151
+ }
1152
+ };
1153
+ record();
1154
+ const { pushState, replaceState } = window.history;
1155
+ window.history.pushState = function(...args) {
1156
+ pushState.apply(this, args);
1157
+ record();
1158
+ };
1159
+ window.history.replaceState = function(...args) {
1160
+ replaceState.apply(this, args);
1161
+ record();
1162
+ };
1163
+ window.addEventListener("popstate", record);
1164
+ return () => {
1165
+ window.history.pushState = pushState;
1166
+ window.history.replaceState = replaceState;
1167
+ window.removeEventListener("popstate", record);
1168
+ };
1169
+ }, [client, enabled]);
1170
+ }
1140
1171
  var LOCATION_STORAGE_KEY = "cimplify_location_id";
1141
1172
  var DISPLAY_CURRENCY_STORAGE_KEY = "cimplify_display_currency";
1142
1173
  var FX_REFRESH_INTERVAL = 12e4;
@@ -1145,7 +1176,7 @@ var DEFAULT_COUNTRY = "US";
1145
1176
  function createDefaultClient() {
1146
1177
  const processRef = globalThis.process;
1147
1178
  const envPublicKey = processRef?.env?.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY || "";
1148
- return chunkDCRN67NO_js.createCimplifyClient({ publicKey: envPublicKey });
1179
+ return chunkQLGZIBRO_js.createCimplifyClient({ publicKey: envPublicKey });
1149
1180
  }
1150
1181
  function getStoredLocationId() {
1151
1182
  if (typeof window === "undefined" || !window.localStorage) {
@@ -1217,9 +1248,11 @@ function CimplifyProvider({
1217
1248
  initialLocations,
1218
1249
  optimistic = true,
1219
1250
  prefetch,
1220
- linkComponent
1251
+ linkComponent,
1252
+ trackPageViews = true
1221
1253
  }) {
1222
1254
  const resolvedClient = React14.useMemo(() => client ?? createDefaultClient(), [client]);
1255
+ usePageViewTracking(resolvedClient, trackPageViews);
1223
1256
  const resolvedPrefetch = React14.useMemo(
1224
1257
  () => ({
1225
1258
  mode: prefetch?.mode ?? DEFAULT_PREFETCH_OPTIONS.mode,
@@ -3457,7 +3490,7 @@ function CimplifySignInButton({
3457
3490
  const onClick = async () => {
3458
3491
  if (busy || disabled || !signInOpts.clientId) return;
3459
3492
  setBusy(true);
3460
- await chunkIVJEUGWF_js.startSignIn({
3493
+ await chunkVEOAMBEX_js.startSignIn({
3461
3494
  ...signInOpts,
3462
3495
  onSuccess: () => {
3463
3496
  setBusy(false);
@@ -5913,6 +5946,20 @@ var IDLE_TIMEOUT_MS = 6e4;
5913
5946
  var WS_MAX_RECONNECT_ATTEMPTS = 10;
5914
5947
  var WS_PATH = "/api/v1/support/conversation/ws";
5915
5948
  var AWAIT_REPLY_TIMEOUT_MS = 3e4;
5949
+ function mergeMessages(prev, incoming) {
5950
+ const ids = new Set(prev.map((m) => m.id));
5951
+ const fresh = incoming.filter((m) => !ids.has(m.id));
5952
+ if (fresh.length === 0) return prev;
5953
+ const merged = [...prev, ...fresh].sort(
5954
+ (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
5955
+ );
5956
+ const confirmed = new Set(
5957
+ merged.filter((m) => !m.id.startsWith("opt_") && m.client_id).map((m) => m.client_id)
5958
+ );
5959
+ return merged.filter(
5960
+ (m) => !(m.id.startsWith("opt_") && m.client_id && confirmed.has(m.client_id))
5961
+ );
5962
+ }
5916
5963
  function useChat(options = {}) {
5917
5964
  const context = useOptionalCimplifyClient();
5918
5965
  const client = options.client ?? context?.client;
@@ -5965,18 +6012,22 @@ function useChat(options = {}) {
5965
6012
  if (incoming.some((m) => m.sender_type === "bot" || m.sender_type === "agent")) {
5966
6013
  stopAwaitingReply();
5967
6014
  }
5968
- setMessages((prev) => {
5969
- const ids = new Set(prev.map((m) => m.id));
5970
- const fresh = incoming.filter((m) => !ids.has(m.id));
5971
- return fresh.length > 0 ? [...prev, ...fresh] : prev;
5972
- });
6015
+ if (incoming.length > 0) lastActivityRef.current = Date.now();
6016
+ setMessages((prev) => mergeMessages(prev, incoming));
5973
6017
  },
5974
6018
  [stopAwaitingReply]
5975
6019
  );
6020
+ const lastConfirmedMessage = React14.useCallback(() => {
6021
+ const list = messagesRef.current;
6022
+ for (let i = list.length - 1; i >= 0; i--) {
6023
+ if (!list[i].id.startsWith("opt_")) return list[i];
6024
+ }
6025
+ return void 0;
6026
+ }, []);
5976
6027
  const fetchNewMessages = React14.useCallback(async () => {
5977
6028
  const c = clientRef.current;
5978
6029
  if (!c) return;
5979
- const last = messagesRef.current[messagesRef.current.length - 1];
6030
+ const last = lastConfirmedMessage();
5980
6031
  const result = await c.support.getMessages({
5981
6032
  after: last?.created_at,
5982
6033
  limit: 50
@@ -5984,16 +6035,23 @@ function useChat(options = {}) {
5984
6035
  if (result.ok && result.value.length > 0) {
5985
6036
  appendMessages(result.value);
5986
6037
  }
5987
- }, [appendMessages]);
5988
- const pollMessages = React14.useCallback(async () => {
5989
- if (Date.now() - lastActivityRef.current > IDLE_TIMEOUT_MS) return;
5990
- await fetchNewMessages();
5991
- }, [fetchNewMessages]);
6038
+ }, [appendMessages, lastConfirmedMessage]);
5992
6039
  const startPolling = React14.useCallback(() => {
5993
6040
  if (pollRef.current) clearInterval(pollRef.current);
5994
- const interval = Date.now() - lastActivityRef.current > IDLE_TIMEOUT_MS ? pollIdleInterval : pollInterval;
5995
- pollRef.current = setInterval(pollMessages, interval);
5996
- }, [pollMessages, pollInterval, pollIdleInterval]);
6041
+ const idle = () => Date.now() - lastActivityRef.current > IDLE_TIMEOUT_MS;
6042
+ let current = idle() ? pollIdleInterval : pollInterval;
6043
+ const tick = () => {
6044
+ void fetchNewMessages().then(() => {
6045
+ const desired = idle() ? pollIdleInterval : pollInterval;
6046
+ if (desired !== current && pollRef.current) {
6047
+ current = desired;
6048
+ clearInterval(pollRef.current);
6049
+ pollRef.current = setInterval(tick, current);
6050
+ }
6051
+ });
6052
+ };
6053
+ pollRef.current = setInterval(tick, current);
6054
+ }, [fetchNewMessages, pollInterval, pollIdleInterval]);
5997
6055
  const stopPolling = React14.useCallback(() => {
5998
6056
  if (pollRef.current) {
5999
6057
  clearInterval(pollRef.current);
@@ -6005,7 +6063,15 @@ function useChat(options = {}) {
6005
6063
  if (!c || !openedRef.current) return;
6006
6064
  const result = await c.support.openConversation();
6007
6065
  if (!result.ok) return;
6008
- setMessages(result.value.messages);
6066
+ const snapshot = result.value.messages;
6067
+ setMessages((prev) => {
6068
+ const snapshotIds = new Set(snapshot.map((m) => m.id));
6069
+ const tail = snapshot.length > 0 ? new Date(snapshot[snapshot.length - 1].created_at).getTime() : 0;
6070
+ const extras = prev.filter(
6071
+ (m) => !snapshotIds.has(m.id) && (m.id.startsWith("opt_") || new Date(m.created_at).getTime() > tail)
6072
+ );
6073
+ return mergeMessages(snapshot, extras);
6074
+ });
6009
6075
  aiEnabledRef.current = result.value.conversation.ai_enabled;
6010
6076
  setHumanRequested(result.value.conversation.needs_human_review ?? false);
6011
6077
  setSeenAt(result.value.conversation.team_read_at ?? null);
@@ -6017,7 +6083,13 @@ function useChat(options = {}) {
6017
6083
  startPolling();
6018
6084
  return;
6019
6085
  }
6086
+ if (wsReconnectTimerRef.current) {
6087
+ clearTimeout(wsReconnectTimerRef.current);
6088
+ wsReconnectTimerRef.current = null;
6089
+ }
6020
6090
  if (wsRef.current) {
6091
+ wsRef.current.onclose = null;
6092
+ wsRef.current.onmessage = null;
6021
6093
  wsRef.current.close();
6022
6094
  wsRef.current = null;
6023
6095
  }
@@ -6041,7 +6113,7 @@ function useChat(options = {}) {
6041
6113
  else stopAwaitingReply();
6042
6114
  } else if (envelope.type === "resync") {
6043
6115
  const latest = envelope.latest_message_at;
6044
- const last = messagesRef.current[messagesRef.current.length - 1];
6116
+ const last = lastConfirmedMessage();
6045
6117
  if (latest && (!last || new Date(last.created_at).getTime() < new Date(latest).getTime())) {
6046
6118
  fetchNewMessages();
6047
6119
  }
@@ -6058,6 +6130,7 @@ function useChat(options = {}) {
6058
6130
  if (wsReconnectAttemptRef.current < WS_MAX_RECONNECT_ATTEMPTS) {
6059
6131
  const attempt = wsReconnectAttemptRef.current++;
6060
6132
  const delay = Math.min(1e3 * Math.pow(2, attempt) + Math.random() * 500, 3e4);
6133
+ if (wsReconnectTimerRef.current) clearTimeout(wsReconnectTimerRef.current);
6061
6134
  wsReconnectTimerRef.current = setTimeout(connectWebSocket, delay);
6062
6135
  } else {
6063
6136
  startPolling();
@@ -6066,7 +6139,7 @@ function useChat(options = {}) {
6066
6139
  ws.onerror = () => {
6067
6140
  };
6068
6141
  wsRef.current = ws;
6069
- }, [appendMessages, startPolling, stopPolling, fetchNewMessages, resyncConversation]);
6142
+ }, [appendMessages, startPolling, stopPolling, fetchNewMessages, resyncConversation, lastConfirmedMessage]);
6070
6143
  const resetConversation = React14.useCallback(() => {
6071
6144
  stopPolling();
6072
6145
  if (wsReconnectTimerRef.current) {
@@ -6089,21 +6162,51 @@ function useChat(options = {}) {
6089
6162
  setError(null);
6090
6163
  setClaimedAs(null);
6091
6164
  }, [stopPolling, stopAwaitingReply]);
6165
+ const hydrateConversation = React14.useCallback(
6166
+ (value) => {
6167
+ const c = clientRef.current;
6168
+ setMessages(value.messages);
6169
+ setConversationOpened(true);
6170
+ aiEnabledRef.current = value.conversation.ai_enabled;
6171
+ setHumanRequested(value.conversation.needs_human_review ?? false);
6172
+ setSeenAt(value.conversation.team_read_at ?? null);
6173
+ setMayHaveOlder(value.messages.length >= 50);
6174
+ setClaimedAs(c?.support.claimedCustomerName ?? null);
6175
+ connectWebSocket();
6176
+ },
6177
+ [connectWebSocket]
6178
+ );
6179
+ const openIfExisting = React14.useCallback(async () => {
6180
+ const c = clientRef.current;
6181
+ if (!c || openedRef.current) return;
6182
+ openedRef.current = true;
6183
+ setIsLoading(true);
6184
+ const result = await c.support.peekConversation();
6185
+ if (result.ok && result.value) {
6186
+ hydrateConversation(result.value);
6187
+ } else {
6188
+ openedRef.current = false;
6189
+ }
6190
+ setIsLoading(false);
6191
+ }, [hydrateConversation]);
6092
6192
  const watchSessionClaims = React14.useCallback(() => {
6093
6193
  const c = clientRef.current;
6094
6194
  if (!c || claimUnsubRef.current) return;
6095
- const unsubClaimed = c.events.on(chunkDCRN67NO_js.SESSION_CLAIMED, (event) => {
6096
- setClaimedAs(event.customerName || null);
6097
- if (!openedRef.current) return;
6195
+ const unsubClaimed = c.events.on(chunkQLGZIBRO_js.SESSION_CLAIMED, (event) => {
6196
+ setClaimedAs(event.customerName || c.support.claimedCustomerName || null);
6197
+ if (!openedRef.current) {
6198
+ void openIfExisting();
6199
+ return;
6200
+ }
6098
6201
  connectWebSocket();
6099
6202
  void resyncConversation();
6100
6203
  });
6101
- const unsubCleared = c.events.on(chunkDCRN67NO_js.SESSION_CLEARED, resetConversation);
6204
+ const unsubCleared = c.events.on(chunkQLGZIBRO_js.SESSION_CLEARED, resetConversation);
6102
6205
  claimUnsubRef.current = () => {
6103
6206
  unsubClaimed();
6104
6207
  unsubCleared();
6105
6208
  };
6106
- }, [connectWebSocket, resyncConversation, resetConversation]);
6209
+ }, [connectWebSocket, resyncConversation, resetConversation, openIfExisting]);
6107
6210
  const signOut = React14.useCallback(async () => {
6108
6211
  const c = clientRef.current;
6109
6212
  if (!c) return false;
@@ -6111,6 +6214,7 @@ function useChat(options = {}) {
6111
6214
  return result.ok;
6112
6215
  }, []);
6113
6216
  React14.useEffect(() => {
6217
+ watchSessionClaims();
6114
6218
  return () => {
6115
6219
  if (pollRef.current) clearInterval(pollRef.current);
6116
6220
  if (wsReconnectTimerRef.current) clearTimeout(wsReconnectTimerRef.current);
@@ -6119,6 +6223,7 @@ function useChat(options = {}) {
6119
6223
  claimUnsubRef.current = null;
6120
6224
  if (wsRef.current) {
6121
6225
  wsRef.current.onclose = null;
6226
+ wsRef.current.onmessage = null;
6122
6227
  wsRef.current.close();
6123
6228
  }
6124
6229
  };
@@ -6130,21 +6235,13 @@ function useChat(options = {}) {
6130
6235
  setError(null);
6131
6236
  const result = await client.support.openConversation();
6132
6237
  if (result.ok) {
6133
- setMessages(result.value.messages);
6134
- setConversationOpened(true);
6135
- aiEnabledRef.current = result.value.conversation.ai_enabled;
6136
- setHumanRequested(result.value.conversation.needs_human_review ?? false);
6137
- setSeenAt(result.value.conversation.team_read_at ?? null);
6138
- setMayHaveOlder(result.value.messages.length >= 50);
6139
- setClaimedAs(client.support.claimedCustomerName);
6140
- connectWebSocket();
6141
- watchSessionClaims();
6238
+ hydrateConversation(result.value);
6142
6239
  } else {
6143
6240
  openedRef.current = false;
6144
6241
  setError(result.error);
6145
6242
  }
6146
6243
  setIsLoading(false);
6147
- }, [client, connectWebSocket, watchSessionClaims]);
6244
+ }, [client, hydrateConversation]);
6148
6245
  const send = React14.useCallback(
6149
6246
  async (content, opts) => {
6150
6247
  const attachments = opts?.attachments ?? [];
@@ -6156,7 +6253,8 @@ function useChat(options = {}) {
6156
6253
  if (!openedRef.current) return false;
6157
6254
  }
6158
6255
  const replyTo = opts?.replyTo;
6159
- const optimisticId = `opt_${Date.now()}`;
6256
+ const clientId = chunkVEOAMBEX_js.generateIdempotencyKey();
6257
+ const optimisticId = `opt_${clientId}`;
6160
6258
  const optimistic = {
6161
6259
  id: optimisticId,
6162
6260
  sender_type: "customer",
@@ -6164,6 +6262,7 @@ function useChat(options = {}) {
6164
6262
  content_type: attachments.length > 0 ? "image" : "text",
6165
6263
  attachments,
6166
6264
  metadata: {},
6265
+ client_id: clientId,
6167
6266
  // Same shape the server echoes back, so the bubble doesn't reflow
6168
6267
  // when the confirmed message replaces it.
6169
6268
  reply_to: replyTo ? {
@@ -6176,6 +6275,7 @@ function useChat(options = {}) {
6176
6275
  setMessages((prev) => [...prev, optimistic]);
6177
6276
  setIsSending(true);
6178
6277
  const result = await client.support.sendMessage(content.trim(), {
6278
+ idempotencyKey: clientId,
6179
6279
  replyToMessageId: replyTo?.id,
6180
6280
  attachments: attachments.length > 0 ? attachments : void 0
6181
6281
  });
@@ -6246,11 +6346,7 @@ function useChat(options = {}) {
6246
6346
  const older = result.value;
6247
6347
  setMayHaveOlder(older.length >= 50);
6248
6348
  if (older.length === 0) return false;
6249
- setMessages((prev) => {
6250
- const ids = new Set(prev.map((m) => m.id));
6251
- const fresh = older.filter((m) => !ids.has(m.id));
6252
- return fresh.length > 0 ? [...fresh, ...prev] : prev;
6253
- });
6349
+ setMessages((prev) => mergeMessages(prev, older));
6254
6350
  return true;
6255
6351
  }, []);
6256
6352
  const agentArrived = messages.some((m) => m.sender_type === "agent");
@@ -6270,7 +6366,8 @@ function useChat(options = {}) {
6270
6366
  startConversation,
6271
6367
  react,
6272
6368
  requestHuman,
6273
- loadOlder
6369
+ loadOlder,
6370
+ openIfExisting
6274
6371
  };
6275
6372
  }
6276
6373
  var ElementsContext = React14.createContext({
@@ -6345,15 +6442,15 @@ function AuthElement({
6345
6442
  }, [onReady, onAuthenticated, onError]);
6346
6443
  React14.useEffect(() => {
6347
6444
  if (!elements || !containerRef.current) return;
6348
- const element = elements.create(chunkIVJEUGWF_js.ELEMENT_TYPES.AUTH, { prefillEmail });
6445
+ const element = elements.create(chunkVEOAMBEX_js.ELEMENT_TYPES.AUTH, { prefillEmail });
6349
6446
  elementRef.current = element;
6350
- element.on(chunkIVJEUGWF_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6447
+ element.on(chunkVEOAMBEX_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6351
6448
  element.on(
6352
- chunkIVJEUGWF_js.EVENT_TYPES.AUTHENTICATED,
6449
+ chunkVEOAMBEX_js.EVENT_TYPES.AUTHENTICATED,
6353
6450
  (data) => onAuthenticatedRef.current?.(data)
6354
6451
  );
6355
6452
  element.on(
6356
- chunkIVJEUGWF_js.EVENT_TYPES.ERROR,
6453
+ chunkVEOAMBEX_js.EVENT_TYPES.ERROR,
6357
6454
  (data) => onErrorRef.current?.(data)
6358
6455
  );
6359
6456
  element.mount(containerRef.current);
@@ -6382,15 +6479,15 @@ function AddressElement({
6382
6479
  }, [onReady, onChange, onError]);
6383
6480
  React14.useEffect(() => {
6384
6481
  if (!elements || !containerRef.current) return;
6385
- const element = elements.create(chunkIVJEUGWF_js.ELEMENT_TYPES.ADDRESS, { mode });
6482
+ const element = elements.create(chunkVEOAMBEX_js.ELEMENT_TYPES.ADDRESS, { mode });
6386
6483
  elementRef.current = element;
6387
- element.on(chunkIVJEUGWF_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6484
+ element.on(chunkVEOAMBEX_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6388
6485
  element.on(
6389
- chunkIVJEUGWF_js.EVENT_TYPES.CHANGE,
6486
+ chunkVEOAMBEX_js.EVENT_TYPES.CHANGE,
6390
6487
  (data) => onChangeRef.current?.(data)
6391
6488
  );
6392
6489
  element.on(
6393
- chunkIVJEUGWF_js.EVENT_TYPES.ERROR,
6490
+ chunkVEOAMBEX_js.EVENT_TYPES.ERROR,
6394
6491
  (data) => onErrorRef.current?.(data)
6395
6492
  );
6396
6493
  element.mount(containerRef.current);
@@ -6420,15 +6517,15 @@ function PaymentElement({
6420
6517
  }, [onReady, onChange, onError]);
6421
6518
  React14.useEffect(() => {
6422
6519
  if (!elements || !containerRef.current) return;
6423
- const element = elements.create(chunkIVJEUGWF_js.ELEMENT_TYPES.PAYMENT, { amount, currency });
6520
+ const element = elements.create(chunkVEOAMBEX_js.ELEMENT_TYPES.PAYMENT, { amount, currency });
6424
6521
  elementRef.current = element;
6425
- element.on(chunkIVJEUGWF_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6522
+ element.on(chunkVEOAMBEX_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6426
6523
  element.on(
6427
- chunkIVJEUGWF_js.EVENT_TYPES.CHANGE,
6524
+ chunkVEOAMBEX_js.EVENT_TYPES.CHANGE,
6428
6525
  (data) => onChangeRef.current?.(data)
6429
6526
  );
6430
6527
  element.on(
6431
- chunkIVJEUGWF_js.EVENT_TYPES.ERROR,
6528
+ chunkVEOAMBEX_js.EVENT_TYPES.ERROR,
6432
6529
  (data) => onErrorRef.current?.(data)
6433
6530
  );
6434
6531
  element.mount(containerRef.current);
@@ -12438,6 +12535,9 @@ function ProductGrid({
12438
12535
 
12439
12536
  // src/react/utils/attachment.ts
12440
12537
  var IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".svg"];
12538
+ function safeAttachmentUrl(url) {
12539
+ return /^https?:\/\//i.test(url) ? url : null;
12540
+ }
12441
12541
  function isImageAttachment(attachment) {
12442
12542
  if (attachment.mime_type?.startsWith("image/")) return true;
12443
12543
  if (attachment.mime_type) return false;
@@ -17084,6 +17184,7 @@ function partitionMessages(messages) {
17084
17184
  }
17085
17185
  return { bubbles, reactionsByTarget };
17086
17186
  }
17187
+ var MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
17087
17188
  function ChatWidget({
17088
17189
  client: clientProp,
17089
17190
  businessName = "Support",
@@ -17100,7 +17201,8 @@ function ChatWidget({
17100
17201
  const context = useOptionalCimplifyClient();
17101
17202
  const client = clientProp ?? context?.client;
17102
17203
  const [open, setOpen] = React14.useState(defaultOpen);
17103
- const [readCount, setReadCount] = React14.useState(0);
17204
+ const [readAt, setReadAt] = React14.useState(null);
17205
+ const [attachError, setAttachError] = React14.useState(null);
17104
17206
  const {
17105
17207
  messages,
17106
17208
  isLoading,
@@ -17113,14 +17215,27 @@ function ChatWidget({
17113
17215
  signOut,
17114
17216
  send,
17115
17217
  react,
17116
- startConversation
17218
+ startConversation,
17219
+ openIfExisting
17117
17220
  } = useChat({ client, pollInterval });
17118
17221
  const showTyping = isSending || awaitingReply;
17119
17222
  const attachPhoto = React14.useCallback(
17120
17223
  async (file) => {
17121
- if (!client || !file.type.startsWith("image/") || file.size > 5 * 1024 * 1024) return;
17224
+ if (!client) return;
17225
+ setAttachError(null);
17226
+ if (!file.type.startsWith("image/")) {
17227
+ setAttachError(new chunkAETVRBKI_js.CimplifyError("invalid_attachment", "Only photos can be attached."));
17228
+ return;
17229
+ }
17230
+ if (file.size > MAX_ATTACHMENT_BYTES) {
17231
+ setAttachError(new chunkAETVRBKI_js.CimplifyError("attachment_too_large", "Photos must be under 5MB."));
17232
+ return;
17233
+ }
17122
17234
  const uploaded = await client.uploads.upload(file);
17123
- if (!uploaded.ok) return;
17235
+ if (!uploaded.ok) {
17236
+ setAttachError(uploaded.error);
17237
+ return;
17238
+ }
17124
17239
  const attachment = {
17125
17240
  upload_id: uploaded.value.id,
17126
17241
  url: uploaded.value.url,
@@ -17132,14 +17247,28 @@ function ChatWidget({
17132
17247
  },
17133
17248
  [client, send]
17134
17249
  );
17135
- const unread = open ? 0 : messages.slice(readCount).filter((m) => m.sender_type !== "customer").length;
17250
+ const handleSend = React14.useCallback(
17251
+ (content) => {
17252
+ setAttachError(null);
17253
+ return send(content);
17254
+ },
17255
+ [send]
17256
+ );
17257
+ const unread = open ? 0 : messages.filter(
17258
+ (m) => m.sender_type !== "customer" && (!readAt || m.created_at > readAt)
17259
+ ).length;
17260
+ React14.useState(() => {
17261
+ if (defaultOpen) void openIfExisting();
17262
+ });
17136
17263
  const handleOpen = React14.useCallback(() => {
17137
17264
  setOpen(true);
17138
- }, []);
17265
+ void openIfExisting();
17266
+ }, [openIfExisting]);
17139
17267
  const handleClose = React14.useCallback(() => {
17140
- setReadCount(messages.length);
17268
+ const newest = messages[messages.length - 1];
17269
+ setReadAt(newest?.created_at ?? null);
17141
17270
  setOpen(false);
17142
- }, [messages.length]);
17271
+ }, [messages]);
17143
17272
  const isLeft = position === "bottom-left";
17144
17273
  return /* @__PURE__ */ jsxRuntime.jsxs(
17145
17274
  "div",
@@ -17172,13 +17301,13 @@ function ChatWidget({
17172
17301
  isSending,
17173
17302
  isTyping: showTyping,
17174
17303
  seenAt,
17175
- error,
17304
+ error: error ?? attachError,
17176
17305
  isActive,
17177
17306
  isLeft,
17178
17307
  claimedAs,
17179
17308
  onSignOut: () => void signOut(),
17180
17309
  onClose: handleClose,
17181
- onSend: send,
17310
+ onSend: handleSend,
17182
17311
  onReact: react,
17183
17312
  onAttach: attachPhoto,
17184
17313
  onStartConversation: startConversation,
@@ -17289,7 +17418,7 @@ function Panel({
17289
17418
  className: classNames?.messages
17290
17419
  }
17291
17420
  ),
17292
- error != null && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-red-200 bg-red-50 px-4 py-2 text-xs text-red-600", children: "Something went wrong. Please try again." }),
17421
+ error != null && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-red-200 bg-red-50 px-4 py-2 text-xs text-red-600", children: error.userMessage || "Something went wrong. Please try again." }),
17293
17422
  (isActive || isLoading) && /* @__PURE__ */ jsxRuntime.jsx(
17294
17423
  ChatInput,
17295
17424
  {
@@ -17499,11 +17628,13 @@ function MessageList({
17499
17628
  ]
17500
17629
  }
17501
17630
  ),
17502
- msg.attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex flex-wrap gap-1.5", msg.content && "mb-1.5"), children: msg.attachments.map(
17503
- (attachment, index) => attachment.mime_type?.startsWith("image/") ? /* @__PURE__ */ jsxRuntime.jsx("a", { href: attachment.url, target: "_blank", rel: "noreferrer", children: /* @__PURE__ */ jsxRuntime.jsx(
17631
+ msg.attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex flex-wrap gap-1.5", msg.content && "mb-1.5"), children: msg.attachments.map((attachment, index) => {
17632
+ const url = safeAttachmentUrl(attachment.url);
17633
+ if (!url) return null;
17634
+ return attachment.mime_type?.startsWith("image/") ? /* @__PURE__ */ jsxRuntime.jsx("a", { href: url, target: "_blank", rel: "noreferrer", children: /* @__PURE__ */ jsxRuntime.jsx(
17504
17635
  "img",
17505
17636
  {
17506
- src: attachment.url,
17637
+ src: url,
17507
17638
  alt: attachment.filename ?? "Photo",
17508
17639
  loading: "lazy",
17509
17640
  className: "max-h-48 max-w-full rounded-lg object-cover"
@@ -17511,15 +17642,15 @@ function MessageList({
17511
17642
  ) }, index) : /* @__PURE__ */ jsxRuntime.jsx(
17512
17643
  "a",
17513
17644
  {
17514
- href: attachment.url,
17645
+ href: url,
17515
17646
  target: "_blank",
17516
17647
  rel: "noreferrer",
17517
17648
  className: "truncate text-xs underline",
17518
17649
  children: attachment.filename ?? "Attachment"
17519
17650
  },
17520
17651
  index
17521
- )
17522
- ) }),
17652
+ );
17653
+ }) }),
17523
17654
  isCustomer ? msg.content && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "whitespace-pre-wrap", children: msg.content }) : /* @__PURE__ */ jsxRuntime.jsx(
17524
17655
  ChatMarkdown,
17525
17656
  {
@@ -19810,6 +19941,7 @@ exports.prefetchProduct = prefetchProduct;
19810
19941
  exports.prefetchProducts = prefetchProducts;
19811
19942
  exports.resolveBoundaryKind = resolveBoundaryKind;
19812
19943
  exports.roomToResource = roomToResource;
19944
+ exports.safeAttachmentUrl = safeAttachmentUrl;
19813
19945
  exports.sanitizeRichTextHtml = sanitizeRichTextHtml;
19814
19946
  exports.stripHtmlToText = stripHtmlToText;
19815
19947
  exports.toAccountLineItem = toAccountLineItem;