@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.cjs +548 -6
- package/dist/index.d.cts +237 -1
- package/dist/index.d.ts +237 -1
- package/dist/index.js +544 -5
- package/package.json +1 -1
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
|
-
|
|
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({
|
|
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
|
-
|
|
1757
|
+
pickFire,
|
|
1758
|
+
resolveLocale,
|
|
1759
|
+
startTriggersRuntime
|
|
1221
1760
|
};
|