@cimplify/sdk 0.90.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 (67) 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-KJP4U6L3.js → chunk-6Y7KC5YZ.js} +23 -0
  6. package/dist/{chunk-JVWFIFQ7.js → chunk-AWNWP5J3.js} +4 -4
  7. package/dist/{chunk-2UDEWILL.mjs → chunk-FKARGZK3.mjs} +307 -90
  8. package/dist/{chunk-T7Z25EY4.js → chunk-HKN3YG76.js} +84 -0
  9. package/dist/{chunk-BN4V4ONE.js → chunk-QLGZIBRO.js} +348 -128
  10. package/dist/{chunk-NGE37REW.mjs → chunk-QXWX7CD2.mjs} +2 -2
  11. package/dist/{chunk-OZDDHSIB.mjs → chunk-RRBY35TJ.mjs} +80 -1
  12. package/dist/{chunk-GHFDYM5K.js → chunk-VEOAMBEX.js} +11 -4
  13. package/dist/{chunk-ZU4ZRYUR.mjs → chunk-VUSHG3HA.mjs} +11 -4
  14. package/dist/{chunk-NNGKC3MM.mjs → chunk-X36FZZT6.mjs} +23 -0
  15. package/dist/{client-YiSelM7x.d.mts → client-BTL9d_nN.d.mts} +117 -11
  16. package/dist/{client-CR1f94wY.d.ts → client-BX30FAWZ.d.ts} +117 -11
  17. package/dist/{client-DMttx1xk.d.mts → client-Bhe2TnFW.d.mts} +2 -2
  18. package/dist/{client-CqpOcbin.d.ts → client-DrkuEK_I.d.ts} +2 -2
  19. package/dist/{index-DD9z396M.d.mts → index-BuNjyfaV.d.mts} +3 -3
  20. package/dist/{index-CxTVu0HZ.d.ts → index-D-w5h82t.d.ts} +3 -3
  21. package/dist/index.d.mts +49 -7
  22. package/dist/index.d.ts +49 -7
  23. package/dist/index.js +103 -83
  24. package/dist/index.mjs +5 -5
  25. package/dist/mock/cli.mjs +23 -0
  26. package/dist/mock/library.js +23 -0
  27. package/dist/mock/library.mjs +23 -0
  28. package/dist/mock/msw.js +23 -0
  29. package/dist/mock/msw.mjs +23 -0
  30. package/dist/{payment-BX7ccIKY.d.ts → payment-By0JidTe.d.ts} +1 -1
  31. package/dist/{payment-5d8FTA1q.d.mts → payment-pKPpaORI.d.mts} +1 -1
  32. package/dist/{price-DtfP_Xfq.d.ts → price-23RNXftz.d.ts} +1 -1
  33. package/dist/{price-arl_4Rzn.d.mts → price-BvRtp6ak.d.mts} +1 -1
  34. package/dist/{product-DZX4QuKS.d.ts → product-O8-wcHSe.d.mts} +49 -0
  35. package/dist/{product-DZX4QuKS.d.mts → product-O8-wcHSe.d.ts} +49 -0
  36. package/dist/react.d.mts +79 -6
  37. package/dist/react.d.ts +79 -6
  38. package/dist/react.js +607 -379
  39. package/dist/react.mjs +539 -315
  40. package/dist/{server-BceZMxcK.d.mts → server-BN9ZCAKT.d.mts} +5 -1
  41. package/dist/{server-DoJGSC7L.d.ts → server-DDGbfW_S.d.ts} +5 -1
  42. package/dist/server.d.mts +4 -4
  43. package/dist/server.d.ts +4 -4
  44. package/dist/server.js +7 -7
  45. package/dist/server.mjs +2 -2
  46. package/dist/styles.css +1 -1
  47. package/dist/testing/msw.d.mts +2 -2
  48. package/dist/testing/msw.d.ts +2 -2
  49. package/dist/testing/msw.js +2 -2
  50. package/dist/testing/msw.mjs +1 -1
  51. package/dist/testing/suite.d.mts +6 -6
  52. package/dist/testing/suite.d.ts +6 -6
  53. package/dist/testing/suite.js +23 -23
  54. package/dist/testing/suite.mjs +4 -4
  55. package/dist/testing.d.mts +5 -5
  56. package/dist/testing.d.ts +5 -5
  57. package/dist/testing.js +79 -79
  58. package/dist/testing.mjs +5 -5
  59. package/dist/utils.d.mts +4 -4
  60. package/dist/utils.d.ts +4 -4
  61. package/package.json +1 -1
  62. package/registry/booking-page.json +1 -1
  63. package/registry/chat-widget.json +1 -1
  64. package/registry/customer-input-fields.json +1 -1
  65. package/registry/date-slot-picker.json +1 -1
  66. package/registry/product-customizer.json +1 -1
  67. package/registry/rental-service-card.json +1 -1
package/dist/react.js CHANGED
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
  'use strict';
3
3
 
4
- var chunkT7Z25EY4_js = require('./chunk-T7Z25EY4.js');
4
+ var chunkHKN3YG76_js = require('./chunk-HKN3YG76.js');
5
5
  var chunkXREIZ56K_js = require('./chunk-XREIZ56K.js');
