close
Skip to content

Commit f4ae65f

Browse files
feat(tawk,#93): expose missing methods, event bridges, visitor preload
Vendor-doc audit fixes for Tawk: Methods (12 + login/logout): maximize, minimize, toggle, popup, toggleVisibility, endChat, start, addTags, removeTags, getStatus, getWindowType, isChatMaximized, isChatMinimized, isChatHidden, isChatOngoing, isVisitorEngaged, login (restores past conversations via Tawk.login), logout. switchWidget gained the documented completion callback. identify() now also forwards phone (E.164). Event bridge: new public on(event, handler) wraps every documented Tawk hook (beforeLoad/load/statusChange/chatMaximized/.../offlineSubmit) with the same multi-listener pattern as onUnreadCountChange. The 15 declared hooks are now actually reachable. TawkLoadOptions: visitor{} preload (set before script downloads), customStyleZIndex, autoStart (defer socket), onBeforeLoad as a load-time shortcut to on('beforeLoad'). Bumps tawk bundle budget 6KB → 9KB (raw 8437B; gzip ≈ 1/3). Tawk's SDK surface is the largest of the providers we wrap. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent c367914 commit f4ae65f

File tree

3 files changed

+431
-6
lines changed

3 files changed

+431
-6
lines changed

‎scripts/bundle-budget.mjs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const BUDGETS = [
1414
{ glob: /^providers\/intercom\.mjs$/, max: 9216, label: "provider:intercom" },
1515
{ glob: /^providers\/helpscout\.mjs$/, max: 7168, label: "provider:helpscout" },
1616
{ glob: /^providers\/hubspot\.mjs$/, max: 7168, label: "provider:hubspot" },
17+
{ glob: /^providers\/tawk\.mjs$/, max: 9216, label: "provider:tawk" },
1718
{ glob: /^providers\/.+\.mjs$/, max: 6144, label: "provider" },
1819
{ glob: /^index\.mjs$/, max: 4096, label: "core" },
1920
{ glob: /^facade\.mjs$/, max: 3072, label: "facade" },

‎src/providers/tawk.ts‎

Lines changed: 222 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,46 @@ interface TawkVisitor {
1515
name?: string
1616
email?: string
1717
hash?: string
18+
phone?: string
19+
userId?: string
20+
}
21+
22+
interface TawkCustomStyle {
23+
zIndex?: number | string
1824
}
1925

2026
interface TawkAPI {
2127
visitor?: TawkVisitor
28+
customStyle?: TawkCustomStyle
29+
autoStart?: boolean
2230
setAttributes?: (attrs: Record<string, unknown>, cb?: (err?: Error) => void) => void
2331
addEvent?: (event: string, metadata?: Record<string, unknown>, cb?: (err?: Error) => void) => void
32+
addTags?: (tags: string[], cb?: (err?: Error) => void) => void
33+
removeTags?: (tags: string[], cb?: (err?: Error) => void) => void
2434
showWidget?: () => void
2535
hideWidget?: () => void
36+
toggleVisibility?: () => void
2637
maximize?: () => void
2738
minimize?: () => void
2839
toggle?: () => void
2940
popup?: () => void
3041
endChat?: () => void
42+
start?: (options?: { showWidget?: boolean }) => void
43+
getStatus?: () => "online" | "away" | "offline"
44+
getWindowType?: () => "inline" | "embed"
45+
isChatMaximized?: () => boolean
46+
isChatMinimized?: () => boolean
47+
isChatHidden?: () => boolean
48+
isChatOngoing?: () => boolean
49+
isVisitorEngaged?: () => boolean
3150
login?: (user: TawkVisitor, cb?: (err?: Error) => void) => void
3251
logout?: (cb?: (err?: Error) => void) => void
33-
switchWidget?: (options: { propertyId: string; widgetId: string }) => void
52+
switchWidget?: (
53+
options: { propertyId: string; widgetId: string },
54+
cb?: (err?: Error) => void,
55+
) => void
3456
// Event hooks Tawk exposes; each must be assignable.
57+
onBeforeLoad?: () => void
3558
onLoad?: () => void
3659
onStatusChange?: (status: "online" | "away" | "offline") => void
3760
onChatMaximized?: () => void
@@ -49,6 +72,8 @@ interface TawkAPI {
4972
onFileUpload?: (url: string) => void
5073
onTagsUpdated?: (data: unknown) => void
5174
onUnreadCountChanged?: (count: number) => void
75+
onPrechatSubmit?: (data: unknown) => void
76+
onOfflineSubmit?: (data: unknown) => void
5277
}
5378

5479
interface TawkWindow {
@@ -70,9 +95,32 @@ const queue = createQueue<TawkAPI>()
7095
const store = createIdentityStore()
7196
const lifecycle = createLifecycle()
7297
const unreadListeners = new Set<(count: number) => void>()
98+
const eventListeners = new Map<string, Set<(payload?: unknown) => void>>()
7399
let readyPromise: Promise<void> | undefined
74100
let readyResolve: (() => void) | undefined
75101

102+
const HOOK_TO_EVENT: Record<string, string> = {
103+
onBeforeLoad: "beforeLoad",
104+
onLoad: "load",
105+
onStatusChange: "statusChange",
106+
onChatMaximized: "chatMaximized",
107+
onChatMinimized: "chatMinimized",
108+
onChatHidden: "chatHidden",
109+
onChatStarted: "chatStarted",
110+
onChatEnded: "chatEnded",
111+
onChatMessageVisitor: "chatMessageVisitor",
112+
onChatMessageAgent: "chatMessageAgent",
113+
onChatMessageSystem: "chatMessageSystem",
114+
onAgentJoinChat: "agentJoinChat",
115+
onAgentLeaveChat: "agentLeaveChat",
116+
onChatSatisfaction: "chatSatisfaction",
117+
onVisitorNameChanged: "visitorNameChanged",
118+
onFileUpload: "fileUpload",
119+
onTagsUpdated: "tagsUpdated",
120+
onPrechatSubmit: "prechatSubmit",
121+
onOfflineSubmit: "offlineSubmit",
122+
}
123+
76124
const TAWK_COLLISION_SYMBOLS = ["L", "R", "T"]
77125

78126
function warnCollisions(): void {
@@ -87,9 +135,38 @@ function warnCollisions(): void {
87135
}
88136
}
89137

138+
export type TawkEventName =
139+
| "beforeLoad"
140+
| "load"
141+
| "statusChange"
142+
| "chatMaximized"
143+
| "chatMinimized"
144+
| "chatHidden"
145+
| "chatStarted"
146+
| "chatEnded"
147+
| "chatMessageVisitor"
148+
| "chatMessageAgent"
149+
| "chatMessageSystem"
150+
| "agentJoinChat"
151+
| "agentLeaveChat"
152+
| "chatSatisfaction"
153+
| "visitorNameChanged"
154+
| "fileUpload"
155+
| "tagsUpdated"
156+
| "prechatSubmit"
157+
| "offlineSubmit"
158+
90159
export interface TawkLoadOptions extends LoadOptions {
91160
propertyId: string
92161
widgetId?: string
162+
/** Visitor preload — must be assigned before the embed script downloads. */
163+
visitor?: { name?: string; email?: string; hash?: string; phone?: string; userId?: string }
164+
/** Tawk's only documented customStyle field. */
165+
customStyleZIndex?: number | string
166+
/** When false, defers the socket connection until start() is called. Default: true. */
167+
autoStart?: boolean
168+
/** Pre-load hook fired before the embed script downloads. */
169+
onBeforeLoad?: () => void
93170
}
94171

95172
export async function load(options: TawkLoadOptions): Promise<void> {
@@ -108,9 +185,31 @@ export async function load(options: TawkLoadOptions): Promise<void> {
108185
await waitForDefer(options.defer ?? "immediate")
109186
const a = api()
110187
w().Tawk_LoadStart = new Date()
111-
a.onLoad = () => {
112-
queue.ready(a)
113-
readyResolve?.()
188+
if (options.visitor) a.visitor = { ...options.visitor }
189+
if (options.customStyleZIndex !== undefined) {
190+
a.customStyle = { ...a.customStyle, zIndex: options.customStyleZIndex }
191+
}
192+
if (options.autoStart === false) a.autoStart = false
193+
// If the caller passed onBeforeLoad as a load option, register it through
194+
// the same emitter so the bridge below doesn't clobber it.
195+
if (options.onBeforeLoad) {
196+
let set = eventListeners.get("beforeLoad")
197+
if (!set) {
198+
set = new Set()
199+
eventListeners.set("beforeLoad", set)
200+
}
201+
set.add(options.onBeforeLoad)
202+
}
203+
// Wire every documented Tawk hook to our typed emitter (multi-listener bridge).
204+
for (const [hook, eventName] of Object.entries(HOOK_TO_EVENT)) {
205+
;(a as unknown as Record<string, (payload?: unknown) => void>)[hook] = (payload) => {
206+
const set = eventListeners.get(eventName)
207+
if (set) for (const l of set) l(payload)
208+
if (eventName === "load") {
209+
queue.ready(a)
210+
readyResolve?.()
211+
}
212+
}
114213
}
115214
a.onUnreadCountChanged = (count: number) => {
116215
for (const l of unreadListeners) l(count)
@@ -142,6 +241,7 @@ export function identify(identity: Identity): Promise<void> {
142241
const attrs: Record<string, unknown> = {}
143242
if (identity.name) attrs["name"] = identity.name
144243
if (identity.email) attrs["email"] = identity.email
244+
if (identity.phone) attrs["phone"] = identity.phone
145245
if (identity.verification?.kind === "hmac") attrs["hash"] = identity.verification.hash
146246
if (identity.attributes) Object.assign(attrs, identity.attributes)
147247
a.setAttributes?.(attrs, (err) => {
@@ -205,11 +305,126 @@ export function onUnreadCountChange(listener: (count: number) => void): () => vo
205305
return () => unreadListeners.delete(listener)
206306
}
207307

208-
export function switchWidget(options: { propertyId: string; widgetId: string }): Promise<void> {
308+
export function switchWidget(
309+
options: { propertyId: string; widgetId: string },
310+
cb?: (err?: Error) => void,
311+
): Promise<void> {
312+
if (!isBrowser()) return Promise.resolve()
313+
return queue.enqueue((a) => {
314+
a.switchWidget?.({ propertyId: options.propertyId, widgetId: options.widgetId }, cb)
315+
})
316+
}
317+
318+
function callMethod(name: keyof TawkAPI, ...args: unknown[]): Promise<void> {
209319
if (!isBrowser()) return Promise.resolve()
210320
return queue.enqueue((a) => {
211-
a.switchWidget?.({ propertyId: options.propertyId, widgetId: options.widgetId })
321+
const fn = a[name] as ((...rest: unknown[]) => unknown) | undefined
322+
fn?.(...args)
323+
})
324+
}
325+
326+
export function maximize(): Promise<void> {
327+
return callMethod("maximize")
328+
}
329+
330+
export function minimize(): Promise<void> {
331+
return callMethod("minimize")
332+
}
333+
334+
export function toggle(): Promise<void> {
335+
return callMethod("toggle")
336+
}
337+
338+
export function popup(): Promise<void> {
339+
return callMethod("popup")
340+
}
341+
342+
export function toggleVisibility(): Promise<void> {
343+
return callMethod("toggleVisibility")
344+
}
345+
346+
export function endChat(): Promise<void> {
347+
return callMethod("endChat")
348+
}
349+
350+
export function start(options?: { showWidget?: boolean }): Promise<void> {
351+
if (!isBrowser()) return Promise.resolve()
352+
return queue.enqueue((a) => a.start?.(options))
353+
}
354+
355+
export function addTags(tags: string[], cb?: (err?: Error) => void): Promise<void> {
356+
if (!isBrowser()) return Promise.resolve()
357+
return queue.enqueue((a) => a.addTags?.(tags, cb))
358+
}
359+
360+
export function removeTags(tags: string[], cb?: (err?: Error) => void): Promise<void> {
361+
if (!isBrowser()) return Promise.resolve()
362+
return queue.enqueue((a) => a.removeTags?.(tags, cb))
363+
}
364+
365+
export function login(
366+
user: { name?: string; email?: string; phone?: string; hash?: string; userId?: string },
367+
cb?: (err?: Error) => void,
368+
): Promise<void> {
369+
if (!isBrowser()) return Promise.resolve()
370+
store.identify({
371+
id: user.userId,
372+
name: user.name,
373+
email: user.email,
374+
phone: user.phone,
375+
...(user.hash ? { verification: { kind: "hmac" as const, hash: user.hash } } : {}),
212376
})
377+
return queue.enqueue((a) => a.login?.(user, cb))
378+
}
379+
380+
export function logout(cb?: (err?: Error) => void): Promise<void> {
381+
if (!isBrowser()) return Promise.resolve()
382+
store.reset()
383+
return queue.enqueue((a) => a.logout?.(cb))
384+
}
385+
386+
function syncRead<T>(name: keyof TawkAPI): T | undefined {
387+
if (!isBrowser()) return undefined
388+
const fn = w().Tawk_API?.[name] as (() => T) | undefined
389+
return fn?.()
390+
}
391+
392+
export function getStatus(): "online" | "away" | "offline" | undefined {
393+
return syncRead<"online" | "away" | "offline">("getStatus")
394+
}
395+
396+
export function getWindowType(): "inline" | "embed" | undefined {
397+
return syncRead<"inline" | "embed">("getWindowType")
398+
}
399+
400+
export function isChatMaximized(): boolean | undefined {
401+
return syncRead<boolean>("isChatMaximized")
402+
}
403+
404+
export function isChatMinimized(): boolean | undefined {
405+
return syncRead<boolean>("isChatMinimized")
406+
}
407+
408+
export function isChatHidden(): boolean | undefined {
409+
return syncRead<boolean>("isChatHidden")
410+
}
411+
412+
export function isChatOngoing(): boolean | undefined {
413+
return syncRead<boolean>("isChatOngoing")
414+
}
415+
416+
export function isVisitorEngaged(): boolean | undefined {
417+
return syncRead<boolean>("isVisitorEngaged")
418+
}
419+
420+
export function on(event: TawkEventName, listener: (payload?: unknown) => void): () => void {
421+
let set = eventListeners.get(event)
422+
if (!set) {
423+
set = new Set()
424+
eventListeners.set(event, set)
425+
}
426+
set.add(listener)
427+
return () => set?.delete(listener)
213428
}
214429

215430
export async function destroy(): Promise<void> {
@@ -222,6 +437,7 @@ export async function destroy(): Promise<void> {
222437
queue.reset()
223438
store.reset()
224439
unreadListeners.clear()
440+
eventListeners.clear()
225441
readyPromise = undefined
226442
readyResolve = undefined
227443
lifecycle.clearConfigHash()

0 commit comments

Comments
 (0)