@customerhero/js 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -513,6 +513,377 @@ ${line.slice(5).trim()}` : line.slice(5).trim();
513
513
  return { event, data };
514
514
  }
515
515
 
516
+ // src/triggers.ts
517
+ function evaluate(node, ctx) {
518
+ if (isGroup(node, "all")) {
519
+ if (node.all.length === 0) return false;
520
+ return node.all.every((child) => evaluate(child, ctx));
521
+ }
522
+ if (isGroup(node, "any")) {
523
+ if (node.any.length === 0) return false;
524
+ return node.any.some((child) => evaluate(child, ctx));
525
+ }
526
+ return evaluateLeaf(node, ctx);
527
+ }
528
+ function isGroup(node, key) {
529
+ return !!node && typeof node === "object" && Array.isArray(node[key]);
530
+ }
531
+ function evaluateLeaf(leaf, ctx) {
532
+ switch (leaf.kind) {
533
+ case "url_path": {
534
+ const path = parseUrlPath(ctx.url);
535
+ switch (leaf.op) {
536
+ case "equals":
537
+ return path === leaf.value;
538
+ case "contains":
539
+ return path.includes(leaf.value);
540
+ case "starts_with":
541
+ return path.startsWith(leaf.value);
542
+ case "regex":
543
+ return safeRegex(leaf.value).test(path);
544
+ default:
545
+ return false;
546
+ }
547
+ }
548
+ case "url_query": {
549
+ const has = Object.prototype.hasOwnProperty.call(
550
+ ctx.queryParams,
551
+ leaf.key
552
+ );
553
+ if (leaf.op === "exists") return has;
554
+ if (!has) return false;
555
+ const v = ctx.queryParams[leaf.key] ?? "";
556
+ if (leaf.op === "equals") return v === (leaf.value ?? "");
557
+ if (leaf.op === "contains") return v.includes(leaf.value ?? "");
558
+ return false;
559
+ }
560
+ case "referrer": {
561
+ const r = ctx.referrer;
562
+ if (leaf.op === "equals") return r === leaf.value;
563
+ if (leaf.op === "contains") return r.includes(leaf.value);
564
+ if (leaf.op === "regex") return safeRegex(leaf.value).test(r);
565
+ return false;
566
+ }
567
+ case "time_on_page":
568
+ return ctx.timeOnPageMs >= leaf.seconds * 1e3;
569
+ case "scroll_depth":
570
+ return ctx.scrollPercent >= leaf.percent;
571
+ case "exit_intent":
572
+ return ctx.exitIntentSeen;
573
+ case "device":
574
+ return ctx.device === leaf.value;
575
+ case "browser_language": {
576
+ const lang = (ctx.language || "").toLowerCase();
577
+ return leaf.values.some((v) => {
578
+ const target = v.toLowerCase();
579
+ return lang === target || lang.startsWith(`${target}-`);
580
+ });
581
+ }
582
+ case "visitor_trait": {
583
+ const has = Object.prototype.hasOwnProperty.call(ctx.traits, leaf.key);
584
+ if (leaf.op === "exists") return has;
585
+ if (!has) return false;
586
+ const v = ctx.traits[leaf.key];
587
+ if (leaf.op === "equals") return v === leaf.value;
588
+ if (leaf.op === "gt")
589
+ return typeof v === "number" && typeof leaf.value === "number" && v > leaf.value;
590
+ if (leaf.op === "lt")
591
+ return typeof v === "number" && typeof leaf.value === "number" && v < leaf.value;
592
+ return false;
593
+ }
594
+ case "return_visit": {
595
+ if (leaf.op === "gte") return ctx.returnVisitCount >= leaf.count;
596
+ if (leaf.op === "eq") return ctx.returnVisitCount === leaf.count;
597
+ return false;
598
+ }
599
+ default:
600
+ return false;
601
+ }
602
+ }
603
+ function parseUrlPath(url) {
604
+ try {
605
+ return new URL(url).pathname;
606
+ } catch {
607
+ return url.split("?")[0] ?? url;
608
+ }
609
+ }
610
+ function safeRegex(pattern) {
611
+ try {
612
+ return new RegExp(pattern);
613
+ } catch {
614
+ return /^\b$/;
615
+ }
616
+ }
617
+ function pickFire(triggers, ctx, firedSet) {
618
+ const sorted = triggers.slice().sort((a, b) => a.priority - b.priority);
619
+ for (const t of sorted) {
620
+ if (t.frequency !== "every_time" && firedSet.has(t.id)) continue;
621
+ if (evaluate(t.conditions, ctx)) return t;
622
+ }
623
+ return null;
624
+ }
625
+
626
+ // src/triggers-runtime.ts
627
+ var STORAGE_NS = "ch_trigger";
628
+ var RETURN_VISIT_KEY = "ch_visits";
629
+ var SCROLL_THROTTLE_MS = 250;
630
+ var EXIT_INTENT_GRACE_MS = 5e3;
631
+ function getLocalStorage() {
632
+ try {
633
+ return typeof window !== "undefined" ? window.localStorage : null;
634
+ } catch {
635
+ return null;
636
+ }
637
+ }
638
+ function getSessionStorage() {
639
+ try {
640
+ return typeof window !== "undefined" ? window.sessionStorage : null;
641
+ } catch {
642
+ return null;
643
+ }
644
+ }
645
+ function detectDevice() {
646
+ if (typeof window === "undefined") return "desktop";
647
+ const ua = navigator.userAgent || "";
648
+ if (/iPad|Tablet|PlayBook/i.test(ua)) return "tablet";
649
+ if (/Mobi|Android|iPhone|iPod/i.test(ua)) return "mobile";
650
+ if (window.matchMedia?.("(pointer: coarse)").matches && window.innerWidth < 1024) {
651
+ return window.innerWidth < 600 ? "mobile" : "tablet";
652
+ }
653
+ return "desktop";
654
+ }
655
+ function parseQueryParams(url) {
656
+ const out = {};
657
+ try {
658
+ const u = new URL(url);
659
+ u.searchParams.forEach((v, k) => {
660
+ out[k] = v;
661
+ });
662
+ } catch {
663
+ }
664
+ return out;
665
+ }
666
+ function readReturnVisitCount() {
667
+ const ls = getLocalStorage();
668
+ if (!ls) return 1;
669
+ try {
670
+ const raw = ls.getItem(RETURN_VISIT_KEY);
671
+ return raw ? Math.max(1, parseInt(raw, 10) || 1) : 1;
672
+ } catch {
673
+ return 1;
674
+ }
675
+ }
676
+ function bumpReturnVisitCount() {
677
+ const ls = getLocalStorage();
678
+ const ss = getSessionStorage();
679
+ if (!ls || !ss) return readReturnVisitCount();
680
+ try {
681
+ if (ss.getItem("ch_visit_seen") === "1") return readReturnVisitCount();
682
+ const current = parseInt(ls.getItem(RETURN_VISIT_KEY) ?? "0", 10) || 0;
683
+ const next = current + 1;
684
+ ls.setItem(RETURN_VISIT_KEY, String(next));
685
+ ss.setItem("ch_visit_seen", "1");
686
+ return next;
687
+ } catch {
688
+ return readReturnVisitCount();
689
+ }
690
+ }
691
+ function firedKey(chatbotId, triggerId) {
692
+ return `${STORAGE_NS}_${chatbotId}_${triggerId}_fired`;
693
+ }
694
+ function readFired(chatbotId, triggers) {
695
+ const ls = getLocalStorage();
696
+ const ss = getSessionStorage();
697
+ const out = /* @__PURE__ */ new Set();
698
+ for (const t of triggers) {
699
+ if (t.frequency === "every_time") continue;
700
+ try {
701
+ const key = firedKey(chatbotId, t.id);
702
+ if (t.frequency === "once_ever" && ls?.getItem(key) === "1") {
703
+ out.add(t.id);
704
+ } else if (t.frequency === "once_per_session" && ss?.getItem(key) === "1") {
705
+ out.add(t.id);
706
+ }
707
+ } catch {
708
+ }
709
+ }
710
+ return out;
711
+ }
712
+ function writeFired(chatbotId, triggerId, frequency) {
713
+ if (frequency === "every_time") return;
714
+ const key = firedKey(chatbotId, triggerId);
715
+ try {
716
+ if (frequency === "once_ever") {
717
+ getLocalStorage()?.setItem(key, "1");
718
+ } else if (frequency === "once_per_session") {
719
+ getSessionStorage()?.setItem(key, "1");
720
+ }
721
+ } catch {
722
+ }
723
+ }
724
+ function startTriggersRuntime(options) {
725
+ if (typeof window === "undefined") {
726
+ return {
727
+ stop() {
728
+ },
729
+ reevaluate() {
730
+ },
731
+ setTraits() {
732
+ },
733
+ markFired() {
734
+ }
735
+ };
736
+ }
737
+ const { chatbotId, triggers, onFire, isAllowedToFire } = options;
738
+ let stopped = false;
739
+ let exitIntentSeen = false;
740
+ let scrollPercent = 0;
741
+ let pageStartedAt = Date.now();
742
+ let timeOnPageMs = 0;
743
+ let lastVisibleAt = Date.now();
744
+ let traits = {
745
+ ...options.initialTraits ?? {}
746
+ };
747
+ const firedThisSession = /* @__PURE__ */ new Set();
748
+ for (const id of readFired(chatbotId, triggers)) firedThisSession.add(id);
749
+ const returnVisitCount = bumpReturnVisitCount();
750
+ function snapshot() {
751
+ const url = window.location.href;
752
+ return {
753
+ url,
754
+ queryParams: parseQueryParams(url),
755
+ referrer: document.referrer || "",
756
+ language: (navigator.language || "en").toLowerCase(),
757
+ device: detectDevice(),
758
+ timeOnPageMs,
759
+ scrollPercent,
760
+ exitIntentSeen,
761
+ returnVisitCount,
762
+ traits
763
+ };
764
+ }
765
+ function evaluate2() {
766
+ if (stopped) return;
767
+ if (!isAllowedToFire()) return;
768
+ if (triggers.length === 0) return;
769
+ const ctx = snapshot();
770
+ const trigger = pickFire(triggers, ctx, firedThisSession);
771
+ if (!trigger) return;
772
+ firedThisSession.add(trigger.id);
773
+ writeFired(chatbotId, trigger.id, trigger.frequency);
774
+ try {
775
+ onFire(trigger);
776
+ } catch (err) {
777
+ console.error("CustomerHero: trigger onFire handler threw", err);
778
+ }
779
+ }
780
+ const timerId = window.setInterval(() => {
781
+ if (document.hidden) return;
782
+ const now = Date.now();
783
+ timeOnPageMs += now - lastVisibleAt;
784
+ lastVisibleAt = now;
785
+ evaluate2();
786
+ }, 1e3);
787
+ function handleVisibility() {
788
+ const now = Date.now();
789
+ if (document.hidden) {
790
+ timeOnPageMs += now - lastVisibleAt;
791
+ if (now - pageStartedAt > EXIT_INTENT_GRACE_MS && !exitIntentSeen) {
792
+ exitIntentSeen = true;
793
+ evaluate2();
794
+ }
795
+ } else {
796
+ lastVisibleAt = now;
797
+ evaluate2();
798
+ }
799
+ }
800
+ document.addEventListener("visibilitychange", handleVisibility);
801
+ let scrollScheduled = false;
802
+ function handleScroll() {
803
+ if (scrollScheduled) return;
804
+ scrollScheduled = true;
805
+ window.setTimeout(() => {
806
+ scrollScheduled = false;
807
+ const doc = document.documentElement;
808
+ const scrollable = doc.scrollHeight - window.innerHeight;
809
+ if (scrollable <= 0) {
810
+ scrollPercent = Math.max(scrollPercent, 100);
811
+ } else {
812
+ const next = window.scrollY / scrollable * 100;
813
+ if (next > scrollPercent) {
814
+ scrollPercent = next;
815
+ evaluate2();
816
+ }
817
+ }
818
+ }, SCROLL_THROTTLE_MS);
819
+ }
820
+ window.addEventListener("scroll", handleScroll, { passive: true });
821
+ function handleMouseOut(e) {
822
+ if (exitIntentSeen) return;
823
+ if (e.relatedTarget !== null) return;
824
+ if (e.clientY > 0) return;
825
+ if (Date.now() - pageStartedAt < EXIT_INTENT_GRACE_MS) return;
826
+ exitIntentSeen = true;
827
+ evaluate2();
828
+ }
829
+ document.addEventListener("mouseout", handleMouseOut);
830
+ const origPush = history.pushState;
831
+ const origReplace = history.replaceState;
832
+ function onUrlChange() {
833
+ pageStartedAt = Date.now();
834
+ timeOnPageMs = 0;
835
+ lastVisibleAt = Date.now();
836
+ scrollPercent = 0;
837
+ exitIntentSeen = false;
838
+ evaluate2();
839
+ }
840
+ history.pushState = function patched(...args) {
841
+ const ret = origPush.apply(this, args);
842
+ try {
843
+ onUrlChange();
844
+ } catch {
845
+ }
846
+ return ret;
847
+ };
848
+ history.replaceState = function patched(...args) {
849
+ const ret = origReplace.apply(
850
+ this,
851
+ args
852
+ );
853
+ try {
854
+ onUrlChange();
855
+ } catch {
856
+ }
857
+ return ret;
858
+ };
859
+ window.addEventListener("popstate", onUrlChange);
860
+ const initialEval = window.setTimeout(evaluate2, 0);
861
+ return {
862
+ stop() {
863
+ stopped = true;
864
+ window.clearInterval(timerId);
865
+ window.clearTimeout(initialEval);
866
+ document.removeEventListener("visibilitychange", handleVisibility);
867
+ window.removeEventListener("scroll", handleScroll);
868
+ document.removeEventListener("mouseout", handleMouseOut);
869
+ window.removeEventListener("popstate", onUrlChange);
870
+ history.pushState = origPush;
871
+ history.replaceState = origReplace;
872
+ },
873
+ reevaluate() {
874
+ evaluate2();
875
+ },
876
+ setTraits(next) {
877
+ traits = { ...traits, ...next };
878
+ evaluate2();
879
+ },
880
+ markFired(triggerId, frequency) {
881
+ firedThisSession.add(triggerId);
882
+ writeFired(chatbotId, triggerId, frequency);
883
+ }
884
+ };
885
+ }
886
+
516
887
  // src/client.ts
517
888
  function resolveConfig(userConfig, fetched) {
518
889
  return {
@@ -567,9 +938,35 @@ var CustomerHeroChat = class {
567
938
  error: null,
568
939
  identity: null,
569
940
  locale,
570
- isRtl: isRtlLocale(locale)
941
+ isRtl: isRtlLocale(locale),
942
+ triggers: [],
943
+ preChatForm: null,
944
+ preChatFormVisible: false,
945
+ preChatSubmission: null,
946
+ consent: this.readStoredConsent(),
947
+ pendingTriggerId: null,
948
+ pendingPrefill: null
571
949
  };
572
950
  }
951
+ // ── Proactive engagement state ─────────────────────────────────────
952
+ triggersRuntime = null;
953
+ preChatFormSubmitted = false;
954
+ readStoredConsent() {
955
+ try {
956
+ const raw = this.storage?.getItem("ch_consent");
957
+ if (!raw) return { analytics: false };
958
+ const parsed = JSON.parse(raw);
959
+ return { analytics: parsed.analytics === true };
960
+ } catch {
961
+ return { analytics: false };
962
+ }
963
+ }
964
+ writeStoredConsent(consent) {
965
+ try {
966
+ this.storage?.setItem("ch_consent", JSON.stringify(consent));
967
+ } catch {
968
+ }
969
+ }
573
970
  // Switch the active locale at runtime. No-op when the resolved tag matches
574
971
  // the current locale and `stringOverrides` is unchanged. Subscribers get a
575
972
  // single state notification with the new `locale` / `isRtl`.
@@ -625,8 +1022,16 @@ var CustomerHeroChat = class {
625
1022
  }
626
1023
  const fetched = await response.json();
627
1024
  const resolved = resolveConfig(this.userConfig, fetched);
628
- this.setState({ config: resolved, configLoaded: true });
1025
+ const triggers = Array.isArray(fetched.triggers) ? fetched.triggers : [];
1026
+ const preChatForm = fetched.preChatForm ?? null;
1027
+ this.setState({
1028
+ config: resolved,
1029
+ configLoaded: true,
1030
+ triggers,
1031
+ preChatForm
1032
+ });
629
1033
  if (resolved.stringOverrides) this.rebuildTranslator();
1034
+ this.startTriggersRuntimeIfPossible();
630
1035
  } catch (error) {
631
1036
  const errorMsg = error instanceof Error ? error.message : "Failed to load widget config";
632
1037
  console.error("CustomerHero: Failed to fetch widget config", error);
@@ -684,6 +1089,14 @@ var CustomerHeroChat = class {
684
1089
  const trimmed = message.trim();
685
1090
  const attachmentTokens = options?.attachmentTokens ?? [];
686
1091
  if (!trimmed || this.state.isLoading) return;
1092
+ if (this.shouldShowPreChatForm()) {
1093
+ this.pendingMessageAfterPreChat = {
1094
+ message: trimmed,
1095
+ attachmentTokens
1096
+ };
1097
+ this.setState({ preChatFormVisible: true });
1098
+ return;
1099
+ }
687
1100
  const userMsg = {
688
1101
  role: "user",
689
1102
  content: trimmed,
@@ -716,7 +1129,13 @@ var CustomerHeroChat = class {
716
1129
  message: trimmed,
717
1130
  ...this.state.conversationId ? { conversationId: this.state.conversationId } : {},
718
1131
  ...this.identityData ? { identity: this.identityData } : {},
719
- ...attachmentTokens.length > 0 ? { attachmentTokens } : {}
1132
+ ...attachmentTokens.length > 0 ? { attachmentTokens } : {},
1133
+ // Trigger attribution + pre-chat submission only land on the very
1134
+ // first turn. We only consume them when there's no conversationId
1135
+ // yet — the server ignores them on subsequent turns anyway, but
1136
+ // sending them again would be misleading.
1137
+ ...!this.state.conversationId && this.state.pendingTriggerId ? { triggeredByTriggerId: this.state.pendingTriggerId } : {},
1138
+ ...!this.state.conversationId && this.state.preChatSubmission ? { prechatSubmission: this.state.preChatSubmission } : {}
720
1139
  })
721
1140
  });
722
1141
  if (!response.ok) {
@@ -738,7 +1157,11 @@ var CustomerHeroChat = class {
738
1157
  `ch_conv_${chatbotId}`,
739
1158
  meta.conversationId
740
1159
  );
741
- this.setState({ conversationId: meta.conversationId });
1160
+ this.setState({
1161
+ conversationId: meta.conversationId,
1162
+ pendingTriggerId: null,
1163
+ preChatSubmission: null
1164
+ });
742
1165
  }
743
1166
  this.patchMessageAt(userMsgIndex, { status: "sent" });
744
1167
  if (meta?.messageId) {
@@ -1011,6 +1434,119 @@ var CustomerHeroChat = class {
1011
1434
  error: null
1012
1435
  });
1013
1436
  }
1437
+ // ── Proactive engagement public API ────────────────────────────────
1438
+ /** Update visitor consent. Until `analytics: true` is set, only direct
1439
+ * launcher clicks fire — URL/time/scroll/exit-intent/trait conditions
1440
+ * stay dormant. The setting is persisted in localStorage so revisits
1441
+ * don't re-prompt. */
1442
+ setConsent(consent) {
1443
+ const next = {
1444
+ analytics: typeof consent.analytics === "boolean" ? consent.analytics : this.state.consent.analytics
1445
+ };
1446
+ this.writeStoredConsent(next);
1447
+ this.setState({ consent: next });
1448
+ this.triggersRuntime?.reevaluate();
1449
+ }
1450
+ /** Set or update visitor traits used by trait-based conditions. The trait
1451
+ * values are kept in memory (not persisted) so the integrator decides
1452
+ * the source of truth. */
1453
+ setTraits(traits) {
1454
+ this.triggersRuntime?.setTraits(traits);
1455
+ }
1456
+ /** Submit pre-chat form answers. Synthesizes a customer record server-side
1457
+ * on the next sendMessage. Resumes any pending message that was deferred
1458
+ * while the form was open. */
1459
+ async submitPreChatForm(submission) {
1460
+ this.preChatFormSubmitted = true;
1461
+ this.setState({
1462
+ preChatSubmission: submission,
1463
+ preChatFormVisible: false
1464
+ });
1465
+ const pending = this.pendingMessageAfterPreChat;
1466
+ this.pendingMessageAfterPreChat = null;
1467
+ if (pending) {
1468
+ await this.sendMessage(pending.message, {
1469
+ attachmentTokens: pending.attachmentTokens
1470
+ });
1471
+ }
1472
+ }
1473
+ /** Dismiss the pre-chat form without submitting. The form will reappear
1474
+ * on the next sendMessage attempt — call `setConsent` to acknowledge a
1475
+ * refusal, or `reset()` to clear pending state. */
1476
+ cancelPreChatForm() {
1477
+ this.pendingMessageAfterPreChat = null;
1478
+ this.setState({ preChatFormVisible: false });
1479
+ }
1480
+ /** Programmatically dispatch the action attached to a trigger. Used by
1481
+ * integrators who want to act on a custom button, e.g. an exit-intent
1482
+ * modal in their own UI. */
1483
+ fireTrigger(triggerId) {
1484
+ const trigger = this.state.triggers.find((t) => t.id === triggerId);
1485
+ if (!trigger) return;
1486
+ this.triggersRuntime?.markFired(trigger.id, trigger.frequency);
1487
+ this.handleTriggerAction(trigger);
1488
+ }
1489
+ // ── Internals ──────────────────────────────────────────────────────
1490
+ pendingMessageAfterPreChat = null;
1491
+ shouldShowPreChatForm() {
1492
+ const form = this.state.preChatForm;
1493
+ if (!form) return false;
1494
+ if (this.preChatFormSubmitted) return false;
1495
+ if (this.state.conversationId) return false;
1496
+ if (form.skipForIdentified && this.identityData?.userId) return false;
1497
+ return true;
1498
+ }
1499
+ startTriggersRuntimeIfPossible() {
1500
+ if (this.triggersRuntime) return;
1501
+ if (this.state.triggers.length === 0) return;
1502
+ this.triggersRuntime = startTriggersRuntime({
1503
+ chatbotId: this.state.config.chatbotId,
1504
+ triggers: this.state.triggers,
1505
+ isAllowedToFire: () => this.state.consent.analytics,
1506
+ onFire: (trigger) => this.handleTriggerAction(trigger)
1507
+ });
1508
+ }
1509
+ handleTriggerAction(trigger) {
1510
+ this.setState({ pendingTriggerId: trigger.id });
1511
+ const action = trigger.action;
1512
+ switch (action.kind) {
1513
+ case "open_widget":
1514
+ this.open();
1515
+ break;
1516
+ case "open_with_prefill":
1517
+ this.open();
1518
+ this.setState({ pendingPrefill: action.prefill });
1519
+ break;
1520
+ case "show_form":
1521
+ this.open();
1522
+ if (this.state.preChatForm) {
1523
+ this.setState({ preChatFormVisible: true });
1524
+ }
1525
+ break;
1526
+ case "send_message":
1527
+ this.open();
1528
+ if (this.state.messages.length === 0) {
1529
+ this.setState({
1530
+ messages: [{ role: "bot", content: action.message }]
1531
+ });
1532
+ }
1533
+ break;
1534
+ }
1535
+ }
1536
+ /** Read and clear the pending prefill (set by an `open_with_prefill`
1537
+ * trigger). The host calls this once when mounting the input and seeds
1538
+ * its controlled value with the result. */
1539
+ consumePendingPrefill() {
1540
+ const prefill = this.state.pendingPrefill;
1541
+ if (prefill !== null) this.setState({ pendingPrefill: null });
1542
+ return prefill;
1543
+ }
1544
+ /** Stop the triggers runtime and detach listeners. Safe to call multiple
1545
+ * times; safe to call before the runtime started. */
1546
+ destroy() {
1547
+ this.triggersRuntime?.stop();
1548
+ this.triggersRuntime = null;
1549
+ }
1014
1550
  identify(payload) {
1015
1551
  const { userId, email, name, phone, company, userHash, ...rest } = payload;
1016
1552
  const customProperties = {};
@@ -1216,6 +1752,9 @@ export {
1216
1752
  captureScreenshot,
1217
1753
  createTranslator,
1218
1754
  detectLocale,
1755
+ evaluate,
1219
1756
  isRtlLocale,
1220
- resolveLocale
1757
+ pickFire,
1758
+ resolveLocale,
1759
+ startTriggersRuntime
1221
1760
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@customerhero/js",
3
- "version": "1.0.1",
3
+ "version": "2.0.0",
4
4
  "private": false,
5
5
  "description": "Framework-agnostic JavaScript client for the CustomerHero chat widget.",
6
6
  "keywords": [