6
- var chunkBN4V4ONE_js = require('./chunk-BN4V4ONE.js');
7
- var chunkGHFDYM5K_js = require('./chunk-GHFDYM5K.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');
@@ -747,6 +747,7 @@ function mapServerCart(serverCart, fallbackCurrency) {
747
747
  scheduledStart: item.scheduled_start || void 0,
748
748
  scheduledEnd: item.scheduled_end || void 0,
749
749
  staffId: item.staff_id || void 0,
750
+ units: item.units || void 0,
750
751
  lineType: item.line_type,
751
752
  confirmationCode: item.confirmation_code || void 0,
752
753
  serviceStatus: item.service_status || void 0,
@@ -805,7 +806,7 @@ async function syncFromServer(entry) {
805
806
  return next;
806
807
  }
807
808
  function emitMutationError(entry, operation, opId, idempotencyKey, error) {
808
- entry.client.events.emit("mutation:error", {
809
+ entry.client.events.emit(chunkQLGZIBRO_js.MUTATION_ERROR, {
809
810
  service: "cart",
810
811
  operation,
811
812
  opId,
@@ -845,6 +846,12 @@ function getOrCreateStore(params) {
845
846
  syncTail: Promise.resolve(),
846
847
  failedOpRemovers: /* @__PURE__ */ new Set()
847
848
  };
849
+ client.events.on(chunkQLGZIBRO_js.SESSION_CLAIMED, () => {
850
+ if (entry.initialized) void syncFromServer(entry);
851
+ });
852
+ client.events.on(chunkQLGZIBRO_js.SESSION_CLEARED, () => {
853
+ if (entry.initialized) void syncFromServer(entry);
854
+ });
848
855
  cartStores.set(storeKey, entry);
849
856
  return entry;
850
857
  }
@@ -903,7 +910,8 @@ async function performAddItem(entry, product, quantity, options) {
903
910
  specialInstructions: normalizedOptions.specialInstructions,
904
911
  scheduledStart: normalizedOptions.scheduledStart,
905
912
  scheduledEnd: normalizedOptions.scheduledEnd,
906
- staffId: normalizedOptions.staffId
913
+ staffId: normalizedOptions.staffId,
914
+ units: normalizedOptions.units
907
915
  });
908
916
  const idempotencyKey = `idem_${typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}_${Math.random().toString(36).slice(2, 10)}`}`;
909
917
  const patch = { kind: "add_item", tempItem, lineKey };
@@ -925,6 +933,7 @@ async function performAddItem(entry, product, quantity, options) {
925
933
  scheduled_end: normalizedOptions.scheduledEnd,
926
934
  staff_id: normalizedOptions.staffId,
927
935
  resource_id: normalizedOptions.resourceId,
936
+ units: normalizedOptions.units,
928
937
  customer_inputs: normalizedOptions.customerInputs
929
938
  },
930
939
  { idempotencyKey }
@@ -1136,7 +1145,7 @@ var DEFAULT_COUNTRY = "US";
1136
1145
  function createDefaultClient() {
1137
1146
  const processRef = globalThis.process;
1138
1147
  const envPublicKey = processRef?.env?.NEXT_PUBLIC_CIMPLIFY_PUBLIC_KEY || "";
1139
- return chunkBN4V4ONE_js.createCimplifyClient({ publicKey: envPublicKey });
1148
+ return chunkQLGZIBRO_js.createCimplifyClient({ publicKey: envPublicKey });
1140
1149
  }
1141
1150
  function getStoredLocationId() {
1142
1151
  if (typeof window === "undefined" || !window.localStorage) {
@@ -1289,7 +1298,7 @@ function CimplifyProvider({
1289
1298
  const apiCredentials = resolvedClient.getCredentialsMode() === "include" ? "use-credentials" : "anonymous";
1290
1299
  preconnectOrigin(resolvedClient.getBaseUrl(), apiCredentials);
1291
1300
  preconnectOrigin(resolvedClient.getLinkApiUrl(), apiCredentials);
1292
- preconnectOrigin(chunkT7Z25EY4_js.DEFAULT_CDN_BASE_URL);
1301
+ preconnectOrigin(chunkHKN3YG76_js.DEFAULT_CDN_BASE_URL);
1293
1302
  }
1294
1303
  const setCurrentLocation = React14.useCallback(
1295
1304
  (location) => {
@@ -3448,7 +3457,7 @@ function CimplifySignInButton({
3448
3457
  const onClick = async () => {
3449
3458
  if (busy || disabled || !signInOpts.clientId) return;
3450
3459
  setBusy(true);
3451
- await chunkGHFDYM5K_js.startSignIn({
3460
+ await chunkVEOAMBEX_js.startSignIn({
3452
3461
  ...signInOpts,
3453
3462
  onSuccess: () => {
3454
3463
  setBusy(false);
@@ -5478,14 +5487,24 @@ function useServices(options = {}) {
5478
5487
  }
5479
5488
 
5480
5489
  // src/react/hooks/use-available-slots.ts
5490
+ function narrowingUnits(units) {
5491
+ return units != null && units > 1 ? units : void 0;
5492
+ }
5481
5493
  var slotsQuery = createCachedQuery({
5482
5494
  name: "useAvailableSlots",
5483
- key: (client, options) => cacheKey(client.getPublicKey(), options.serviceId, options.date, options.participantCount),
5495
+ key: (client, options) => cacheKey(
5496
+ client.getPublicKey(),
5497
+ options.serviceId,
5498
+ options.date,
5499
+ options.participantCount,
5500
+ narrowingUnits(options.units)
5501
+ ),
5484
5502
  fetch: async (client, options) => {
5485
5503
  const result = await client.scheduling.getAvailableSlots({
5486
5504
  service_id: options.serviceId,
5487
5505
  date: options.date,
5488
- participant_count: options.participantCount
5506
+ participant_count: options.participantCount,
5507
+ units: narrowingUnits(options.units)
5489
5508
  });
5490
5509
  if (!result.ok) throw result.error;
5491
5510
  return result.value;
@@ -5502,6 +5521,9 @@ function useAvailableSlots(serviceId, date, options = {}) {
5502
5521
  }
5503
5522
 
5504
5523
  // src/react/hooks/use-service-availability.ts
5524
+ function narrowingUnits2(units) {
5525
+ return units != null && units > 1 ? units : void 0;
5526
+ }
5505
5527
  var serviceAvailabilityQuery = createCachedQuery({
5506
5528
  name: "useServiceAvailability",
5507
5529
  key: (client, options) => cacheKey(
@@ -5509,7 +5531,8 @@ var serviceAvailabilityQuery = createCachedQuery({
5509
5531
  options.serviceId,
5510
5532
  options.startDate,
5511
5533
  options.endDate,
5512
- options.participantCount
5534
+ options.participantCount,
5535
+ narrowingUnits2(options.units)
5513
5536
  ),
5514
5537
  fetch: async (client, options) => {
5515
5538
  const result = await client.scheduling.getServiceAvailability({
@@ -5517,7 +5540,8 @@ var serviceAvailabilityQuery = createCachedQuery({
5517
5540
  start_date: options.startDate,
5518
5541
  end_date: options.endDate,
5519
5542
  location_id: options.locationId,
5520
- participant_count: options.participantCount
5543
+ participant_count: options.participantCount,
5544
+ units: narrowingUnits2(options.units)
5521
5545
  });
5522
5546
  if (!result.ok) throw result.error;
5523
5547
  return result.value.availability;
@@ -5889,6 +5913,20 @@ var IDLE_TIMEOUT_MS = 6e4;
5889
5913
  var WS_MAX_RECONNECT_ATTEMPTS = 10;
5890
5914
  var WS_PATH = "/api/v1/support/conversation/ws";
5891
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
+ }
5892
5930
  function useChat(options = {}) {
5893
5931
  const context = useOptionalCimplifyClient();
5894
5932
  const client = options.client ?? context?.client;
@@ -5901,6 +5939,9 @@ function useChat(options = {}) {
5901
5939
  const [awaitingReply, setAwaitingReply] = React14.useState(false);
5902
5940
  const [seenAt, setSeenAt] = React14.useState(null);
5903
5941
  const [mayHaveOlder, setMayHaveOlder] = React14.useState(false);
5942
+ const [claimedAs, setClaimedAs] = React14.useState(
5943
+ () => client?.support.claimedCustomerName ?? null
5944
+ );
5904
5945
  const pollRef = React14.useRef(null);
5905
5946
  const lastActivityRef = React14.useRef(Date.now());
5906
5947
  const openedRef = React14.useRef(false);
@@ -5912,6 +5953,7 @@ function useChat(options = {}) {
5912
5953
  const aiEnabledRef = React14.useRef(false);
5913
5954
  const humanRequestedRef = React14.useRef(false);
5914
5955
  const awaitReplyTimerRef = React14.useRef(null);
5956
+ const claimUnsubRef = React14.useRef(null);
5915
5957
  messagesRef.current = messages;
5916
5958
  clientRef.current = client;
5917
5959
  humanRequestedRef.current = humanRequested;
@@ -5937,18 +5979,22 @@ function useChat(options = {}) {
5937
5979
  if (incoming.some((m) => m.sender_type === "bot" || m.sender_type === "agent")) {
5938
5980
  stopAwaitingReply();
5939
5981
  }
5940
- setMessages((prev) => {
5941
- const ids = new Set(prev.map((m) => m.id));
5942
- const fresh = incoming.filter((m) => !ids.has(m.id));
5943
- return fresh.length > 0 ? [...prev, ...fresh] : prev;
5944
- });
5982
+ if (incoming.length > 0) lastActivityRef.current = Date.now();
5983
+ setMessages((prev) => mergeMessages(prev, incoming));
5945
5984
  },
5946
5985
  [stopAwaitingReply]
5947
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
+ }, []);
5948
5994
  const fetchNewMessages = React14.useCallback(async () => {
5949
5995
  const c = clientRef.current;
5950
5996
  if (!c) return;
5951
- const last = messagesRef.current[messagesRef.current.length - 1];
5997
+ const last = lastConfirmedMessage();
5952
5998
  const result = await c.support.getMessages({
5953
5999
  after: last?.created_at,
5954
6000
  limit: 50
@@ -5956,29 +6002,61 @@ function useChat(options = {}) {
5956
6002
  if (result.ok && result.value.length > 0) {
5957
6003
  appendMessages(result.value);
5958
6004
  }
5959
- }, [appendMessages]);
5960
- const pollMessages = React14.useCallback(async () => {
5961
- if (Date.now() - lastActivityRef.current > IDLE_TIMEOUT_MS) return;
5962
- await fetchNewMessages();
5963
- }, [fetchNewMessages]);
6005
+ }, [appendMessages, lastConfirmedMessage]);
5964
6006
  const startPolling = React14.useCallback(() => {
5965
6007
  if (pollRef.current) clearInterval(pollRef.current);
5966
- const interval = Date.now() - lastActivityRef.current > IDLE_TIMEOUT_MS ? pollIdleInterval : pollInterval;
5967
- pollRef.current = setInterval(pollMessages, interval);
5968
- }, [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]);
5969
6022
  const stopPolling = React14.useCallback(() => {
5970
6023
  if (pollRef.current) {
5971
6024
  clearInterval(pollRef.current);
5972
6025
  pollRef.current = null;
5973
6026
  }
5974
6027
  }, []);
6028
+ const resyncConversation = React14.useCallback(async () => {
6029
+ const c = clientRef.current;
6030
+ if (!c || !openedRef.current) return;
6031
+ const result = await c.support.openConversation();
6032
+ if (!result.ok) return;
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
+ });
6042
+ aiEnabledRef.current = result.value.conversation.ai_enabled;
6043
+ setHumanRequested(result.value.conversation.needs_human_review ?? false);
6044
+ setSeenAt(result.value.conversation.team_read_at ?? null);
6045
+ setMayHaveOlder(result.value.messages.length >= 50);
6046
+ }, []);
5975
6047
  const connectWebSocket = React14.useCallback(() => {
5976
6048
  const c = clientRef.current;
5977
6049
  if (!c || typeof WebSocket === "undefined") {
5978
6050
  startPolling();
5979
6051
  return;
5980
6052
  }
6053
+ if (wsReconnectTimerRef.current) {
6054
+ clearTimeout(wsReconnectTimerRef.current);
6055
+ wsReconnectTimerRef.current = null;
6056
+ }
5981
6057
  if (wsRef.current) {
6058
+ wsRef.current.onclose = null;
6059
+ wsRef.current.onmessage = null;
5982
6060
  wsRef.current.close();
5983
6061
  wsRef.current = null;
5984
6062
  }
@@ -6002,10 +6080,12 @@ function useChat(options = {}) {
6002
6080
  else stopAwaitingReply();
6003
6081
  } else if (envelope.type === "resync") {
6004
6082
  const latest = envelope.latest_message_at;
6005
- const last = messagesRef.current[messagesRef.current.length - 1];
6083
+ const last = lastConfirmedMessage();
6006
6084
  if (latest && (!last || new Date(last.created_at).getTime() < new Date(latest).getTime())) {
6007
6085
  fetchNewMessages();
6008
6086
  }
6087
+ } else if (envelope.type === "rekey") {
6088
+ void resyncConversation();
6009
6089
  }
6010
6090
  } catch {
6011
6091
  }
@@ -6017,6 +6097,7 @@ function useChat(options = {}) {
6017
6097
  if (wsReconnectAttemptRef.current < WS_MAX_RECONNECT_ATTEMPTS) {
6018
6098
  const attempt = wsReconnectAttemptRef.current++;
6019
6099
  const delay = Math.min(1e3 * Math.pow(2, attempt) + Math.random() * 500, 3e4);
6100
+ if (wsReconnectTimerRef.current) clearTimeout(wsReconnectTimerRef.current);
6020
6101
  wsReconnectTimerRef.current = setTimeout(connectWebSocket, delay);
6021
6102
  } else {
6022
6103
  startPolling();
@@ -6025,14 +6106,91 @@ function useChat(options = {}) {
6025
6106
  ws.onerror = () => {
6026
6107
  };
6027
6108
  wsRef.current = ws;
6028
- }, [appendMessages, startPolling, stopPolling, fetchNewMessages]);
6109
+ }, [appendMessages, startPolling, stopPolling, fetchNewMessages, resyncConversation, lastConfirmedMessage]);
6110
+ const resetConversation = React14.useCallback(() => {
6111
+ stopPolling();
6112
+ if (wsReconnectTimerRef.current) {
6113
+ clearTimeout(wsReconnectTimerRef.current);
6114
+ wsReconnectTimerRef.current = null;
6115
+ }
6116
+ if (wsRef.current) {
6117
+ wsRef.current.onclose = null;
6118
+ wsRef.current.close();
6119
+ wsRef.current = null;
6120
+ }
6121
+ wsReconnectAttemptRef.current = 0;
6122
+ openedRef.current = false;
6123
+ stopAwaitingReply();
6124
+ setMessages([]);
6125
+ setConversationOpened(false);
6126
+ setHumanRequested(false);
6127
+ setSeenAt(null);
6128
+ setMayHaveOlder(false);
6129
+ setError(null);
6130
+ setClaimedAs(null);
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]);
6159
+ const watchSessionClaims = React14.useCallback(() => {
6160
+ const c = clientRef.current;
6161
+ if (!c || claimUnsubRef.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
+ }
6168
+ connectWebSocket();
6169
+ void resyncConversation();
6170
+ });
6171
+ const unsubCleared = c.events.on(chunkQLGZIBRO_js.SESSION_CLEARED, resetConversation);
6172
+ claimUnsubRef.current = () => {
6173
+ unsubClaimed();
6174
+ unsubCleared();
6175
+ };
6176
+ }, [connectWebSocket, resyncConversation, resetConversation, openIfExisting]);
6177
+ const signOut = React14.useCallback(async () => {
6178
+ const c = clientRef.current;
6179
+ if (!c) return false;
6180
+ const result = await c.link.logout();
6181
+ return result.ok;
6182
+ }, []);
6029
6183
  React14.useEffect(() => {
6184
+ watchSessionClaims();
6030
6185
  return () => {
6031
6186
  if (pollRef.current) clearInterval(pollRef.current);
6032
6187
  if (wsReconnectTimerRef.current) clearTimeout(wsReconnectTimerRef.current);
6033
6188
  if (awaitReplyTimerRef.current) clearTimeout(awaitReplyTimerRef.current);
6189
+ claimUnsubRef.current?.();
6190
+ claimUnsubRef.current = null;
6034
6191
  if (wsRef.current) {
6035
6192
  wsRef.current.onclose = null;
6193
+ wsRef.current.onmessage = null;
6036
6194
  wsRef.current.close();
6037
6195
  }
6038
6196
  };
@@ -6044,19 +6202,13 @@ function useChat(options = {}) {
6044
6202
  setError(null);
6045
6203
  const result = await client.support.openConversation();
6046
6204
  if (result.ok) {
6047
- setMessages(result.value.messages);
6048
- setConversationOpened(true);
6049
- aiEnabledRef.current = result.value.conversation.ai_enabled;
6050
- setHumanRequested(result.value.conversation.needs_human_review ?? false);
6051
- setSeenAt(result.value.conversation.team_read_at ?? null);
6052
- setMayHaveOlder(result.value.messages.length >= 50);
6053
- connectWebSocket();
6205
+ hydrateConversation(result.value);
6054
6206
  } else {
6055
6207
  openedRef.current = false;
6056
6208
  setError(result.error);
6057
6209
  }
6058
6210
  setIsLoading(false);
6059
- }, [client, connectWebSocket]);
6211
+ }, [client, hydrateConversation]);
6060
6212
  const send = React14.useCallback(
6061
6213
  async (content, opts) => {
6062
6214
  const attachments = opts?.attachments ?? [];
@@ -6068,7 +6220,8 @@ function useChat(options = {}) {
6068
6220
  if (!openedRef.current) return false;
6069
6221
  }
6070
6222
  const replyTo = opts?.replyTo;
6071
- const optimisticId = `opt_${Date.now()}`;
6223
+ const clientId = chunkVEOAMBEX_js.generateIdempotencyKey();
6224
+ const optimisticId = `opt_${clientId}`;
6072
6225
  const optimistic = {
6073
6226
  id: optimisticId,
6074
6227
  sender_type: "customer",
@@ -6076,6 +6229,7 @@ function useChat(options = {}) {
6076
6229
  content_type: attachments.length > 0 ? "image" : "text",
6077
6230
  attachments,
6078
6231
  metadata: {},
6232
+ client_id: clientId,
6079
6233
  // Same shape the server echoes back, so the bubble doesn't reflow
6080
6234
  // when the confirmed message replaces it.
6081
6235
  reply_to: replyTo ? {
@@ -6088,6 +6242,7 @@ function useChat(options = {}) {
6088
6242
  setMessages((prev) => [...prev, optimistic]);
6089
6243
  setIsSending(true);
6090
6244
  const result = await client.support.sendMessage(content.trim(), {
6245
+ idempotencyKey: clientId,
6091
6246
  replyToMessageId: replyTo?.id,
6092
6247
  attachments: attachments.length > 0 ? attachments : void 0
6093
6248
  });
@@ -6158,11 +6313,7 @@ function useChat(options = {}) {
6158
6313
  const older = result.value;
6159
6314
  setMayHaveOlder(older.length >= 50);
6160
6315
  if (older.length === 0) return false;
6161
- setMessages((prev) => {
6162
- const ids = new Set(prev.map((m) => m.id));
6163
- const fresh = older.filter((m) => !ids.has(m.id));
6164
- return fresh.length > 0 ? [...fresh, ...prev] : prev;
6165
- });
6316
+ setMessages((prev) => mergeMessages(prev, older));
6166
6317
  return true;
6167
6318
  }, []);
6168
6319
  const agentArrived = messages.some((m) => m.sender_type === "agent");
@@ -6176,11 +6327,14 @@ function useChat(options = {}) {
6176
6327
  awaitingReply,
6177
6328
  seenAt,
6178
6329
  mayHaveOlder,
6330
+ claimedAs,
6331
+ signOut,
6179
6332
  send,
6180
6333
  startConversation,
6181
6334
  react,
6182
6335
  requestHuman,
6183
- loadOlder
6336
+ loadOlder,
6337
+ openIfExisting
6184
6338
  };
6185
6339
  }
6186
6340
  var ElementsContext = React14.createContext({
@@ -6255,15 +6409,15 @@ function AuthElement({
6255
6409
  }, [onReady, onAuthenticated, onError]);
6256
6410
  React14.useEffect(() => {
6257
6411
  if (!elements || !containerRef.current) return;
6258
- const element = elements.create(chunkGHFDYM5K_js.ELEMENT_TYPES.AUTH, { prefillEmail });
6412
+ const element = elements.create(chunkVEOAMBEX_js.ELEMENT_TYPES.AUTH, { prefillEmail });
6259
6413
  elementRef.current = element;
6260
- element.on(chunkGHFDYM5K_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6414
+ element.on(chunkVEOAMBEX_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6261
6415
  element.on(
6262
- chunkGHFDYM5K_js.EVENT_TYPES.AUTHENTICATED,
6416
+ chunkVEOAMBEX_js.EVENT_TYPES.AUTHENTICATED,
6263
6417
  (data) => onAuthenticatedRef.current?.(data)
6264
6418
  );
6265
6419
  element.on(
6266
- chunkGHFDYM5K_js.EVENT_TYPES.ERROR,
6420
+ chunkVEOAMBEX_js.EVENT_TYPES.ERROR,
6267
6421
  (data) => onErrorRef.current?.(data)
6268
6422
  );
6269
6423
  element.mount(containerRef.current);
@@ -6292,15 +6446,15 @@ function AddressElement({
6292
6446
  }, [onReady, onChange, onError]);
6293
6447
  React14.useEffect(() => {
6294
6448
  if (!elements || !containerRef.current) return;
6295
- const element = elements.create(chunkGHFDYM5K_js.ELEMENT_TYPES.ADDRESS, { mode });
6449
+ const element = elements.create(chunkVEOAMBEX_js.ELEMENT_TYPES.ADDRESS, { mode });
6296
6450
  elementRef.current = element;
6297
- element.on(chunkGHFDYM5K_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6451
+ element.on(chunkVEOAMBEX_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6298
6452
  element.on(
6299
- chunkGHFDYM5K_js.EVENT_TYPES.CHANGE,
6453
+ chunkVEOAMBEX_js.EVENT_TYPES.CHANGE,
6300
6454
  (data) => onChangeRef.current?.(data)
6301
6455
  );
6302
6456
  element.on(
6303
- chunkGHFDYM5K_js.EVENT_TYPES.ERROR,
6457
+ chunkVEOAMBEX_js.EVENT_TYPES.ERROR,
6304
6458
  (data) => onErrorRef.current?.(data)
6305
6459
  );
6306
6460
  element.mount(containerRef.current);
@@ -6330,15 +6484,15 @@ function PaymentElement({
6330
6484
  }, [onReady, onChange, onError]);
6331
6485
  React14.useEffect(() => {
6332
6486
  if (!elements || !containerRef.current) return;
6333
- const element = elements.create(chunkGHFDYM5K_js.ELEMENT_TYPES.PAYMENT, { amount, currency });
6487
+ const element = elements.create(chunkVEOAMBEX_js.ELEMENT_TYPES.PAYMENT, { amount, currency });
6334
6488
  elementRef.current = element;
6335
- element.on(chunkGHFDYM5K_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6489
+ element.on(chunkVEOAMBEX_js.EVENT_TYPES.READY, () => onReadyRef.current?.());
6336
6490
  element.on(
6337
- chunkGHFDYM5K_js.EVENT_TYPES.CHANGE,
6491
+ chunkVEOAMBEX_js.EVENT_TYPES.CHANGE,
6338
6492
  (data) => onChangeRef.current?.(data)
6339
6493
  );
6340
6494
  element.on(
6341
- chunkGHFDYM5K_js.EVENT_TYPES.ERROR,
6495
+ chunkVEOAMBEX_js.EVENT_TYPES.ERROR,
6342
6496
  (data) => onErrorRef.current?.(data)
6343
6497
  );
6344
6498
  element.mount(containerRef.current);
@@ -6629,7 +6783,7 @@ function VariantSelector({
6629
6783
  {
6630
6784
  "data-cimplify-variant-name": true,
6631
6785
  className: cn("flex-1 min-w-0 text-sm", classNames?.name),
6632
- children: chunkT7Z25EY4_js.getVariantDisplayName(variant, productName)
6786
+ children: chunkHKN3YG76_js.getVariantDisplayName(variant, productName)
6633
6787
  }
6634
6788
  ),
6635
6789
  /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-cimplify-variant-pricing": true, className: cn("text-sm text-muted-foreground", classNames?.pricing), children: [
@@ -7910,6 +8064,177 @@ function VolumePricing({
7910
8064
  }
7911
8065
  );
7912
8066
  }
8067
+ function createPlacesSessionToken() {
8068
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
8069
+ return crypto.randomUUID();
8070
+ }
8071
+ return `places_${Date.now()}_${Math.random().toString(16).slice(2)}`;
8072
+ }
8073
+ var MIN_QUERY_LENGTH = 3;
8074
+ var DEBOUNCE_MS = 300;
8075
+ var BLUR_CLOSE_MS = 120;
8076
+ function AddressAutocomplete({
8077
+ value,
8078
+ onSelect,
8079
+ onDirty,
8080
+ placeholder,
8081
+ required,
8082
+ autoFocus,
8083
+ id,
8084
+ className,
8085
+ classNames
8086
+ }) {
8087
+ const clientContext = useOptionalCimplifyClient();
8088
+ const [inputText, setInputText] = React14__default.default.useState(value?.formatted_address ?? "");
8089
+ const [suggestions, setSuggestions] = React14__default.default.useState([]);
8090
+ const [isOpen, setIsOpen] = React14__default.default.useState(false);
8091
+ const [highlightedIndex, setHighlightedIndex] = React14__default.default.useState(-1);
8092
+ const debounceRef = React14__default.default.useRef(null);
8093
+ const seqRef = React14__default.default.useRef(0);
8094
+ const sessionTokenRef = React14__default.default.useRef(createPlacesSessionToken());
8095
+ const blurTimeoutRef = React14__default.default.useRef(null);
8096
+ React14__default.default.useEffect(() => {
8097
+ return () => {
8098
+ if (debounceRef.current) clearTimeout(debounceRef.current);
8099
+ if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8100
+ seqRef.current += 1;
8101
+ };
8102
+ }, []);
8103
+ const fetchSuggestions = React14__default.default.useCallback(
8104
+ async (query) => {
8105
+ const client = clientContext?.client;
8106
+ if (!client || query.length < MIN_QUERY_LENGTH) {
8107
+ setSuggestions([]);
8108
+ setIsOpen(false);
8109
+ return;
8110
+ }
8111
+ const seq = ++seqRef.current;
8112
+ const result = await client.places.autocomplete(query, sessionTokenRef.current);
8113
+ if (seqRef.current !== seq) return;
8114
+ if (result.ok && result.value.predictions.length > 0) {
8115
+ setSuggestions(result.value.predictions);
8116
+ setHighlightedIndex(0);
8117
+ setIsOpen(true);
8118
+ } else {
8119
+ setSuggestions([]);
8120
+ setIsOpen(false);
8121
+ }
8122
+ },
8123
+ [clientContext]
8124
+ );
8125
+ const handleInputChange = (nextValue) => {
8126
+ setInputText(nextValue);
8127
+ onDirty?.();
8128
+ if (debounceRef.current) clearTimeout(debounceRef.current);
8129
+ debounceRef.current = setTimeout(() => void fetchSuggestions(nextValue.trim()), DEBOUNCE_MS);
8130
+ };
8131
+ const selectSuggestion = React14__default.default.useCallback(
8132
+ async (suggestion) => {
8133
+ if (debounceRef.current) clearTimeout(debounceRef.current);
8134
+ if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8135
+ const fallback = suggestion.description.split(",")[0]?.trim() || suggestion.description;
8136
+ setInputText(fallback);
8137
+ setSuggestions([]);
8138
+ setIsOpen(false);
8139
+ const client = clientContext?.client;
8140
+ if (!client) return;
8141
+ const result = await client.places.details(suggestion.place_id, sessionTokenRef.current);
8142
+ sessionTokenRef.current = createPlacesSessionToken();
8143
+ if (result.ok) {
8144
+ const d = result.value;
8145
+ const address = {
8146
+ formatted_address: d.formatted_address || fallback,
8147
+ street_address: d.street_address,
8148
+ apartment: d.apartment,
8149
+ city: d.city,
8150
+ region: d.region,
8151
+ postal_code: d.postal_code,
8152
+ country: d.country,
8153
+ latitude: d.latitude,
8154
+ longitude: d.longitude,
8155
+ place_id: d.place_id
8156
+ };
8157
+ setInputText(address.formatted_address);
8158
+ onSelect(address);
8159
+ }
8160
+ },
8161
+ [clientContext, onSelect]
8162
+ );
8163
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-cimplify-address-autocomplete": true, className: cn("relative", className, classNames?.root), children: [
8164
+ /* @__PURE__ */ jsxRuntime.jsx(
8165
+ "input",
8166
+ {
8167
+ id,
8168
+ type: "text",
8169
+ value: inputText,
8170
+ onChange: (e) => handleInputChange(e.target.value),
8171
+ onFocus: () => {
8172
+ if (suggestions.length > 0) setIsOpen(true);
8173
+ },
8174
+ onBlur: () => {
8175
+ blurTimeoutRef.current = setTimeout(() => setIsOpen(false), BLUR_CLOSE_MS);
8176
+ },
8177
+ onKeyDown: (e) => {
8178
+ if (e.key === "Escape") {
8179
+ setIsOpen(false);
8180
+ return;
8181
+ }
8182
+ if (!isOpen || suggestions.length === 0) return;
8183
+ if (e.key === "ArrowDown") {
8184
+ e.preventDefault();
8185
+ setHighlightedIndex((i) => i < suggestions.length - 1 ? i + 1 : 0);
8186
+ } else if (e.key === "ArrowUp") {
8187
+ e.preventDefault();
8188
+ setHighlightedIndex((i) => i > 0 ? i - 1 : suggestions.length - 1);
8189
+ } else if (e.key === "Enter" && highlightedIndex >= 0) {
8190
+ e.preventDefault();
8191
+ void selectSuggestion(suggestions[highlightedIndex]);
8192
+ }
8193
+ },
8194
+ placeholder: placeholder ?? "Search for an address...",
8195
+ required,
8196
+ autoFocus,
8197
+ autoComplete: "off",
8198
+ "aria-autocomplete": "list",
8199
+ "aria-expanded": isOpen && suggestions.length > 0,
8200
+ "data-cimplify-address-input": true,
8201
+ className: classNames?.input
8202
+ }
8203
+ ),
8204
+ isOpen && suggestions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
8205
+ "div",
8206
+ {
8207
+ role: "listbox",
8208
+ "data-cimplify-address-list": true,
8209
+ className: cn(
8210
+ "absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-border bg-background shadow-lg",
8211
+ classNames?.list
8212
+ ),
8213
+ children: suggestions.map((s, i) => /* @__PURE__ */ jsxRuntime.jsx(
8214
+ "button",
8215
+ {
8216
+ type: "button",
8217
+ role: "option",
8218
+ "aria-selected": i === highlightedIndex,
8219
+ onMouseDown: (e) => {
8220
+ e.preventDefault();
8221
+ void selectSuggestion(s);
8222
+ },
8223
+ "data-cimplify-address-option": true,
8224
+ className: cn(
8225
+ "block w-full px-3 py-2.5 text-left text-sm transition-colors",
8226
+ i === highlightedIndex ? "bg-muted text-foreground" : "text-foreground hover:bg-muted/70",
8227
+ classNames?.option,
8228
+ i === highlightedIndex && classNames?.activeOption
8229
+ ),
8230
+ children: s.description
8231
+ },
8232
+ s.place_id
8233
+ ))
8234
+ }
8235
+ )
8236
+ ] });
8237
+ }
7913
8238
  function parseValue(value) {
7914
8239
  if (!value) return void 0;
7915
8240
  const date = dateFns.parseISO(value);
@@ -8251,12 +8576,12 @@ function FieldInput({
8251
8576
  classNames?.input
8252
8577
  );
8253
8578
  switch (field.field_type) {
8254
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Text:
8255
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Url:
8579
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.Text:
8580
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.Url:
8256
8581
  return /* @__PURE__ */ jsxRuntime.jsx(
8257
8582
  "input",
8258
8583
  {
8259
- type: field.field_type === chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Url ? "url" : "text",
8584
+ type: field.field_type === chunkHKN3YG76_js.INPUT_FIELD_TYPE.Url ? "url" : "text",
8260
8585
  value: typeof value === "string" ? value : "",
8261
8586
  onChange: (e) => onValueChange(e.target.value),
8262
8587
  placeholder: field.placeholder,
@@ -8265,7 +8590,7 @@ function FieldInput({
8265
8590
  className: inputClass
8266
8591
  }
8267
8592
  );
8268
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Textarea:
8593
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.Textarea:
8269
8594
  return /* @__PURE__ */ jsxRuntime.jsx(
8270
8595
  "textarea",
8271
8596
  {
@@ -8278,7 +8603,7 @@ function FieldInput({
8278
8603
  className: cn(inputClass, "resize-none", classNames?.textarea)
8279
8604
  }
8280
8605
  );
8281
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Number:
8606
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.Number:
8282
8607
  return /* @__PURE__ */ jsxRuntime.jsx(
8283
8608
  "input",
8284
8609
  {
@@ -8292,7 +8617,7 @@ function FieldInput({
8292
8617
  className: inputClass
8293
8618
  }
8294
8619
  );
8295
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Select:
8620
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.Select:
8296
8621
  return /* @__PURE__ */ jsxRuntime.jsxs(
8297
8622
  "select",
8298
8623
  {
@@ -8306,7 +8631,7 @@ function FieldInput({
8306
8631
  ]
8307
8632
  }
8308
8633
  );
8309
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Radio:
8634
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.Radio:
8310
8635
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("space-y-2", classNames?.radioGroup), children: (field.options || []).map((opt) => /* @__PURE__ */ jsxRuntime.jsxs(
8311
8636
  "label",
8312
8637
  {
@@ -8331,7 +8656,7 @@ function FieldInput({
8331
8656
  },
8332
8657
  opt
8333
8658
  )) });
8334
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Checkbox:
8659
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.Checkbox:
8335
8660
  return /* @__PURE__ */ jsxRuntime.jsxs("label", { className: cn("flex items-center gap-2 text-sm cursor-pointer", classNames?.checkboxLabel), children: [
8336
8661
  /* @__PURE__ */ jsxRuntime.jsx(
8337
8662
  "input",
@@ -8344,7 +8669,7 @@ function FieldInput({
8344
8669
  ),
8345
8670
  field.placeholder || field.name
8346
8671
  ] });
8347
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Color:
8672
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.Color:
8348
8673
  return /* @__PURE__ */ jsxRuntime.jsx(
8349
8674
  "input",
8350
8675
  {
@@ -8354,7 +8679,7 @@ function FieldInput({
8354
8679
  className: cn("w-12 h-10 rounded-md border border-input cursor-pointer", classNames?.colorInput)
8355
8680
  }
8356
8681
  );
8357
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Date:
8682
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.Date:
8358
8683
  return /* @__PURE__ */ jsxRuntime.jsx(
8359
8684
  DatePicker,
8360
8685
  {
@@ -8365,8 +8690,8 @@ function FieldInput({
8365
8690
  "aria-label": field.name
8366
8691
  }
8367
8692
  );
8368
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.File:
8369
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Image:
8693
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.File:
8694
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.Image:
8370
8695
  return /* @__PURE__ */ jsxRuntime.jsx(
8371
8696
  FileUploadInput,
8372
8697
  {
@@ -8377,7 +8702,7 @@ function FieldInput({
8377
8702
  classNames
8378
8703
  }
8379
8704
  );
8380
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Email:
8705
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.Email:
8381
8706
  return /* @__PURE__ */ jsxRuntime.jsx(
8382
8707
  "input",
8383
8708
  {
@@ -8389,7 +8714,7 @@ function FieldInput({
8389
8714
  className: inputClass
8390
8715
  }
8391
8716
  );
8392
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.DateTime: {
8717
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.DateTime: {
8393
8718
  const stringValue = typeof value === "string" ? value : "";
8394
8719
  const [datePart, timePartRaw] = stringValue.includes("T") ? stringValue.split("T", 2) : [stringValue, ""];
8395
8720
  const timePart = (timePartRaw ?? "").slice(0, 5);
@@ -8428,7 +8753,7 @@ function FieldInput({
8428
8753
  )
8429
8754
  ] });
8430
8755
  }
8431
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Time:
8756
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.Time:
8432
8757
  return /* @__PURE__ */ jsxRuntime.jsx(
8433
8758
  TimePicker,
8434
8759
  {
@@ -8439,7 +8764,7 @@ function FieldInput({
8439
8764
  "aria-label": field.name
8440
8765
  }
8441
8766
  );
8442
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.MultiSelect:
8767
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.MultiSelect:
8443
8768
  return /* @__PURE__ */ jsxRuntime.jsx(
8444
8769
  MultiSelectInput,
8445
8770
  {
@@ -8449,7 +8774,7 @@ function FieldInput({
8449
8774
  classNames
8450
8775
  }
8451
8776
  );
8452
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.DateRange:
8777
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.DateRange:
8453
8778
  return /* @__PURE__ */ jsxRuntime.jsx(
8454
8779
  DateRangeInput,
8455
8780
  {
@@ -8459,7 +8784,7 @@ function FieldInput({
8459
8784
  classNames
8460
8785
  }
8461
8786
  );
8462
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Address:
8787
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.Address:
8463
8788
  return /* @__PURE__ */ jsxRuntime.jsx(
8464
8789
  AddressInput,
8465
8790
  {
@@ -8470,7 +8795,7 @@ function FieldInput({
8470
8795
  inputClass
8471
8796
  }
8472
8797
  );
8473
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Location:
8798
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.Location:
8474
8799
  return /* @__PURE__ */ jsxRuntime.jsx(
8475
8800
  LocationInput,
8476
8801
  {
@@ -8481,7 +8806,7 @@ function FieldInput({
8481
8806
  inputClass
8482
8807
  }
8483
8808
  );
8484
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Phone:
8809
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.Phone:
8485
8810
  return /* @__PURE__ */ jsxRuntime.jsx(
8486
8811
  PhoneInput,
8487
8812
  {
@@ -8492,7 +8817,7 @@ function FieldInput({
8492
8817
  inputClass
8493
8818
  }
8494
8819
  );
8495
- case chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Signature:
8820
+ case chunkHKN3YG76_js.INPUT_FIELD_TYPE.Signature:
8496
8821
  return /* @__PURE__ */ jsxRuntime.jsx(
8497
8822
  SignatureInput,
8498
8823
  {
@@ -8525,7 +8850,7 @@ function FileUploadInput({
8525
8850
  const [isUploading, setIsUploading] = React14__default.default.useState(false);
8526
8851
  const fileUrl = typeof value === "string" ? value : void 0;
8527
8852
  const acceptedFormats = field.validation?.accepted_formats;
8528
- const accept = acceptedFormats ? acceptedFormats.map((f) => `.${f}`).join(",") : field.field_type === chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Image ? "image/*" : void 0;
8853
+ const accept = acceptedFormats ? acceptedFormats.map((f) => `.${f}`).join(",") : field.field_type === chunkHKN3YG76_js.INPUT_FIELD_TYPE.Image ? "image/*" : void 0;
8529
8854
  const handleFile = async (file) => {
8530
8855
  if (!onFileUpload) return;
8531
8856
  if (field.validation?.max_size_mb) {
@@ -8543,7 +8868,7 @@ function FileUploadInput({
8543
8868
  }
8544
8869
  };
8545
8870
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("space-y-2", classNames?.fileInput), children: fileUrl ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
8546
- field.field_type === chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Image && /* @__PURE__ */ jsxRuntime.jsx("img", { src: fileUrl, alt: "Uploaded", className: "w-16 h-16 object-cover rounded-md border border-border" }),
8871
+ field.field_type === chunkHKN3YG76_js.INPUT_FIELD_TYPE.Image && /* @__PURE__ */ jsxRuntime.jsx("img", { src: fileUrl, alt: "Uploaded", className: "w-16 h-16 object-cover rounded-md border border-border" }),
8547
8872
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-foreground truncate", children: fileUrl.split("/").pop() }) }),
8548
8873
  /* @__PURE__ */ jsxRuntime.jsx(
8549
8874
  "button",
@@ -8569,7 +8894,7 @@ function FileUploadInput({
8569
8894
  }
8570
8895
  ),
8571
8896
  isUploading ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-muted-foreground", children: "Uploading..." }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
8572
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-muted-foreground", children: field.placeholder || `Upload ${field.field_type === chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Image ? "image" : "file"}` }),
8897
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm text-muted-foreground", children: field.placeholder || `Upload ${field.field_type === chunkHKN3YG76_js.INPUT_FIELD_TYPE.Image ? "image" : "file"}` }),
8573
8898
  acceptedFormats && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground/60 mt-1", children: [
8574
8899
  acceptedFormats.map((f) => f.toUpperCase()).join(", "),
8575
8900
  field.validation?.max_size_mb && ` \xB7 Max ${field.validation.max_size_mb}MB`
@@ -8670,129 +8995,18 @@ function AddressInput({
8670
8995
  classNames,
8671
8996
  inputClass
8672
8997
  }) {
8673
- const clientContext = useOptionalCimplifyClient();
8674
8998
  const addr = value && typeof value === "object" ? value : null;
8675
- const [inputText, setInputText] = React14__default.default.useState(addr?.formatted_address ?? "");
8676
- const [suggestions, setSuggestions] = React14__default.default.useState([]);
8677
- const [isOpen, setIsOpen] = React14__default.default.useState(false);
8678
- const [highlightedIndex, setHighlightedIndex] = React14__default.default.useState(-1);
8679
- const debounceRef = React14__default.default.useRef(null);
8680
- const seqRef = React14__default.default.useRef(0);
8681
- const sessionTokenRef = React14__default.default.useRef(createSessionToken());
8682
- const blurTimeoutRef = React14__default.default.useRef(null);
8683
- React14__default.default.useEffect(() => {
8684
- return () => {
8685
- if (debounceRef.current) clearTimeout(debounceRef.current);
8686
- if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8687
- seqRef.current += 1;
8688
- };
8689
- }, []);
8690
- const fetchSuggestions = React14__default.default.useCallback(async (query) => {
8691
- const client = clientContext?.client;
8692
- if (!client || query.length < 3) {
8693
- setSuggestions([]);
8694
- setIsOpen(false);
8695
- return;
8696
- }
8697
- const seq = ++seqRef.current;
8698
- const result = await client.places.autocomplete(query, sessionTokenRef.current);
8699
- if (seqRef.current !== seq) return;
8700
- if (result.ok && result.value.predictions.length > 0) {
8701
- setSuggestions(result.value.predictions);
8702
- setHighlightedIndex(0);
8703
- setIsOpen(true);
8704
- } else {
8705
- setSuggestions([]);
8706
- setIsOpen(false);
8707
- }
8708
- }, [clientContext]);
8709
- const handleInputChange = (nextValue) => {
8710
- setInputText(nextValue);
8711
- if (debounceRef.current) clearTimeout(debounceRef.current);
8712
- debounceRef.current = setTimeout(() => void fetchSuggestions(nextValue.trim()), 300);
8713
- };
8714
- const selectSuggestion = React14__default.default.useCallback(async (suggestion) => {
8715
- if (debounceRef.current) clearTimeout(debounceRef.current);
8716
- if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8717
- const fallback = suggestion.description.split(",")[0]?.trim() || suggestion.description;
8718
- setInputText(fallback);
8719
- setSuggestions([]);
8720
- setIsOpen(false);
8721
- const client = clientContext?.client;
8722
- if (!client) return;
8723
- const result = await client.places.details(suggestion.place_id, sessionTokenRef.current);
8724
- sessionTokenRef.current = createSessionToken();
8725
- if (result.ok) {
8726
- const d = result.value;
8727
- const addressValue = {
8728
- formatted_address: d.formatted_address || fallback,
8729
- street_address: d.street_address,
8730
- apartment: d.apartment,
8731
- city: d.city,
8732
- region: d.region,
8733
- postal_code: d.postal_code,
8734
- country: d.country,
8735
- latitude: d.latitude,
8736
- longitude: d.longitude,
8737
- place_id: d.place_id
8738
- };
8739
- setInputText(addressValue.formatted_address);
8740
- onValueChange(addressValue);
8741
- }
8742
- }, [clientContext, onValueChange]);
8743
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("space-y-2 relative", classNames?.addressInput), children: [
8999
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("space-y-2", classNames?.addressInput), children: [
8744
9000
  /* @__PURE__ */ jsxRuntime.jsx(
8745
- "input",
9001
+ AddressAutocomplete,
8746
9002
  {
8747
- type: "text",
8748
- value: inputText,
8749
- onChange: (e) => handleInputChange(e.target.value),
8750
- onFocus: () => {
8751
- if (suggestions.length > 0) setIsOpen(true);
8752
- },
8753
- onBlur: () => {
8754
- blurTimeoutRef.current = setTimeout(() => setIsOpen(false), 120);
8755
- },
8756
- onKeyDown: (e) => {
8757
- if (e.key === "Escape") {
8758
- setIsOpen(false);
8759
- return;
8760
- }
8761
- if (!isOpen || suggestions.length === 0) return;
8762
- if (e.key === "ArrowDown") {
8763
- e.preventDefault();
8764
- setHighlightedIndex((i) => i < suggestions.length - 1 ? i + 1 : 0);
8765
- } else if (e.key === "ArrowUp") {
8766
- e.preventDefault();
8767
- setHighlightedIndex((i) => i > 0 ? i - 1 : suggestions.length - 1);
8768
- } else if (e.key === "Enter" && highlightedIndex >= 0) {
8769
- e.preventDefault();
8770
- void selectSuggestion(suggestions[highlightedIndex]);
8771
- }
8772
- },
9003
+ value: addr,
9004
+ onSelect: onValueChange,
8773
9005
  placeholder: field.placeholder || "Search for an address...",
8774
9006
  required: field.is_required,
8775
- autoComplete: "off",
8776
- "aria-autocomplete": "list",
8777
- "aria-expanded": isOpen && suggestions.length > 0,
8778
- className: inputClass
9007
+ classNames: { input: inputClass }
8779
9008
  }
8780
9009
  ),
8781
- isOpen && suggestions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { role: "listbox", className: "absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-border bg-background shadow-lg", children: suggestions.map((s, i) => /* @__PURE__ */ jsxRuntime.jsx(
8782
- "button",
8783
- {
8784
- type: "button",
8785
- role: "option",
8786
- "aria-selected": i === highlightedIndex,
8787
- onMouseDown: (e) => {
8788
- e.preventDefault();
8789
- void selectSuggestion(s);
8790
- },
8791
- className: cn("block w-full px-3 py-2.5 text-left text-sm transition-colors", i === highlightedIndex ? "bg-muted text-foreground" : "text-foreground hover:bg-muted/70"),
8792
- children: s.description
8793
- },
8794
- s.place_id
8795
- )) }),
8796
9010
  addr && /* @__PURE__ */ jsxRuntime.jsx(
8797
9011
  "input",
8798
9012
  {
@@ -8812,121 +9026,22 @@ function LocationInput({
8812
9026
  classNames,
8813
9027
  inputClass
8814
9028
  }) {
8815
- const clientContext = useOptionalCimplifyClient();
8816
9029
  const loc = value && typeof value === "object" ? value : null;
8817
- const [inputText, setInputText] = React14__default.default.useState(loc?.label ?? "");
8818
- const [suggestions, setSuggestions] = React14__default.default.useState([]);
8819
- const [isOpen, setIsOpen] = React14__default.default.useState(false);
8820
- const [highlightedIndex, setHighlightedIndex] = React14__default.default.useState(-1);
8821
- const debounceRef = React14__default.default.useRef(null);
8822
- const seqRef = React14__default.default.useRef(0);
8823
- const sessionTokenRef = React14__default.default.useRef(createSessionToken());
8824
- const blurTimeoutRef = React14__default.default.useRef(null);
8825
- React14__default.default.useEffect(() => {
8826
- return () => {
8827
- if (debounceRef.current) clearTimeout(debounceRef.current);
8828
- if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8829
- seqRef.current += 1;
8830
- };
8831
- }, []);
8832
- const fetchSuggestions = React14__default.default.useCallback(async (query) => {
8833
- const client = clientContext?.client;
8834
- if (!client || query.length < 3) {
8835
- setSuggestions([]);
8836
- setIsOpen(false);
8837
- return;
8838
- }
8839
- const seq = ++seqRef.current;
8840
- const result = await client.places.autocomplete(query, sessionTokenRef.current);
8841
- if (seqRef.current !== seq) return;
8842
- if (result.ok && result.value.predictions.length > 0) {
8843
- setSuggestions(result.value.predictions);
8844
- setHighlightedIndex(0);
8845
- setIsOpen(true);
8846
- } else {
8847
- setSuggestions([]);
8848
- setIsOpen(false);
8849
- }
8850
- }, [clientContext]);
8851
- const handleInputChange = (nextValue) => {
8852
- setInputText(nextValue);
8853
- if (debounceRef.current) clearTimeout(debounceRef.current);
8854
- debounceRef.current = setTimeout(() => void fetchSuggestions(nextValue.trim()), 300);
8855
- };
8856
- const selectSuggestion = React14__default.default.useCallback(async (suggestion) => {
8857
- if (debounceRef.current) clearTimeout(debounceRef.current);
8858
- if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current);
8859
- const label = suggestion.description.split(",")[0]?.trim() || suggestion.description;
8860
- setInputText(label);
8861
- setSuggestions([]);
8862
- setIsOpen(false);
8863
- const client = clientContext?.client;
8864
- if (!client) return;
8865
- const result = await client.places.details(suggestion.place_id, sessionTokenRef.current);
8866
- sessionTokenRef.current = createSessionToken();
8867
- if (result.ok) {
8868
- const d = result.value;
8869
- onValueChange({
8870
- latitude: d.latitude,
8871
- longitude: d.longitude,
8872
- label: d.formatted_address || label
8873
- });
8874
- setInputText(d.formatted_address || label);
8875
- }
8876
- }, [clientContext, onValueChange]);
8877
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("relative", classNames?.locationInput), children: [
9030
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn(classNames?.locationInput), children: [
8878
9031
  /* @__PURE__ */ jsxRuntime.jsx(
8879
- "input",
9032
+ AddressAutocomplete,
8880
9033
  {
8881
- type: "text",
8882
- value: inputText,
8883
- onChange: (e) => handleInputChange(e.target.value),
8884
- onFocus: () => {
8885
- if (suggestions.length > 0) setIsOpen(true);
8886
- },
8887
- onBlur: () => {
8888
- blurTimeoutRef.current = setTimeout(() => setIsOpen(false), 120);
8889
- },
8890
- onKeyDown: (e) => {
8891
- if (e.key === "Escape") {
8892
- setIsOpen(false);
8893
- return;
8894
- }
8895
- if (!isOpen || suggestions.length === 0) return;
8896
- if (e.key === "ArrowDown") {
8897
- e.preventDefault();
8898
- setHighlightedIndex((i) => i < suggestions.length - 1 ? i + 1 : 0);
8899
- } else if (e.key === "ArrowUp") {
8900
- e.preventDefault();
8901
- setHighlightedIndex((i) => i > 0 ? i - 1 : suggestions.length - 1);
8902
- } else if (e.key === "Enter" && highlightedIndex >= 0) {
8903
- e.preventDefault();
8904
- void selectSuggestion(suggestions[highlightedIndex]);
8905
- }
8906
- },
9034
+ value: loc ? { formatted_address: loc.label ?? "", latitude: loc.latitude, longitude: loc.longitude } : null,
9035
+ onSelect: (address) => onValueChange({
9036
+ latitude: address.latitude,
9037
+ longitude: address.longitude,
9038
+ label: address.formatted_address
9039
+ }),
8907
9040
  placeholder: field.placeholder || "Search for a location...",
8908
9041
  required: field.is_required,
8909
- autoComplete: "off",
8910
- "aria-autocomplete": "list",
8911
- "aria-expanded": isOpen && suggestions.length > 0,
8912
- className: inputClass
9042
+ classNames: { input: inputClass }
8913
9043
  }
8914
9044
  ),
8915
- isOpen && suggestions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { role: "listbox", className: "absolute left-0 right-0 top-full z-20 mt-1 overflow-hidden rounded-md border border-border bg-background shadow-lg", children: suggestions.map((s, i) => /* @__PURE__ */ jsxRuntime.jsx(
8916
- "button",
8917
- {
8918
- type: "button",
8919
- role: "option",
8920
- "aria-selected": i === highlightedIndex,
8921
- onMouseDown: (e) => {
8922
- e.preventDefault();
8923
- void selectSuggestion(s);
8924
- },
8925
- className: cn("block w-full px-3 py-2.5 text-left text-sm transition-colors", i === highlightedIndex ? "bg-muted text-foreground" : "text-foreground hover:bg-muted/70"),
8926
- children: s.description
8927
- },
8928
- s.place_id
8929
- )) }),
8930
9045
  loc && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground mt-1", children: [
8931
9046
  loc.latitude.toFixed(4),
8932
9047
  ", ",
@@ -9057,12 +9172,6 @@ function SignatureInput({
9057
9172
  !sig?.data_url && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: field.placeholder || "Draw your signature above" })
9058
9173
  ] });
9059
9174
  }
9060
- function createSessionToken() {
9061
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
9062
- return crypto.randomUUID();
9063
- }
9064
- return `places_${Date.now()}_${Math.random().toString(16).slice(2)}`;
9065
- }
9066
9175
  function getTimeOfDay(timeStr) {
9067
9176
  const hour = parseInt(timeStr.split("T").pop()?.split(":")[0] ?? timeStr.split(":")[0], 10);
9068
9177
  if (hour < 12) return "morning";
@@ -9308,6 +9417,7 @@ function DateSlotPicker({
9308
9417
  serviceId,
9309
9418
  daysToShow = 7,
9310
9419
  participantCount,
9420
+ units,
9311
9421
  selectedSlot,
9312
9422
  onSlotSelect,
9313
9423
  availability: availabilityProp,
@@ -9342,6 +9452,7 @@ function DateSlotPicker({
9342
9452
  dateRange.endDate,
9343
9453
  {
9344
9454
  participantCount,
9455
+ units,
9345
9456
  enabled: availabilityProp === void 0
9346
9457
  }
9347
9458
  );
@@ -9499,6 +9610,7 @@ function ProductCustomizer({
9499
9610
  const [specialInstructions, setSpecialInstructions] = React14.useState("");
9500
9611
  const [selectedSlot, setSelectedSlot] = React14.useState(null);
9501
9612
  const [selectedStaffId, setSelectedStaffId] = React14.useState(null);
9613
+ const [unitsCount, setUnitsCount] = React14.useState(1);
9502
9614
  const cart = useCart();
9503
9615
  const currency = chunkPTPOQVXS_js.getProductCurrency(product);
9504
9616
  const productType = product.type || "product";
@@ -9506,6 +9618,7 @@ function ProductCustomizer({
9506
9618
  const isBundle = productType === "bundle";
9507
9619
  const isDigital = productType === "digital";
9508
9620
  const isService = productType === "service";
9621
+ const offersUnits = isService && chunkHKN3YG76_js.isMultiDayService(product);
9509
9622
  const isStandard = !isComposite && !isBundle;
9510
9623
  const hasVariants = isStandard && product.variants && product.variants.length > 0;
9511
9624
  const hasAddOns = isStandard && product.add_ons && product.add_ons.length > 0;
@@ -9527,6 +9640,7 @@ function ProductCustomizer({
9527
9640
  setSpecialInstructions("");
9528
9641
  setSelectedSlot(null);
9529
9642
  setSelectedStaffId(null);
9643
+ setUnitsCount(1);
9530
9644
  }, [product.id, product.min_order_quantity]);
9531
9645
  const slotStaff = React14.useMemo(
9532
9646
  () => (selectedSlot?.available_staff ?? []).map((member) => ({
@@ -9616,7 +9730,17 @@ function ProductCustomizer({
9616
9730
  if (perUnit === void 0 || perUnit === null) return void 0;
9617
9731
  return chunkPTPOQVXS_js.parsePrice(perUnit) * quantity;
9618
9732
  }, [quote, quantity]);
9619
- const displayTotalPrice = quotedTotalPrice ?? localTotalPrice;
9733
+ const displayTotalPrice = React14.useMemo(() => {
9734
+ const total = quotedTotalPrice ?? localTotalPrice;
9735
+ const multiplier = chunkHKN3YG76_js.serviceBillingMultiplier(product, {
9736
+ quantity,
9737
+ units: unitsCount,
9738
+ scheduledStart: selectedSlot?.start_time,
9739
+ scheduledEnd: selectedSlot?.end_time
9740
+ });
9741
+ if (multiplier === null) return total;
9742
+ return total / Math.max(1, quantity) * multiplier;
9743
+ }, [quotedTotalPrice, localTotalPrice, product, quantity, unitsCount, selectedSlot]);
9620
9744
  const handleVariantChange = React14.useCallback(
9621
9745
  (variantId, variant) => {
9622
9746
  setSelectedVariantId(variantId);
@@ -9648,7 +9772,8 @@ function ProductCustomizer({
9648
9772
  scheduledStart: selectedSlot.start_time,
9649
9773
  scheduledEnd: selectedSlot.end_time,
9650
9774
  staffId: selectedStaffId ?? selectedSlot.available_staff?.[0]?.staff_id,
9651
- resourceId: selectedSlot.available_resources?.[0]?.resource_id
9775
+ resourceId: selectedSlot.available_resources?.[0]?.resource_id,
9776
+ units: unitsCount > 1 ? unitsCount : void 0
9652
9777
  } : {},
9653
9778
  customerInputs: Object.keys(customerInputValues).length > 0 ? Object.entries(customerInputValues).filter(([, v]) => v !== void 0 && v !== "").map(([fieldId, value]) => ({ field_id: fieldId, value })) : void 0,
9654
9779
  specialInstructions: specialInstructions.trim() || void 0
@@ -9669,7 +9794,7 @@ function ProductCustomizer({
9669
9794
  } finally {
9670
9795
  setIsSubmitting(false);
9671
9796
  }
9672
- }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, customerInputValues, specialInstructions, isService, selectedSlot, selectedStaffId, isSubmitting, onAddToCart, cart]);
9797
+ }, [product, quantity, selectedVariantId, selectedVariant, quoteId, normalizedAddOnOptionIds, selectedAddOnOptions, isComposite, compositeSelections, isBundle, bundleSelections, selectedBillingPlan, customerInputValues, specialInstructions, isService, selectedSlot, selectedStaffId, unitsCount, isSubmitting, onAddToCart, cart]);
9673
9798
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-cimplify-customizer": true, className: cn("space-y-6", className, classNames?.root), children: [
9674
9799
  isComposite && product.groups && product.composite_id && /* @__PURE__ */ jsxRuntime.jsx(
9675
9800
  CompositeSelector,
@@ -9706,9 +9831,17 @@ function ProductCustomizer({
9706
9831
  setSelectedStaffId(null);
9707
9832
  },
9708
9833
  participantCount: quantity,
9709
- currency
9834
+ units: offersUnits ? unitsCount : void 0,
9835
+ currency,
9836
+ schedulingMode: product.scheduling_mode ?? (offersUnits ? "multi_day" : "intraday"),
9837
+ durationUnit: product.duration_unit,
9838
+ durationValue: product.duration_value
9710
9839
  }
9711
9840
  ),
9841
+ offersUnits && /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-cimplify-customizer-units": true, className: classNames?.units, children: [
9842
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3", children: "Units" }),
9843
+ /* @__PURE__ */ jsxRuntime.jsx(QuantitySelector, { value: unitsCount, onChange: setUnitsCount, min: 1 })
9844
+ ] }),
9712
9845
  isService && slotStaff.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-cimplify-customizer-staff": true, children: [
9713
9846
  /* @__PURE__ */ jsxRuntime.jsx("label", { className: "block text-xs font-medium uppercase tracking-wider text-muted-foreground mb-3", children: "Choose your specialist" }),
9714
9847
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -10313,7 +10446,7 @@ function CartLineItemRow({
10313
10446
  const isOptimistic = item.isOptimistic === true;
10314
10447
  const lineTotal = chunkPTPOQVXS_js.parsePrice(unitPrice) * item.quantity;
10315
10448
  const imageUrl = item.product.image_url;
10316
- const variantLabel2 = item.variant ? chunkT7Z25EY4_js.getVariantDisplayName(item.variant, item.product.name) : null;
10449
+ const variantLabel2 = item.variant ? chunkHKN3YG76_js.getVariantDisplayName(item.variant, item.product.name) : null;
10317
10450
  return /* @__PURE__ */ jsxRuntime.jsxs(
10318
10451
  "div",
10319
10452
  {
@@ -10413,7 +10546,7 @@ function CartLineItemRow({
10413
10546
  input.field_name,
10414
10547
  ":"
10415
10548
  ] }),
10416
- /* @__PURE__ */ jsxRuntime.jsx("dd", { "data-cimplify-cart-input-value": true, className: "text-foreground m-0 truncate", children: input.field_type === chunkT7Z25EY4_js.INPUT_FIELD_TYPE.File || input.field_type === chunkT7Z25EY4_js.INPUT_FIELD_TYPE.Image ? String(input.value).split("/").pop() || "Uploaded file" : String(input.value) })
10549
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { "data-cimplify-cart-input-value": true, className: "text-foreground m-0 truncate", children: input.field_type === chunkHKN3YG76_js.INPUT_FIELD_TYPE.File || input.field_type === chunkHKN3YG76_js.INPUT_FIELD_TYPE.Image ? String(input.value).split("/").pop() || "Uploaded file" : String(input.value) })
10417
10550
  ] }, input.field_id)) }),
10418
10551
  (item.lineType === "service" || item.product.type === "service") && item.scheduledStart && /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-cimplify-cart-service-info": true, className: cn("mt-1.5 flex items-center gap-1.5 text-xs text-muted-foreground", classNames?.serviceInfo), children: [
10419
10552
  /* @__PURE__ */ jsxRuntime.jsx("svg", { className: "w-3.5 h-3.5 shrink-0", fill: "none", stroke: "currentColor", strokeWidth: "2", viewBox: "0 0 24 24", "aria-hidden": true, children: /* @__PURE__ */ jsxRuntime.jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" }) }),
@@ -11298,7 +11431,7 @@ function FoodProductCard({
11298
11431
  const tags = product.tags || [];
11299
11432
  const isSignature = product.metadata?.is_signature === true;
11300
11433
  const isNew = product.metadata?.is_new === true;
11301
- const isComposite = product.type === chunkT7Z25EY4_js.PRODUCT_TYPE.Composite;
11434
+ const isComposite = product.type === chunkHKN3YG76_js.PRODUCT_TYPE.Composite;
11302
11435
  return /* @__PURE__ */ jsxRuntime.jsxs(CardShell, { product, renderLink, className, children: [
11303
11436
  /* @__PURE__ */ jsxRuntime.jsxs(CardImage, { src: image, alt: product.name, aspectRatio: "square", renderImage, children: [
11304
11437
  isSignature && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "absolute top-3 left-3 inline-flex items-center gap-1 text-[11px] font-semibold tracking-wide bg-amber-50 text-amber-700 border border-amber-200/60 px-2 py-0.5 rounded-md", children: [
@@ -11762,18 +11895,18 @@ function RentalServiceCard({
11762
11895
  const currency = chunkPTPOQVXS_js.getProductCurrency(product);
11763
11896
  const hasDeposit = product.deposit_type && product.deposit_type !== "none" && product.deposit_amount;
11764
11897
  const unit = product.duration_unit ? UNIT_LABELS2[product.duration_unit] || product.duration_unit : "day";
11898
+ const priceUnit = product.price_basis === "per_person" ? "guest" : product.price_basis === "flat" ? "" : unit;
11765
11899
  const status = product.inventory_status;
11766
11900
  return /* @__PURE__ */ jsxRuntime.jsxs(CardShell, { product, renderLink, className, children: [
11767
11901
  /* @__PURE__ */ jsxRuntime.jsxs(CardImage, { src: image, alt: product.name, aspectRatio: "16/9", renderImage, children: [
11768
11902
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute inset-0 bg-gradient-to-t from-black/50 to-transparent pointer-events-none" }),
11769
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "absolute bottom-3 left-3 inline-flex items-center text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm", children: [
11903
+ priceUnit && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "absolute bottom-3 left-3 inline-flex items-center text-[12px] font-semibold px-2.5 py-1 rounded-lg bg-background/92 backdrop-blur-xl text-foreground shadow-sm", children: [
11770
11904
  "Per ",
11771
- unit
11905
+ priceUnit
11772
11906
  ] }),
11773
11907
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "absolute bottom-3 right-3 text-white font-bold text-lg drop-shadow-sm", children: [
11774
11908
  /* @__PURE__ */ jsxRuntime.jsx(Price, { amount: product.default_price, currency }),
11775
- "/",
11776
- unit
11909
+ priceUnit && `/${priceUnit}`
11777
11910
  ] }),
11778
11911
  product.general_service_capacity != null && product.general_service_capacity > 1 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "absolute top-3 left-3 text-[11px] font-semibold tracking-wide bg-blue-50/90 text-blue-700 border border-blue-200/50 backdrop-blur px-2 py-0.5 rounded-md", children: [
11779
11912
  "Up to ",
@@ -12065,36 +12198,36 @@ var BUILT_IN_CARDS = {
12065
12198
  ["composite" /* Composite */]: CompositeProductCard,
12066
12199
  ["subscription" /* Subscription */]: SubscriptionCard
12067
12200
  };
12068
- var RENTAL_UNITS = /* @__PURE__ */ new Set([chunkT7Z25EY4_js.DURATION_UNIT.Days, chunkT7Z25EY4_js.DURATION_UNIT.Hours]);
12069
- var LEASE_UNITS = /* @__PURE__ */ new Set([chunkT7Z25EY4_js.DURATION_UNIT.Weeks, chunkT7Z25EY4_js.DURATION_UNIT.Months, chunkT7Z25EY4_js.DURATION_UNIT.Years]);
12201
+ var RENTAL_UNITS = /* @__PURE__ */ new Set([chunkHKN3YG76_js.DURATION_UNIT.Days, chunkHKN3YG76_js.DURATION_UNIT.Hours]);
12202
+ var LEASE_UNITS = /* @__PURE__ */ new Set([chunkHKN3YG76_js.DURATION_UNIT.Weeks, chunkHKN3YG76_js.DURATION_UNIT.Months, chunkHKN3YG76_js.DURATION_UNIT.Years]);
12070
12203
  function resolveCardVariant(product) {
12071
- if (product.type === chunkT7Z25EY4_js.PRODUCT_TYPE.Bundle) {
12204
+ if (product.type === chunkHKN3YG76_js.PRODUCT_TYPE.Bundle) {
12072
12205
  return "bundle" /* Bundle */;
12073
12206
  }
12074
- if (product.type === chunkT7Z25EY4_js.PRODUCT_TYPE.Composite) {
12207
+ if (product.type === chunkHKN3YG76_js.PRODUCT_TYPE.Composite) {
12075
12208
  return "composite" /* Composite */;
12076
12209
  }
12077
12210
  if (product.quantity_pricing && product.quantity_pricing.length > 1) {
12078
12211
  return "wholesale" /* Wholesale */;
12079
12212
  }
12080
- if (product.type === chunkT7Z25EY4_js.PRODUCT_TYPE.Digital) {
12213
+ if (product.type === chunkHKN3YG76_js.PRODUCT_TYPE.Digital) {
12081
12214
  return "digital" /* Digital */;
12082
12215
  }
12083
- if (product.type === chunkT7Z25EY4_js.PRODUCT_TYPE.Service) {
12216
+ if (product.type === chunkHKN3YG76_js.PRODUCT_TYPE.Service) {
12084
12217
  switch (product.boundary_kind) {
12085
- case chunkT7Z25EY4_js.BOUNDARY_KIND.CheckInOut:
12218
+ case chunkHKN3YG76_js.BOUNDARY_KIND.CheckInOut:
12086
12219
  return "accommodation" /* Accommodation */;
12087
- case chunkT7Z25EY4_js.BOUNDARY_KIND.MoveInOut:
12220
+ case chunkHKN3YG76_js.BOUNDARY_KIND.MoveInOut:
12088
12221
  return "lease" /* Lease */;
12089
- case chunkT7Z25EY4_js.BOUNDARY_KIND.PickupReturn:
12090
- case chunkT7Z25EY4_js.BOUNDARY_KIND.DeliveryCollection:
12091
- case chunkT7Z25EY4_js.BOUNDARY_KIND.AccessVacate:
12222
+ case chunkHKN3YG76_js.BOUNDARY_KIND.PickupReturn:
12223
+ case chunkHKN3YG76_js.BOUNDARY_KIND.DeliveryCollection:
12224
+ case chunkHKN3YG76_js.BOUNDARY_KIND.AccessVacate:
12092
12225
  return "rental" /* Rental */;
12093
12226
  }
12094
12227
  if (product.duration_unit && RENTAL_UNITS.has(product.duration_unit)) {
12095
12228
  return "rental" /* Rental */;
12096
12229
  }
12097
- if (product.duration_unit === chunkT7Z25EY4_js.DURATION_UNIT.Nights) {
12230
+ if (product.duration_unit === chunkHKN3YG76_js.DURATION_UNIT.Nights) {
12098
12231
  return "accommodation" /* Accommodation */;
12099
12232
  }
12100
12233
  if (product.duration_unit && LEASE_UNITS.has(product.duration_unit)) {
@@ -12105,10 +12238,10 @@ function resolveCardVariant(product) {
12105
12238
  }
12106
12239
  return "standard" /* Standard */;
12107
12240
  }
12108
- if (product.render_hint === chunkT7Z25EY4_js.RENDER_HINT.Food) {
12241
+ if (product.render_hint === chunkHKN3YG76_js.RENDER_HINT.Food) {
12109
12242
  return "food" /* Food */;
12110
12243
  }
12111
- if (product.render_hint === chunkT7Z25EY4_js.RENDER_HINT.Physical) {
12244
+ if (product.render_hint === chunkHKN3YG76_js.RENDER_HINT.Physical) {
12112
12245
  return "retail" /* Retail */;
12113
12246
  }
12114
12247
  return "retail" /* Retail */;
@@ -12367,6 +12500,18 @@ function ProductGrid({
12367
12500
  ] });
12368
12501
  }
12369
12502
 
12503
+ // src/react/utils/attachment.ts
12504
+ var IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".svg"];
12505
+ function safeAttachmentUrl(url) {
12506
+ return /^https?:\/\//i.test(url) ? url : null;
12507
+ }
12508
+ function isImageAttachment(attachment) {
12509
+ if (attachment.mime_type?.startsWith("image/")) return true;
12510
+ if (attachment.mime_type) return false;
12511
+ const path = attachment.url.split("?")[0].split("#")[0].toLowerCase();
12512
+ return IMAGE_EXTENSIONS.some((extension) => path.endsWith(extension));
12513
+ }
12514
+
12370
12515
  // src/react/utils/boundary-labels.ts
12371
12516
  var PRESET_LABELS = {
12372
12517
  check_in_out: { start: "Check-in", end: "Check-out" },
@@ -12376,14 +12521,14 @@ var PRESET_LABELS = {
12376
12521
  move_in_out: { start: "Move-in", end: "Move-out" }
12377
12522
  };
12378
12523
  function defaultBoundaryKind(unit) {
12379
- return unit === chunkT7Z25EY4_js.DURATION_UNIT.Nights ? chunkT7Z25EY4_js.BOUNDARY_KIND.CheckInOut : chunkT7Z25EY4_js.BOUNDARY_KIND.PickupReturn;
12524
+ return unit === chunkHKN3YG76_js.DURATION_UNIT.Nights ? chunkHKN3YG76_js.BOUNDARY_KIND.CheckInOut : chunkHKN3YG76_js.BOUNDARY_KIND.PickupReturn;
12380
12525
  }
12381
12526
  function resolveBoundaryKind(product) {
12382
12527
  return product.boundary_kind ?? defaultBoundaryKind(product.duration_unit);
12383
12528
  }
12384
12529
  function getBoundaryLabels(product) {
12385
12530
  const kind = resolveBoundaryKind(product);
12386
- if (kind === chunkT7Z25EY4_js.BOUNDARY_KIND.Custom) {
12531
+ if (kind === chunkHKN3YG76_js.BOUNDARY_KIND.Custom) {
12387
12532
  return {
12388
12533
  start: product.boundary_start_label?.trim() || "Start",
12389
12534
  end: product.boundary_end_label?.trim() || "End"
@@ -13548,6 +13693,7 @@ function BookingPage({
13548
13693
  const [selectedDate, setSelectedDate] = React14.useState(null);
13549
13694
  const [selectedStaffId, setSelectedStaffId] = React14.useState(null);
13550
13695
  const [selectedResourceId, setSelectedResourceId] = React14.useState(null);
13696
+ const [bookingUnits, setBookingUnits] = React14.useState(1);
13551
13697
  const [isSubmitting, setIsSubmitting] = React14.useState(false);
13552
13698
  const [error, setError] = React14.useState(null);
13553
13699
  const hasResourceStep = resources && resources.length > 0;
@@ -13641,7 +13787,8 @@ function BookingPage({
13641
13787
  scheduledStart: selectedSlot.start_time,
13642
13788
  scheduledEnd: selectedSlot.end_time,
13643
13789
  staffId: selectedStaffId || void 0,
13644
- resourceId: selectedResourceId || void 0
13790
+ resourceId: selectedResourceId || void 0,
13791
+ units: bookingUnits > 1 ? bookingUnits : void 0
13645
13792
  });
13646
13793
  onBooked?.(selectedSlot, selectedStaffId);
13647
13794
  }
@@ -13737,6 +13884,31 @@ function BookingPage({
13737
13884
  ] }),
13738
13885
  step === STEP.CONFIRM && selectedSlot && /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-cimplify-booking-step": "confirm", className: classNames?.step, children: [
13739
13886
  /* @__PURE__ */ jsxRuntime.jsx("h2", { "data-cimplify-booking-step-title": true, className: classNames?.stepTitle, children: "Confirm your booking" }),
13887
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-cimplify-booking-units": true, className: classNames?.summaryRow, children: [
13888
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Units" }),
13889
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-cimplify-booking-units-stepper": true, children: [
13890
+ /* @__PURE__ */ jsxRuntime.jsx(
13891
+ "button",
13892
+ {
13893
+ type: "button",
13894
+ "data-cimplify-booking-units-decrement": true,
13895
+ onClick: () => setBookingUnits((current) => Math.max(1, current - 1)),
13896
+ disabled: bookingUnits <= 1,
13897
+ children: "\u2212"
13898
+ }
13899
+ ),
13900
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "data-cimplify-booking-units-value": true, children: bookingUnits }),
13901
+ /* @__PURE__ */ jsxRuntime.jsx(
13902
+ "button",
13903
+ {
13904
+ type: "button",
13905
+ "data-cimplify-booking-units-increment": true,
13906
+ onClick: () => setBookingUnits((current) => current + 1),
13907
+ children: "+"
13908
+ }
13909
+ )
13910
+ ] })
13911
+ ] }),
13740
13912
  /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-cimplify-booking-summary": true, className: classNames?.summary, children: [
13741
13913
  /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-cimplify-booking-summary-row": true, className: classNames?.summaryRow, children: [
13742
13914
  /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Service" }),
@@ -14444,7 +14616,7 @@ function DefaultProductLayout({
14444
14616
  className
14445
14617
  }) {
14446
14618
  const [selectedVariantId, setSelectedVariantId] = React14__default.default.useState();
14447
- const images = chunkT7Z25EY4_js.resolveGallery(product, selectedVariantId);
14619
+ const images = chunkHKN3YG76_js.resolveGallery(product, selectedVariantId);
14448
14620
  const currency = chunkPTPOQVXS_js.getProductCurrency(product);
14449
14621
  const hasRange = product.quantity_pricing && product.quantity_pricing.length > 1 || product.variants && product.variants.length > 1;
14450
14622
  const onSale = chunkPTPOQVXS_js.isOnSale(product);
@@ -15091,25 +15263,25 @@ function resolveTemplateKey(product) {
15091
15263
  if (typeof metaTemplate === "string" && metaTemplate.trim()) {
15092
15264
  return metaTemplate.trim();
15093
15265
  }
15094
- if (product.type === chunkT7Z25EY4_js.PRODUCT_TYPE.Bundle) {
15266
+ if (product.type === chunkHKN3YG76_js.PRODUCT_TYPE.Bundle) {
15095
15267
  return "bundle" /* Bundle */;
15096
15268
  }
15097
- if (product.type === chunkT7Z25EY4_js.PRODUCT_TYPE.Composite) {
15269
+ if (product.type === chunkHKN3YG76_js.PRODUCT_TYPE.Composite) {
15098
15270
  return "composite" /* Composite */;
15099
15271
  }
15100
15272
  if (product.quantity_pricing && product.quantity_pricing.length > 1) {
15101
15273
  return "wholesale" /* Wholesale */;
15102
15274
  }
15103
- if (product.type === chunkT7Z25EY4_js.PRODUCT_TYPE.Service) {
15275
+ if (product.type === chunkHKN3YG76_js.PRODUCT_TYPE.Service) {
15104
15276
  return "service" /* Service */;
15105
15277
  }
15106
- if (product.type === chunkT7Z25EY4_js.PRODUCT_TYPE.Digital) {
15278
+ if (product.type === chunkHKN3YG76_js.PRODUCT_TYPE.Digital) {
15107
15279
  return "digital" /* Digital */;
15108
15280
  }
15109
- if (product.render_hint === chunkT7Z25EY4_js.RENDER_HINT.Food) {
15281
+ if (product.render_hint === chunkHKN3YG76_js.RENDER_HINT.Food) {
15110
15282
  return "food" /* Food */;
15111
15283
  }
15112
- if (product.render_hint === chunkT7Z25EY4_js.RENDER_HINT.Physical) {
15284
+ if (product.render_hint === chunkHKN3YG76_js.RENDER_HINT.Physical) {
15113
15285
  return "physical" /* Physical */;
15114
15286
  }
15115
15287
  return "default" /* Default */;
@@ -16979,6 +17151,7 @@ function partitionMessages(messages) {
16979
17151
  }
16980
17152
  return { bubbles, reactionsByTarget };
16981
17153
  }
17154
+ var MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
16982
17155
  function ChatWidget({
16983
17156
  client: clientProp,
16984
17157
  businessName = "Support",
@@ -16995,7 +17168,8 @@ function ChatWidget({
16995
17168
  const context = useOptionalCimplifyClient();
16996
17169
  const client = clientProp ?? context?.client;
16997
17170
  const [open, setOpen] = React14.useState(defaultOpen);
16998
- const [unread, setUnread] = React14.useState(0);
17171
+ const [readAt, setReadAt] = React14.useState(null);
17172
+ const [attachError, setAttachError] = React14.useState(null);
16999
17173
  const {
17000
17174
  messages,
17001
17175
  isLoading,
@@ -17004,16 +17178,31 @@ function ChatWidget({
17004
17178
  seenAt,
17005
17179
  error,
17006
17180
  isActive,
17181
+ claimedAs,
17182
+ signOut,
17007
17183
  send,
17008
17184
  react,
17009
- startConversation
17185
+ startConversation,
17186
+ openIfExisting
17010
17187
  } = useChat({ client, pollInterval });
17011
17188
  const showTyping = isSending || awaitingReply;
17012
17189
  const attachPhoto = React14.useCallback(
17013
17190
  async (file) => {
17014
- 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
+ }
17015
17201
  const uploaded = await client.uploads.upload(file);
17016
- if (!uploaded.ok) return;
17202
+ if (!uploaded.ok) {
17203
+ setAttachError(uploaded.error);
17204
+ return;
17205
+ }
17017
17206
  const attachment = {
17018
17207
  upload_id: uploaded.value.id,
17019
17208
  url: uploaded.value.url,
@@ -17025,22 +17214,28 @@ function ChatWidget({
17025
17214
  },
17026
17215
  [client, send]
17027
17216
  );
17028
- const prevCountRef = React14.useRef(messages.length);
17029
- React14.useEffect(() => {
17030
- if (!open && messages.length > prevCountRef.current) {
17031
- const newMessages = messages.slice(prevCountRef.current);
17032
- const incomingCount = newMessages.filter(
17033
- (m) => m.sender_type !== "customer"
17034
- ).length;
17035
- if (incomingCount > 0) setUnread((n) => n + incomingCount);
17036
- }
17037
- prevCountRef.current = messages.length;
17038
- }, [messages, open]);
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
+ });
17039
17230
  const handleOpen = React14.useCallback(() => {
17040
17231
  setOpen(true);
17041
- setUnread(0);
17042
- }, []);
17043
- const handleClose = React14.useCallback(() => setOpen(false), []);
17232
+ void openIfExisting();
17233
+ }, [openIfExisting]);
17234
+ const handleClose = React14.useCallback(() => {
17235
+ const newest = messages[messages.length - 1];
17236
+ setReadAt(newest?.created_at ?? null);
17237
+ setOpen(false);
17238
+ }, [messages]);
17044
17239
  const isLeft = position === "bottom-left";
17045
17240
  return /* @__PURE__ */ jsxRuntime.jsxs(
17046
17241
  "div",
@@ -17073,11 +17268,13 @@ function ChatWidget({
17073
17268
  isSending,
17074
17269
  isTyping: showTyping,
17075
17270
  seenAt,
17076
- error,
17271
+ error: error ?? attachError,
17077
17272
  isActive,
17078
17273
  isLeft,
17274
+ claimedAs,
17275
+ onSignOut: () => void signOut(),
17079
17276
  onClose: handleClose,
17080
- onSend: send,
17277
+ onSend: handleSend,
17081
17278
  onReact: react,
17082
17279
  onAttach: attachPhoto,
17083
17280
  onStartConversation: startConversation,
@@ -17137,6 +17334,8 @@ function Panel({
17137
17334
  error,
17138
17335
  isActive,
17139
17336
  isLeft,
17337
+ claimedAs,
17338
+ onSignOut,
17140
17339
  onClose,
17141
17340
  onSend,
17142
17341
  onReact,
@@ -17160,6 +17359,8 @@ function Panel({
17160
17359
  Header,
17161
17360
  {
17162
17361
  businessName,
17362
+ claimedAs,
17363
+ onSignOut,
17163
17364
  onClose,
17164
17365
  className: classNames?.header
17165
17366
  }
@@ -17184,7 +17385,7 @@ function Panel({
17184
17385
  className: classNames?.messages
17185
17386
  }
17186
17387
  ),
17187
- 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." }),
17188
17389
  (isActive || isLoading) && /* @__PURE__ */ jsxRuntime.jsx(
17189
17390
  ChatInput,
17190
17391
  {
@@ -17206,6 +17407,8 @@ function Panel({
17206
17407
  }
17207
17408
  function Header({
17208
17409
  businessName,
17410
+ claimedAs,
17411
+ onSignOut,
17209
17412
  onClose,
17210
17413
  className
17211
17414
  }) {
@@ -17220,7 +17423,22 @@ function Header({
17220
17423
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[10px] bg-white/15", children: /* @__PURE__ */ jsxRuntime.jsx(ChatIcon, { className: "h-5 w-5" }) }),
17221
17424
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
17222
17425
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "truncate text-[15px] font-semibold leading-tight", children: businessName }),
17223
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-white/60", children: [
17426
+ claimedAs ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1 truncate text-xs text-white/60", children: [
17427
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "truncate", children: [
17428
+ "Chatting as ",
17429
+ claimedAs
17430
+ ] }),
17431
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: "\xB7" }),
17432
+ /* @__PURE__ */ jsxRuntime.jsx(
17433
+ "button",
17434
+ {
17435
+ type: "button",
17436
+ onClick: onSignOut,
17437
+ className: "flex-shrink-0 underline underline-offset-2 hover:text-white",
17438
+ children: "Not you?"
17439
+ }
17440
+ )
17441
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-xs text-white/60", children: [
17224
17442
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-[7px] w-[7px] flex-shrink-0 rounded-full bg-emerald-400" }),
17225
17443
  "Online"
17226
17444
  ] })
@@ -17304,10 +17522,6 @@ function MessageList({
17304
17522
  onReact,
17305
17523
  className
17306
17524
  }) {
17307
- const bottomRef = React14.useRef(null);
17308
- React14.useEffect(() => {
17309
- bottomRef.current?.scrollIntoView({ behavior: "smooth" });
17310
- }, [messages.length, isSending]);
17311
17525
  const { bubbles, reactionsByTarget } = partitionMessages(messages);
17312
17526
  const seenMessageId = lastSeenCustomerMessageId(bubbles, seenAt);
17313
17527
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -17381,11 +17595,13 @@ function MessageList({
17381
17595
  ]
17382
17596
  }
17383
17597
  ),
17384
- 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(
17385
- (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(
17386
17602
  "img",
17387
17603
  {
17388
- src: attachment.url,
17604
+ src: url,
17389
17605
  alt: attachment.filename ?? "Photo",
17390
17606
  loading: "lazy",
17391
17607
  className: "max-h-48 max-w-full rounded-lg object-cover"
@@ -17393,15 +17609,15 @@ function MessageList({
17393
17609
  ) }, index) : /* @__PURE__ */ jsxRuntime.jsx(
17394
17610
  "a",
17395
17611
  {
17396
- href: attachment.url,
17612
+ href: url,
17397
17613
  target: "_blank",
17398
17614
  rel: "noreferrer",
17399
17615
  className: "truncate text-xs underline",
17400
17616
  children: attachment.filename ?? "Attachment"
17401
17617
  },
17402
17618
  index
17403
- )
17404
- ) }),
17619
+ );
17620
+ }) }),
17405
17621
  isCustomer ? msg.content && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "whitespace-pre-wrap", children: msg.content }) : /* @__PURE__ */ jsxRuntime.jsx(
17406
17622
  ChatMarkdown,
17407
17623
  {
@@ -17427,7 +17643,15 @@ function MessageList({
17427
17643
  ] }, msg.id);
17428
17644
  }),
17429
17645
  isSending && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mr-auto mt-1", children: /* @__PURE__ */ jsxRuntime.jsx(TypingDots, {}) }),
17430
- /* @__PURE__ */ jsxRuntime.jsx("div", { ref: bottomRef })
17646
+ /* @__PURE__ */ jsxRuntime.jsx(
17647
+ "div",
17648
+ {
17649
+ ref: (node) => {
17650
+ node?.scrollIntoView({ behavior: "smooth" });
17651
+ }
17652
+ },
17653
+ `${messages.length}:${isSending}`
17654
+ )
17431
17655
  ]
17432
17656
  }
17433
17657
  );
@@ -19476,7 +19700,7 @@ async function genUIFingerprint(manifest) {
19476
19700
 
19477
19701
  Object.defineProperty(exports, "getVariantDisplayName", {
19478
19702
  enumerable: true,
19479
- get: function () { return chunkT7Z25EY4_js.getVariantDisplayName; }
19703
+ get: function () { return chunkHKN3YG76_js.getVariantDisplayName; }
19480
19704
  });
19481
19705
  Object.defineProperty(exports, "messagePreview", {
19482
19706
  enumerable: true,
@@ -19505,6 +19729,7 @@ exports.AccountSubscriptionsPage = AccountSubscriptionsPage;
19505
19729
  exports.Ad = Ad;
19506
19730
  exports.AdProvider = AdProvider;
19507
19731
  exports.AddOnSelector = AddOnSelector;
19732
+ exports.AddressAutocomplete = AddressAutocomplete;
19508
19733
  exports.AddressElement = AddressElement;
19509
19734
  exports.AuthElement = AuthElement;
19510
19735
  exports.AvailabilityBadge = AvailabilityBadge;
@@ -19668,12 +19893,14 @@ exports.WholesaleProductLayout = WholesaleProductLayout;
19668
19893
  exports.WishlistButton = WishlistButton;
19669
19894
  exports.clearOrdersCache = clearOrdersCache;
19670
19895
  exports.cn = cn;
19896
+ exports.createPlacesSessionToken = createPlacesSessionToken;
19671
19897
  exports.defaultBoundaryKind = defaultBoundaryKind;
19672
19898
  exports.genUICatalog = genUICatalog;
19673
19899
  exports.genUIFingerprint = genUIFingerprint;
19674
19900
  exports.genUIManifest = genUIManifest;
19675
19901
  exports.genUIManifestJson = genUIManifestJson;
19676
19902
  exports.getBoundaryLabels = getBoundaryLabels;
19903
+ exports.isImageAttachment = isImageAttachment;
19677
19904
  exports.isTerminalOrderStatus = isTerminalOrderStatus;
19678
19905
  exports.parseGenUIProgram = parse;
19679
19906
  exports.partitionMessages = partitionMessages;
@@ -19681,6 +19908,7 @@ exports.prefetchProduct = prefetchProduct;
19681
19908
  exports.prefetchProducts = prefetchProducts;
19682
19909
  exports.resolveBoundaryKind = resolveBoundaryKind;
19683
19910
  exports.roomToResource = roomToResource;
19911
+ exports.safeAttachmentUrl = safeAttachmentUrl;
19684
19912
  exports.sanitizeRichTextHtml = sanitizeRichTextHtml;
19685
19913
  exports.stripHtmlToText = stripHtmlToText;
19686
19914
  exports.toAccountLineItem = toAccountLineItem;