@cimplify/sdk 0.92.0 → 0.93.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 +13 -5
  35. package/dist/react.d.ts +13 -5
  36. package/dist/react.js +174 -75
  37. package/dist/react.mjs +156 -58
  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);
@@ -1145,7 +1145,7 @@ var DEFAULT_COUNTRY = "US";
1145
1145
  function createDefaultClient() {
1146
1146
  const processRef = globalThis.process;
1147
1147
  const envPublicKey = processRef?.env?.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY || "";
1148
- return chunkDCRN67NO_js.createCimplifyClient({ publicKey: envPublicKey });
1148
+ return chunkQLGZIBRO_js.createCimplifyClient({ publicKey: envPublicKey });
1149
1149
  }
1150
1150
  function getStoredLocationId() {
1151
1151
  if (typeof window === "undefined" || !window.localStorage) {
@@ -3457,7 +3457,7 @@ function CimplifySignInButton({
3457
3457
  const onClick = async () => {
3458
3458
  if (busy || disabled || !signInOpts.clientId) return;
3459
3459
  setBusy(true);
3460
- await chunkIVJEUGWF_js.startSignIn({
3460
+ await chunkVEOAMBEX_js.startSignIn({
3461
3461
  ...signInOpts,
3462
3462
  onSuccess: () => {
3463
3463
  setBusy(false);
@@ -5913,6 +5913,20 @@ var IDLE_TIMEOUT_MS = 6e4;
5913
5913
  var WS_MAX_RECONNECT_ATTEMPTS = 10;
5914
5914
  var WS_PATH = "/api/v1/support/conversation/ws";
5915
5915
  var AWAIT_REPLY_TIMEOUT_MS = 3e4;
5916
+ function mergeMessages(prev, incoming) {
5917
+ const ids = new Set(prev.map((m) => m.id));
5918
+ const fresh = incoming.filter((m) => !ids.has(m.id));
5919
+ if (fresh.length === 0) return prev;
5920
+ const merged = [...prev, ...fresh].sort(
5921
+ (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
5922
+ );
5923
+ const confirmed = new Set(
5924
+ merged.filter((m) => !m.id.startsWith("opt_") && m.client_id).map((m) => m.client_id)
5925
+ );
5926
+ return merged.filter(
5927
+ (m) => !(m.id.startsWith("opt_") && m.client_id && confirmed.has(m.client_id))
5928
+ );
5929
+ }
5916
5930
  function useChat(options = {}) {
5917
5931
  const context = useOptionalCimplifyClient();
5918
5932
  const client = options.client ?? context?.client;
@@ -5965,18 +5979,22 @@ function useChat(options = {}) {
5965
5979
  if (incoming.some((m) => m.sender_type === "bot" || m.sender_type === "agent")) {
5966
5980
  stopAwaitingReply();
5967
5981
  }
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
- });
5982
+ if (incoming.length > 0) lastActivityRef.current = Date.now();
5983
+ setMessages((prev) => mergeMessages(prev, incoming));
5973
5984
  },
5974
5985
  [stopAwaitingReply]
5975
5986
  );
5987
+ const lastConfirmedMessage = React14.useCallback(() => {
5988
+ const list = messagesRef.current;
5989
+ for (let i = list.length - 1; i >= 0; i--) {
5990
+ if (!list[i].id.startsWith("opt_")) return list[i];
5991
+ }
5992
+ return void 0;
5993
+ }, []);
5976
5994
  const fetchNewMessages = React14.useCallback(async () => {
5977
5995
  const c = clientRef.current;
5978
5996
  if (!c) return;
5979
- const last = messagesRef.current[messagesRef.current.length - 1];
5997
+ const last = lastConfirmedMessage();
5980
5998
  const result = await c.support.getMessages({
5981
5999
  after: last?.created_at,
5982
6000
  limit: 50
@@ -5984,16 +6002,23 @@ function useChat(options = {}) {
5984
6002
  if (result.ok && result.value.length > 0) {
5985
6003
  appendMessages(result.value);
5986
6004
  }
5987
- }, [appendMessages]);
5988
- const pollMessages = React14.useCallback(async () => {
5989
- if (Date.now() - lastActivityRef.current > IDLE_TIMEOUT_MS) return;
5990
- await fetchNewMessages();
5991
- }, [fetchNewMessages]);
6005
+ }, [appendMessages, lastConfirmedMessage]);
5992
6006
  const startPolling = React14.useCallback(() => {
5993
6007
  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]);
6008
+ const idle = () => Date.now() - lastActivityRef.current > IDLE_TIMEOUT_MS;
6009
+ let current = idle() ? pollIdleInterval : pollInterval;
6010
+ const tick = () => {
6011
+ void fetchNewMessages().then(() => {
6012
+ const desired = idle() ? pollIdleInterval : pollInterval;
6013
+ if (desired !== current && pollRef.current) {
6014
+ current = desired;
6015
+ clearInterval(pollRef.current);
6016
+ pollRef.current = setInterval(tick, current);
6017
+ }
6018
+ });
6019
+ };
6020
+ pollRef.current = setInterval(tick, current);
6021
+ }, [fetchNewMessages, pollInterval, pollIdleInterval]);
5997
6022
  const stopPolling = React14.useCallback(() => {
5998
6023
  if (pollRef.current) {
5999
6024
  clearInterval(pollRef.current);
@@ -6005,7 +6030,15 @@ function useChat(options = {}) {
6005
6030
  if (!c || !openedRef.current) return;
6006
6031
  const result = await c.support.openConversation();
6007
6032
  if (!result.ok) return;
6008
- setMessages(result.value.messages);
6033
+ const snapshot = result.value.messages;
6034
+ setMessages((prev) => {
6035
+ const snapshotIds = new Set(snapshot.map((m) => m.id));
6036
+ const tail = snapshot.length > 0 ? new Date(snapshot[snapshot.length - 1].created_at).getTime() : 0;
6037
+ const extras = prev.filter(
6038
+ (m) => !snapshotIds.has(m.id) && (m.id.startsWith("opt_") || new Date(m.created_at).getTime() > tail)
6039
+ );
6040
+ return mergeMessages(snapshot, extras);
6041
+ });
6009
6042
  aiEnabledRef.current = result.value.conversation.ai_enabled;
6010
6043
  setHumanRequested(result.value.conversation.needs_human_review ?? false);
6011
6044
  setSeenAt(result.value.conversation.team_read_at ?? null);
@@ -6017,7 +6050,13 @@ function useChat(options = {}) {
6017
6050
  startPolling();
6018
6051
  return;
6019
6052
  }
6053
+ if (wsReconnectTimerRef.current) {
6054
+ clearTimeout(wsReconnectTimerRef.current);
6055
+ wsReconnectTimerRef.current = null;
6056
+ }
6020
6057
  if (wsRef.current) {
6058
+ wsRef.current.onclose = null;
6059
+ wsRef.current.onmessage = null;
6021
6060
  wsRef.current.close();
6022
6061
  wsRef.current = null;
6023
6062
  }
@@ -6041,7 +6080,7 @@ function useChat(options = {}) {
6041
6080
  else stopAwaitingReply();
6042
6081
  } else if (envelope.type === "resync") {
6043
6082
  const latest = envelope.latest_message_at;
6044
- const last = messagesRef.current[messagesRef.current.length - 1];
6083
+ const last = lastConfirmedMessage();
6045
6084
  if (latest && (!last || new Date(last.created_at).getTime() < new Date(latest).getTime())) {
6046
6085
  fetchNewMessages();
6047
6086
  }
@@ -6058,6 +6097,7 @@ function useChat(options = {}) {
6058
6097
  if (wsReconnectAttemptRef.current < WS_MAX_RECONNECT_ATTEMPTS) {
6059
6098
  const attempt = wsReconnectAttemptRef.current++;
6060
6099
  const delay = Math.min(1e3 * Math.pow(2, attempt) + Math.random() * 500, 3e4);
6100
+ if (wsReconnectTimerRef.current) clearTimeout(wsReconnectTimerRef.current);
6061
6101
  wsReconnectTimerRef.current = setTimeout(connectWebSocket, delay);
6062
6102
  } else {
6063
6103
  startPolling();
@@ -6066,7 +6106,7 @@ function useChat(options = {}) {
6066
6106
  ws.onerror = () => {
6067
6107
  };
6068
6108
  wsRef.current = ws;
6069
- }, [appendMessages, startPolling, stopPolling, fetchNewMessages, resyncConversation]);
6109
+ }, [appendMessages, startPolling, stopPolling, fetchNewMessages, resyncConversation, lastConfirmedMessage]);
6070
6110
  const resetConversation = React14.useCallback(() => {
6071
6111
  stopPolling();
6072
6112
  if (wsReconnectTimerRef.current) {
@@ -6089,21 +6129,51 @@ function useChat(options = {}) {
6089
6129
  setError(null);
6090
6130
  setClaimedAs(null);
6091
6131
  }, [stopPolling, stopAwaitingReply]);
6132
+ const hydrateConversation = React14.useCallback(
6133
+ (value) => {
6134
+ const c = clientRef.current;
6135
+ setMessages(value.messages);
6136
+ setConversationOpened(true);
6137
+ aiEnabledRef.current = value.conversation.ai_enabled;
6138
+ setHumanRequested(value.conversation.needs_human_review ?? false);
6139
+ setSeenAt(value.conversation.team_read_at ?? null);
6140
+ setMayHaveOlder(value.messages.length >= 50);
6141
+ setClaimedAs(c?.support.claimedCustomerName ?? null);
6142
+ connectWebSocket();
6143
+ },
6144
+ [connectWebSocket]
6145
+ );
6146
+ const openIfExisting = React14.useCallback(async () => {
6147
+ const c = clientRef.current;
6148
+ if (!c || openedRef.current) return;
6149
+ openedRef.current = true;
6150
+ setIsLoading(true);
6151
+ const result = await c.support.peekConversation();
6152
+ if (result.ok && result.value) {
6153
+ hydrateConversation(result.value);
6154
+ } else {
6155
+ openedRef.current = false;
6156
+ }
6157
+ setIsLoading(false);
6158
+ }, [hydrateConversation]);
6092
6159
  const watchSessionClaims = React14.useCallback(() => {
6093
6160
  const c = clientRef.current;
6094
6161
  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;
6162
+ const unsubClaimed = c.events.on(chunkQLGZIBRO_js.SESSION_CLAIMED, (event) => {
6163
+ setClaimedAs(event.customerName || c.support.claimedCustomerName || null);
6164
+ if (!openedRef.current) {
6165
+ void openIfExisting();
6166
+ return;
6167
+ }
6098
6168
  connectWebSocket();
6099
6169
  void resyncConversation();
6100
6170
  });
6101
- const unsubCleared = c.events.on(chunkDCRN67NO_js.SESSION_CLEARED, resetConversation);
6171
+ const unsubCleared = c.events.on(chunkQLGZIBRO_js.SESSION_CLEARED, resetConversation);
6102
6172
  claimUnsubRef.current = () => {
6103
6173
  unsubClaimed();
6104
6174
  unsubCleared();
6105
6175
  };
6106
- }, [connectWebSocket, resyncConversation, resetConversation]);
6176
+ }, [connectWebSocket, resyncConversation, resetConversation, openIfExisting]);
6107
6177
  const signOut = React14.useCallback(async () => {
6108
6178
  const c = clientRef.current;
6109
6179
  if (!c) return false;
@@ -6111,6 +6181,7 @@ function useChat(options = {}) {
6111
6181
  return result.ok;
6112
6182
  }, []);
6113
6183
  React14.useEffect(() => {
6184
+ watchSessionClaims();
6114
6185
  return () => {
6115
6186
  if (pollRef.current) clearInterval(pollRef.current);
6116
6187
  if (wsReconnectTimerRef.current) clearTimeout(wsReconnectTimerRef.current);
@@ -6119,6 +6190,7 @@ function useChat(options = {}) {
6119
6190
  claimUnsubRef.current = null;
6120
6191
  if (wsRef.current) {
6121
6192
  wsRef.current.onclose = null;
6193
+ wsRef.current.onmessage = null;
6122
6194
  wsRef.current.close();
6123
6195
  }
6124
6196
  };
@@ -6130,21 +6202,13 @@ function useChat(options = {}) {
6130
6202
  setError(null);
6131
6203
  const result = await client.support.openConversation();
6132
6204
  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();
6205
+ hydrateConversation(result.value);
6142
6206
  } else {
6143
6207
  openedRef.current = false;
6144
6208
  setError(result.error);
6145
6209
  }
6146
6210
  setIsLoading(false);
6147
- }, [client, connectWebSocket, watchSessionClaims]);
6211
+ }, [client, hydrateConversation]);
6148
6212
  const send = React14.useCallback(
6149
6213
  async (content, opts) => {
6150
6214
  const attachments = opts?.attachments ?? [];
@@ -6156,7 +6220,8 @@ function useChat(options = {}) {
6156
6220
  if (!openedRef.current) return false;
6157
6221
  }
6158
6222
  const replyTo = opts?.replyTo;
6159
- const optimisticId = `opt_${Date.now()}`;
6223
+ const clientId = chunkVEOAMBEX_js.generateIdempotencyKey();
6224
+ const optimisticId = `opt_${clientId}`;
6160
6225
  const optimistic = {
6161
6226
  id: optimisticId,
6162
6227
  sender_type: "customer",
@@ -6164,6 +6229,7 @@ function useChat(options = {}) {
6164
6229
  content_type: attachments.length > 0 ? "image" : "text",
6165
6230
  attachments,
6166
6231
  metadata: {},
6232
+ client_id: clientId,
6167
6233
  // Same shape the server echoes back, so the bubble doesn't reflow
6168
6234
  // when the confirmed message replaces it.
6169
6235
  reply_to: replyTo ? {
@@ -6176,6 +6242,7 @@ function useChat(options = {}) {
6176
6242
  setMessages((prev) => [...prev, optimistic]);
6177
6243
  setIsSending(true);
6178
6244
  const result = await client.support.sendMessage(content.trim(), {
6245
+ idempotencyKey: clientId,
6179
6246
  replyToMessageId: replyTo?.id,
6180
6247
  attachments: attachments.length > 0 ? attachments : void 0
6181
6248
  });
@@ -6246,11 +6313,7 @@ function useChat(options = {}) {
6246
6313
  const older = result.value;
6247
6314
  setMayHaveOlder(older.length >= 50);
6248
6315
  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
- });
6316
+ setMessages((prev) => mergeMessages(prev, older));
6254
6317
  return true;
6255
6318
  }, []);
6256
6319
  const agentArrived = messages.some((m) => m.sender_type === "agent");
@@ -6270,7 +6333,8 @@ function useChat(options = {}) {
6270
6333
  startConversation,
6271
6334
  react,
6272
6335
  requestHuman,
6273
- loadOlder
6336
+ loadOlder,
6337
+ openIfExisting
6274
6338
  };
6275
6339
  }
6276
6340
  var ElementsContext = React14.createContext({
@@ -6345,15 +6409,15 @@ function AuthElement({
6345
6409
  }, [onReady, onAuthenticated, onError]);
6346
6410
  React14.useEffect(() => {
6347
6411
  if (!elements || !containerRef.current) return;
6348
- const element = elements.create(chunkIVJEUGWF_js.ELEMENT_TYPES.AUTH, { prefillEmail });
6412
+ const element = elements.create(chunkVEOAMBEX_js.ELEMENT_TYPES.AUTH, { prefillEmail });
6349
6413
  elementRef.current = element;
6350
- element.on(chunkIVJEUGWF_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6414
+ element.on(chunkVEOAMBEX_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6351
6415
  element.on(
6352
- chunkIVJEUGWF_js.EVENT_TYPES.AUTHENTICATED,
6416
+ chunkVEOAMBEX_js.EVENT_TYPES.AUTHENTICATED,
6353
6417
  (data) => onAuthenticatedRef.current?.(data)
6354
6418
  );
6355
6419
  element.on(
6356
- chunkIVJEUGWF_js.EVENT_TYPES.ERROR,
6420
+ chunkVEOAMBEX_js.EVENT_TYPES.ERROR,
6357
6421
  (data) => onErrorRef.current?.(data)
6358
6422
  );
6359
6423
  element.mount(containerRef.current);
@@ -6382,15 +6446,15 @@ function AddressElement({
6382
6446
  }, [onReady, onChange, onError]);
6383
6447
  React14.useEffect(() => {
6384
6448
  if (!elements || !containerRef.current) return;
6385
- const element = elements.create(chunkIVJEUGWF_js.ELEMENT_TYPES.ADDRESS, { mode });
6449
+ const element = elements.create(chunkVEOAMBEX_js.ELEMENT_TYPES.ADDRESS, { mode });
6386
6450
  elementRef.current = element;
6387
- element.on(chunkIVJEUGWF_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6451
+ element.on(chunkVEOAMBEX_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6388
6452
  element.on(
6389
- chunkIVJEUGWF_js.EVENT_TYPES.CHANGE,
6453
+ chunkVEOAMBEX_js.EVENT_TYPES.CHANGE,
6390
6454
  (data) => onChangeRef.current?.(data)
6391
6455
  );
6392
6456
  element.on(
6393
- chunkIVJEUGWF_js.EVENT_TYPES.ERROR,
6457
+ chunkVEOAMBEX_js.EVENT_TYPES.ERROR,
6394
6458
  (data) => onErrorRef.current?.(data)
6395
6459
  );
6396
6460
  element.mount(containerRef.current);
@@ -6420,15 +6484,15 @@ function PaymentElement({
6420
6484
  }, [onReady, onChange, onError]);
6421
6485
  React14.useEffect(() => {
6422
6486
  if (!elements || !containerRef.current) return;
6423
- const element = elements.create(chunkIVJEUGWF_js.ELEMENT_TYPES.PAYMENT, { amount, currency });
6487
+ const element = elements.create(chunkVEOAMBEX_js.ELEMENT_TYPES.PAYMENT, { amount, currency });
6424
6488
  elementRef.current = element;
6425
- element.on(chunkIVJEUGWF_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6489
+ element.on(chunkVEOAMBEX_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6426
6490
  element.on(
6427
- chunkIVJEUGWF_js.EVENT_TYPES.CHANGE,
6491
+ chunkVEOAMBEX_js.EVENT_TYPES.CHANGE,
6428
6492
  (data) => onChangeRef.current?.(data)
6429
6493
  );
6430
6494
  element.on(
6431
- chunkIVJEUGWF_js.EVENT_TYPES.ERROR,
6495
+ chunkVEOAMBEX_js.EVENT_TYPES.ERROR,
6432
6496
  (data) => onErrorRef.current?.(data)
6433
6497
  );
6434
6498
  element.mount(containerRef.current);
@@ -12438,6 +12502,9 @@ function ProductGrid({
12438
12502
 
12439
12503
  // src/react/utils/attachment.ts
12440
12504
  var IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".svg"];
12505
+ function safeAttachmentUrl(url) {
12506
+ return /^https?:\/\//i.test(url) ? url : null;
12507
+ }
12441
12508
  function isImageAttachment(attachment) {
12442
12509
  if (attachment.mime_type?.startsWith("image/")) return true;
12443
12510
  if (attachment.mime_type) return false;
@@ -17084,6 +17151,7 @@ function partitionMessages(messages) {
17084
17151
  }
17085
17152
  return { bubbles, reactionsByTarget };
17086
17153
  }
17154
+ var MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
17087
17155
  function ChatWidget({
17088
17156
  client: clientProp,
17089
17157
  businessName = "Support",
@@ -17100,7 +17168,8 @@ function ChatWidget({
17100
17168
  const context = useOptionalCimplifyClient();
17101
17169
  const client = clientProp ?? context?.client;
17102
17170
  const [open, setOpen] = React14.useState(defaultOpen);
17103
- const [readCount, setReadCount] = React14.useState(0);
17171
+ const [readAt, setReadAt] = React14.useState(null);
17172
+ const [attachError, setAttachError] = React14.useState(null);
17104
17173
  const {
17105
17174
  messages,
17106
17175
  isLoading,
@@ -17113,14 +17182,27 @@ function ChatWidget({
17113
17182
  signOut,
17114
17183
  send,
17115
17184
  react,
17116
- startConversation
17185
+ startConversation,
17186
+ openIfExisting
17117
17187
  } = useChat({ client, pollInterval });
17118
17188
  const showTyping = isSending || awaitingReply;
17119
17189
  const attachPhoto = React14.useCallback(
17120
17190
  async (file) => {
17121
- if (!client || !file.type.startsWith("image/") || file.size > 5 * 1024 * 1024) return;
17191
+ if (!client) return;
17192
+ setAttachError(null);
17193
+ if (!file.type.startsWith("image/")) {
17194
+ setAttachError(new chunkAETVRBKI_js.CimplifyError("invalid_attachment", "Only photos can be attached."));
17195
+ return;
17196
+ }
17197
+ if (file.size > MAX_ATTACHMENT_BYTES) {
17198
+ setAttachError(new chunkAETVRBKI_js.CimplifyError("attachment_too_large", "Photos must be under 5MB."));
17199
+ return;
17200
+ }
17122
17201
  const uploaded = await client.uploads.upload(file);
17123
- if (!uploaded.ok) return;
17202
+ if (!uploaded.ok) {
17203
+ setAttachError(uploaded.error);
17204
+ return;
17205
+ }
17124
17206
  const attachment = {
17125
17207
  upload_id: uploaded.value.id,
17126
17208
  url: uploaded.value.url,
@@ -17132,14 +17214,28 @@ function ChatWidget({
17132
17214
  },
17133
17215
  [client, send]
17134
17216
  );
17135
- const unread = open ? 0 : messages.slice(readCount).filter((m) => m.sender_type !== "customer").length;
17217
+ const handleSend = React14.useCallback(
17218
+ (content) => {
17219
+ setAttachError(null);
17220
+ return send(content);
17221
+ },
17222
+ [send]
17223
+ );
17224
+ const unread = open ? 0 : messages.filter(
17225
+ (m) => m.sender_type !== "customer" && (!readAt || m.created_at > readAt)
17226
+ ).length;
17227
+ React14.useState(() => {
17228
+ if (defaultOpen) void openIfExisting();
17229
+ });
17136
17230
  const handleOpen = React14.useCallback(() => {
17137
17231
  setOpen(true);
17138
- }, []);
17232
+ void openIfExisting();
17233
+ }, [openIfExisting]);
17139
17234
  const handleClose = React14.useCallback(() => {
17140
- setReadCount(messages.length);
17235
+ const newest = messages[messages.length - 1];
17236
+ setReadAt(newest?.created_at ?? null);
17141
17237
  setOpen(false);
17142
- }, [messages.length]);
17238
+ }, [messages]);
17143
17239
  const isLeft = position === "bottom-left";
17144
17240
  return /* @__PURE__ */ jsxRuntime.jsxs(
17145
17241
  "div",
@@ -17172,13 +17268,13 @@ function ChatWidget({
17172
17268
  isSending,
17173
17269
  isTyping: showTyping,
17174
17270
  seenAt,
17175
- error,
17271
+ error: error ?? attachError,
17176
17272
  isActive,
17177
17273
  isLeft,
17178
17274
  claimedAs,
17179
17275
  onSignOut: () => void signOut(),
17180
17276
  onClose: handleClose,
17181
- onSend: send,
17277
+ onSend: handleSend,
17182
17278
  onReact: react,
17183
17279
  onAttach: attachPhoto,
17184
17280
  onStartConversation: startConversation,
@@ -17289,7 +17385,7 @@ function Panel({
17289
17385
  className: classNames?.messages
17290
17386
  }
17291
17387
  ),
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." }),
17388
+ 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
17389
  (isActive || isLoading) && /* @__PURE__ */ jsxRuntime.jsx(
17294
17390
  ChatInput,
17295
17391
  {
@@ -17499,11 +17595,13 @@ function MessageList({
17499
17595
  ]
17500
17596
  }
17501
17597
  ),
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(
17598
+ 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) => {
17599
+ const url = safeAttachmentUrl(attachment.url);
17600
+ if (!url) return null;
17601
+ return attachment.mime_type?.startsWith("image/") ? /* @__PURE__ */ jsxRuntime.jsx("a", { href: url, target: "_blank", rel: "noreferrer", children: /* @__PURE__ */ jsxRuntime.jsx(
17504
17602
  "img",
17505
17603
  {
17506
- src: attachment.url,
17604
+ src: url,
17507
17605
  alt: attachment.filename ?? "Photo",
17508
17606
  loading: "lazy",
17509
17607
  className: "max-h-48 max-w-full rounded-lg object-cover"
@@ -17511,15 +17609,15 @@ function MessageList({
17511
17609
  ) }, index) : /* @__PURE__ */ jsxRuntime.jsx(
17512
17610
  "a",
17513
17611
  {
17514
- href: attachment.url,
17612
+ href: url,
17515
17613
  target: "_blank",
17516
17614
  rel: "noreferrer",
17517
17615
  className: "truncate text-xs underline",
17518
17616
  children: attachment.filename ?? "Attachment"
17519
17617
  },
17520
17618
  index
17521
- )
17522
- ) }),
17619
+ );
17620
+ }) }),
17523
17621
  isCustomer ? msg.content && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "whitespace-pre-wrap", children: msg.content }) : /* @__PURE__ */ jsxRuntime.jsx(
17524
17622
  ChatMarkdown,
17525
17623
  {
@@ -19810,6 +19908,7 @@ exports.prefetchProduct = prefetchProduct;
19810
19908
  exports.prefetchProducts = prefetchProducts;
19811
19909
  exports.resolveBoundaryKind = resolveBoundaryKind;
19812
19910
  exports.roomToResource = roomToResource;
19911
+ exports.safeAttachmentUrl = safeAttachmentUrl;
19813
19912
  exports.sanitizeRichTextHtml = sanitizeRichTextHtml;
19814
19913
  exports.stripHtmlToText = stripHtmlToText;
19815
19914
  exports.toAccountLineItem = toAccountLineItem;