close
Skip to content

Commit f94b1e5

Browse files
feat(tidio,#94): fix track(), add methods, events, pre-load config
Vendor-doc audit fixes for Tidio: Bug fix: track() previously wrote metadata into setContactProperties under the event name, silently breaking Tidio automation triggers. Now calls the documented tidioChatApi.track(eventName), and stores any metadata as a separate `${event}_metadata` contact property so it isn't lost. Methods (8): setColorPalette, display (async visibility distinct from show/hide), messageFromOperator, messageFromVisitor, addVisitorTags, setVisitorCurrency, plus open/close (split from show/hide which used to fire both unconditionally). Events (6 added to TidioEventName): setStatus, conversationStart, preFormFilled, resize, open, close. Existing ready/messageFromVisitor/ messageFromOperator/visitorJoined kept (visitorJoined retained for backward compat even though current public docs don't list it). Pre-load config (TidioLoadOptions): language → window.tidioChatLang, identify → window.tidioIdentify (set before script load per the documented installation pattern). Bundle within existing 6KB budget (raw 5915B; gzip ≈ 1/3). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3c44fe3 commit f94b1e5

File tree

2 files changed

+252
-12
lines changed

2 files changed

+252
-12
lines changed

‎src/providers/tidio.ts‎

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,31 @@ interface TidioAPI {
2020
tags?: string[]
2121
}): void
2222
setContactProperties(props: Record<string, unknown>): void
23+
track?(event: string): void
24+
setColorPalette?(hex: string): void
25+
display?(state: boolean): void
26+
messageFromOperator?(message: string): void
27+
messageFromVisitor?(message: string): void
28+
addVisitorTags?(tags: string[]): void
29+
setVisitorCurrency?(currency: { code: string; exchangeRate?: number }): void
2330
show(): void
2431
hide(): void
2532
open(): void
2633
close(): void
27-
display?(state: boolean): void
28-
messageFromOperator?(message: string): void
34+
}
35+
36+
interface TidioIdentity {
37+
distinct_id?: string
38+
email?: string
39+
name?: string
40+
phone?: string
41+
tags?: string[]
2942
}
3043

3144
interface TidioWindow {
3245
tidioChatApi?: TidioAPI
46+
tidioChatLang?: string
47+
tidioIdentify?: TidioIdentity
3348
}
3449

3550
function w(): TidioWindow {
@@ -47,9 +62,15 @@ const TIDIO_EVENTS = {
4762
messageFromVisitor: "tidioChat-messageFromVisitor",
4863
messageFromOperator: "tidioChat-messageFromOperator",
4964
visitorJoined: "tidioChat-visitorJoined",
65+
setStatus: "tidioChat-setStatus",
66+
conversationStart: "tidioChat-conversationStart",
67+
preFormFilled: "tidioChat-preFormFilled",
68+
resize: "tidioChat-resize",
69+
open: "tidioChat-open",
70+
close: "tidioChat-close",
5071
} as const
5172

52-
type TidioEventName = keyof typeof TIDIO_EVENTS
73+
export type TidioEventName = keyof typeof TIDIO_EVENTS
5374
const eventListeners = new Map<TidioEventName, Set<(payload: unknown) => void>>()
5475
const domHandlers = new Map<TidioEventName, () => void>()
5576

@@ -66,6 +87,10 @@ function bindDomEvent(event: TidioEventName): void {
6687

6788
export interface TidioLoadOptions extends LoadOptions {
6889
publicKey: string
90+
/** Pre-load language (writes window.tidioChatLang before script load). */
91+
language?: string
92+
/** Pre-load identity (writes window.tidioIdentify before script load). */
93+
identify?: TidioIdentity
6994
}
7095

7196
export async function load(options: TidioLoadOptions): Promise<void> {
@@ -82,13 +107,15 @@ export async function load(options: TidioLoadOptions): Promise<void> {
82107
})
83108
await waitForDefer(options.defer ?? "immediate")
84109

85-
bindDomEvent("ready")
86-
bindDomEvent("messageFromVisitor")
87-
bindDomEvent("messageFromOperator")
88-
bindDomEvent("visitorJoined")
110+
for (const evt of Object.keys(TIDIO_EVENTS) as TidioEventName[]) {
111+
bindDomEvent(evt)
112+
}
89113

90114
document.addEventListener(TIDIO_EVENTS.ready, () => readyResolve?.(), { once: true } as never)
91115

116+
if (options.language !== undefined) w().tidioChatLang = options.language
117+
if (options.identify !== undefined) w().tidioIdentify = options.identify
118+
92119
try {
93120
await injectScript({
94121
id: "ahize-tidio",
@@ -136,13 +163,56 @@ export function track<T extends EventMetadata = EventMetadata>(
136163
metadata?: T,
137164
): Promise<void> {
138165
if (!isBrowser()) return Promise.resolve()
139-
return queue.enqueue((api) => api.setContactProperties({ [event]: metadata }))
166+
return queue.enqueue((api) => {
167+
if (api.track) {
168+
// Prefer the documented automation-trigger API.
169+
api.track(event)
170+
// Tidio's track() doesn't accept metadata; surface it as contact properties
171+
// so it isn't lost.
172+
if (metadata) api.setContactProperties({ [`${event}_metadata`]: metadata })
173+
} else {
174+
api.setContactProperties({ [event]: metadata })
175+
}
176+
})
140177
}
141178

142179
export function pageView(_info?: { path?: string; locale?: string }): Promise<void> {
143180
return Promise.resolve()
144181
}
145182

183+
export function setColorPalette(hex: string): Promise<void> {
184+
if (!isBrowser()) return Promise.resolve()
185+
return queue.enqueue((api) => api.setColorPalette?.(hex))
186+
}
187+
188+
export function display(state: boolean): Promise<void> {
189+
if (!isBrowser()) return Promise.resolve()
190+
return queue.enqueue((api) => api.display?.(state))
191+
}
192+
193+
export function messageFromOperator(message: string): Promise<void> {
194+
if (!isBrowser()) return Promise.resolve()
195+
return queue.enqueue((api) => api.messageFromOperator?.(message))
196+
}
197+
198+
export function messageFromVisitor(message: string): Promise<void> {
199+
if (!isBrowser()) return Promise.resolve()
200+
return queue.enqueue((api) => api.messageFromVisitor?.(message))
201+
}
202+
203+
export function addVisitorTags(tags: string[]): Promise<void> {
204+
if (!isBrowser()) return Promise.resolve()
205+
return queue.enqueue((api) => api.addVisitorTags?.(tags))
206+
}
207+
208+
export function setVisitorCurrency(currency: {
209+
code: string
210+
exchangeRate?: number
211+
}): Promise<void> {
212+
if (!isBrowser()) return Promise.resolve()
213+
return queue.enqueue((api) => api.setVisitorCurrency?.(currency))
214+
}
215+
146216
export function on(event: TidioEventName, listener: (payload: unknown) => void): () => void {
147217
let set = eventListeners.get(event)
148218
if (!set) {
@@ -155,17 +225,24 @@ export function on(event: TidioEventName, listener: (payload: unknown) => void):
155225

156226
export function show(): Promise<void> {
157227
if (!isBrowser()) return Promise.resolve()
158-
return queue.enqueue((api) => {
159-
api.show()
160-
api.open()
161-
})
228+
return queue.enqueue((api) => api.show())
162229
}
163230

164231
export function hide(): Promise<void> {
165232
if (!isBrowser()) return Promise.resolve()
166233
return queue.enqueue((api) => api.hide())
167234
}
168235

236+
export function open(): Promise<void> {
237+
if (!isBrowser()) return Promise.resolve()
238+
return queue.enqueue((api) => api.open())
239+
}
240+
241+
export function close(): Promise<void> {
242+
if (!isBrowser()) return Promise.resolve()
243+
return queue.enqueue((api) => api.close())
244+
}
245+
169246
export function shutdown(): Promise<void> {
170247
if (!isBrowser()) return Promise.resolve()
171248
store.reset()
@@ -182,6 +259,8 @@ export async function destroy(): Promise<void> {
182259
removeScript("ahize-tidio")
183260
const g = w()
184261
Reflect.deleteProperty(g, "tidioChatApi")
262+
Reflect.deleteProperty(g, "tidioChatLang")
263+
Reflect.deleteProperty(g, "tidioIdentify")
185264
queue.reset()
186265
store.reset()
187266
readyPromise = undefined

‎test/tidio.browser.test.ts‎

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// @vitest-environment jsdom
2+
import { beforeEach, describe, expect, it, vi } from "vitest"
3+
4+
interface CallLog {
5+
name: string
6+
args: unknown[]
7+
}
8+
9+
function fakeApi(): {
10+
api: Record<string, unknown>
11+
log: CallLog[]
12+
} {
13+
const log: CallLog[] = []
14+
const rec =
15+
(name: string) =>
16+
(...args: unknown[]) =>
17+
log.push({ name, args })
18+
const api: Record<string, unknown> = {
19+
setVisitorData: rec("setVisitorData"),
20+
setContactProperties: rec("setContactProperties"),
21+
track: rec("track"),
22+
setColorPalette: rec("setColorPalette"),
23+
display: rec("display"),
24+
messageFromOperator: rec("messageFromOperator"),
25+
messageFromVisitor: rec("messageFromVisitor"),
26+
addVisitorTags: rec("addVisitorTags"),
27+
setVisitorCurrency: rec("setVisitorCurrency"),
28+
show: rec("show"),
29+
hide: rec("hide"),
30+
open: rec("open"),
31+
close: rec("close"),
32+
}
33+
return { api, log }
34+
}
35+
36+
async function bootTidio(
37+
options?: Partial<import("../src/providers/tidio.ts").TidioLoadOptions>,
38+
): Promise<{
39+
tidio: typeof import("../src/providers/tidio.ts")
40+
log: CallLog[]
41+
}> {
42+
const tidio = await import("../src/providers/tidio.ts")
43+
const { api, log } = fakeApi()
44+
// biome-ignore lint/suspicious/noExplicitAny: test shim
45+
;(globalThis as any).tidioChatApi = api
46+
const loadPromise = tidio.load({ publicKey: "pub_xyz", ...options })
47+
await new Promise((r) => setTimeout(r, 0))
48+
const script = document.getElementById("ahize-tidio") as HTMLScriptElement
49+
expect(script).toBeTruthy()
50+
script.dispatchEvent(new Event("load"))
51+
await new Promise((r) => setTimeout(r, 60))
52+
document.dispatchEvent(new CustomEvent("tidioChat-ready"))
53+
await loadPromise
54+
return { tidio, log }
55+
}
56+
57+
describe("tidio (browser) — vendor-doc audit fixes (#94)", () => {
58+
beforeEach(() => {
59+
vi.resetModules()
60+
// biome-ignore lint/suspicious/noExplicitAny: test cleanup
61+
delete (globalThis as any).tidioChatApi
62+
// biome-ignore lint/suspicious/noExplicitAny: test cleanup
63+
delete (globalThis as any).tidioChatLang
64+
// biome-ignore lint/suspicious/noExplicitAny: test cleanup
65+
delete (globalThis as any).tidioIdentify
66+
const scripts = document.querySelectorAll("script")
67+
for (let i = 0; i < scripts.length; i++) {
68+
;(scripts[i] as { remove(): void } | undefined)?.remove()
69+
}
70+
})
71+
72+
it("language + identify pre-load globals are written before script load", async () => {
73+
const { tidio } = await bootTidio({
74+
language: "tr",
75+
identify: { distinct_id: "u1", email: "u@example.com" },
76+
})
77+
// biome-ignore lint/suspicious/noExplicitAny: test inspect
78+
expect((globalThis as any).tidioChatLang).toBe("tr")
79+
// biome-ignore lint/suspicious/noExplicitAny: test inspect
80+
expect((globalThis as any).tidioIdentify).toEqual({
81+
distinct_id: "u1",
82+
email: "u@example.com",
83+
})
84+
await tidio.destroy()
85+
})
86+
87+
it("track() now calls tidioChatApi.track instead of setContactProperties", async () => {
88+
const { tidio, log } = await bootTidio()
89+
log.length = 0
90+
await tidio.track("plan_upgraded", { tier: "pro" })
91+
const trackCall = log.find((c) => c.name === "track")
92+
expect(trackCall?.args).toEqual(["plan_upgraded"])
93+
// metadata stored as a separate contact property to avoid loss
94+
const meta = log.find(
95+
(c) =>
96+
c.name === "setContactProperties" &&
97+
JSON.stringify(c.args).includes("plan_upgraded_metadata"),
98+
)
99+
expect(meta).toBeDefined()
100+
await tidio.destroy()
101+
})
102+
103+
it("show()/open() now distinct (no more conflated show+open)", async () => {
104+
const { tidio, log } = await bootTidio()
105+
log.length = 0
106+
await tidio.show()
107+
await tidio.open()
108+
await tidio.hide()
109+
await tidio.close()
110+
expect(log.map((c) => c.name)).toEqual(["show", "open", "hide", "close"])
111+
await tidio.destroy()
112+
})
113+
114+
it("setColorPalette/display/messageFromOperator/messageFromVisitor/addVisitorTags/setVisitorCurrency forward", async () => {
115+
const { tidio, log } = await bootTidio()
116+
log.length = 0
117+
await tidio.setColorPalette("#ff0000")
118+
await tidio.display(true)
119+
await tidio.messageFromOperator("welcome")
120+
await tidio.messageFromVisitor("hi")
121+
await tidio.addVisitorTags(["beta"])
122+
await tidio.setVisitorCurrency({ code: "TRY", exchangeRate: 32 })
123+
const names = log.map((c) => c.name)
124+
for (const m of [
125+
"setColorPalette",
126+
"display",
127+
"messageFromOperator",
128+
"messageFromVisitor",
129+
"addVisitorTags",
130+
"setVisitorCurrency",
131+
]) {
132+
expect(names).toContain(m)
133+
}
134+
await tidio.destroy()
135+
})
136+
137+
it("on(event) covers setStatus/conversationStart/preFormFilled/resize/open/close", async () => {
138+
const { tidio } = await bootTidio()
139+
let status: unknown
140+
let convStart = 0
141+
let openCount = 0
142+
tidio.on("setStatus", (p) => {
143+
status = p
144+
})
145+
tidio.on("conversationStart", () => {
146+
convStart++
147+
})
148+
tidio.on("open", () => {
149+
openCount++
150+
})
151+
152+
document.dispatchEvent(new CustomEvent("tidioChat-setStatus", { detail: { status: "online" } }))
153+
document.dispatchEvent(new CustomEvent("tidioChat-conversationStart"))
154+
document.dispatchEvent(new CustomEvent("tidioChat-open"))
155+
156+
expect(status).toEqual({ status: "online" })
157+
expect(convStart).toBe(1)
158+
expect(openCount).toBe(1)
159+
await tidio.destroy()
160+
})
161+
})

0 commit comments

Comments
 (0)