Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions docs/content/scripts/tawk-to.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
---
title: Tawk.to
description: Load the Tawk.to live chat widget and drive it through a typed proxy, reactive state, and event listeners.
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/scripts/blob/main/packages/script/src/runtime/registry/tawk-to.ts
size: xs
---

[Tawk.to](https://www.tawk.to/) is a free live chat widget.

[`useScriptTawkTo()`{lang="ts"}](/scripts/tawk-to) loads the widget, types the `window.Tawk_API` command surface, and bridges the `window` events the embed script dispatches into reactive state and typed listeners.

::script-stats
::

::script-docs
::

Bundling and proxying are both off. Nobody has verified how the embed script resolves its own API origin, or what live-chat polling connections a proxy would end up in front of, so neither capability is declared rather than guessed at.

Find `propertyId` and `widgetId` under **Administration Settings → Channels → Chat Widget** in your Tawk.to dashboard.

::code-group

```ts [Proxy]
const { proxy } = useScriptTawkTo({
propertyId: 'YOUR_PROPERTY_ID',
widgetId: 'YOUR_WIDGET_ID',
})

function openChat() {
proxy.maximize()
}
```

```ts [onLoaded]
const { onLoaded } = useScriptTawkTo({
propertyId: 'YOUR_PROPERTY_ID',
widgetId: 'YOUR_WIDGET_ID',
})

onLoaded((Tawk_API) => {
Tawk_API.maximize()
})
```

::

## Reactive state and events

Tawk's embed script dispatches `window` `CustomEvent`s (`tawkLoad`, `tawkStatusChange`, `tawkChatMaximized`, …) alongside its documented `Tawk_API.onXxx = fn` callback-property API. `useScriptTawkTo()`{lang="ts"} bridges those events into five readonly refs and twenty typed listeners, so you don't have to wire `window.addEventListener` yourself:

Comment thread
coderabbitai[bot] marked this conversation as resolved.
```vue
<script setup lang="ts">
const { isHidden, isMinimized, isMaximized, chatStatus, unreadCount, onChatStarted, onChatEnded } = useScriptTawkTo({
propertyId: 'YOUR_PROPERTY_ID',
widgetId: 'YOUR_WIDGET_ID',
})

onChatStarted(() => {
console.log('visitor started a chat')
})
onChatEnded(() => {
console.log('chat ended')
})
</script>

<template>
<div v-if="!isHidden">
{{ chatStatus }} · {{ unreadCount }} unread · {{ isMinimized ? 'minimized' : isMaximized ? 'maximized' : 'default' }}
</div>
</template>
```

`chatStatus` is Tawk's own online/away/offline operator status (`getStatus()`{lang="ts"}). It's distinct from `status`, the generic script-load state every registry entry exposes.

Every `onXxx` listener returns a teardown function for use with `onScopeDispose`, mirroring the rest of the registry's event-listener helpers.

The state refs are a single instance shared by every `useScriptTawkTo()`{lang="ts"} call on the page (there's only ever one Tawk widget), not one instance per call.

## Getters

`proxy` is fire-and-forget: calls queue until the script loads and replay once it does, but their return value is always discarded, even after loading. That's fine for actions like `proxy.maximize()`{lang="ts"}, which don't return anything meaningful anyway, but it can't carry a real synchronous getter. `getWindowType`, `getStatus`, `isChatMaximized`, `isChatMinimized`, `isChatHidden`, `isChatOngoing`, `isVisitorEngaged`, and `widgetPosition` are exposed directly on `useScriptTawkTo()`{lang="ts"}'s return value instead, calling straight through to `window.Tawk_API`:

```ts
const { getStatus, isChatHidden } = useScriptTawkTo({
propertyId: 'YOUR_PROPERTY_ID',
widgetId: 'YOUR_WIDGET_ID',
})

getStatus() // 'online' | 'away' | 'offline' | undefined
isChatHidden() // boolean, false before the widget has loaded
```

## Identifying visitors

`proxy.visitor = {...}` doesn't work for the same reason: unhead's script proxy has no `set` trap, so a property assignment through it never reaches the real `Tawk_API`. Use `setVisitor()`{lang="ts"} instead.

It is pre-load only. Tawk honors `Tawk_API.visitor` before the embed script loads and ignores it afterwards, so once the embed has been requested the call warns and does nothing. Change identity after load with `window.Tawk_API.setAttributes({ name, email, phone, hash })`{lang="ts"}.

```ts
const { proxy, setVisitor } = useScriptTawkTo({
propertyId: 'YOUR_PROPERTY_ID',
widgetId: 'YOUR_WIDGET_ID',
})

setVisitor({
name: 'Jane Doe',
email: 'jane@example.com',
// HMAC-SHA256 signature for Secure Mode, generated server-side
hash: visitorHash,
})
proxy.setAttributes({ plan: 'pro' })
proxy.addTags(['vip'])
```

## Switching properties at runtime

```ts
const { proxy } = useScriptTawkTo({
propertyId: 'YOUR_PROPERTY_ID',
widgetId: 'YOUR_WIDGET_ID',
})

proxy.switchWidget({ propertyId: 'OTHER_PROPERTY_ID', widgetId: 'OTHER_WIDGET_ID' })
```

::script-types
::

## Partytown

Do not run Tawk.to under Partytown. The widget renders DOM overlays (the chat bubble, prechat and full chat panels) directly, and the `window` `CustomEvent`s the reactive state and listeners depend on aren't configured for worker forwarding.
1 change: 1 addition & 0 deletions packages/script/src/registry-logos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const LOGOS = {
light: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#0f172a"/><path d="M8.5 10.5 12.5 16l-4 5.5" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/><path d="M15.5 21.5h8" fill="none" stroke="#fff" stroke-width="2.2" stroke-linecap="round"/></svg>`,
dark: `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect width="32" height="32" rx="7" fill="#fff"/><path d="M8.5 10.5 12.5 16l-4 5.5" fill="none" stroke="#0f172a" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/><path d="M15.5 21.5h8" fill="none" stroke="#0f172a" stroke-width="2.2" stroke-linecap="round"/></svg>`,
},
tawkTo: `https://www.tawk.to/icon.png`,
crisp: {
light: `<svg height="30" width="35" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><filter id="a" height="138.7%" width="131.4%" x="-15.7%" y="-15.1%"><feMorphology in="SourceAlpha" operator="dilate" radius="1" result="shadowSpreadOuter1"/><feOffset dy="1" in="shadowSpreadOuter1" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="1"/><feComposite in="shadowBlurOuter1" in2="SourceAlpha" operator="out" result="shadowBlurOuter1"/><feColorMatrix in="shadowBlurOuter1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.07 0"/></filter><path id="b" d="M14.23 20.46l-9.65 1.1L3 5.12 30.07 2l1.58 16.46-9.37 1.07-3.5 5.72-4.55-4.8z"/></defs><g fill="none" fill-rule="evenodd"><use fill="#000" filter="url(#a)" xlink:href="#b"/><use fill="#1972f5" stroke="#1972f5" stroke-width="2" xlink:href="#b"/></g></svg>`,
dark: `<svg height="30" width="35" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><filter id="a" height="138.7%" width="131.4%" x="-15.7%" y="-15.1%"><feMorphology in="SourceAlpha" operator="dilate" radius="1" result="shadowSpreadOuter1"/><feOffset dy="1" in="shadowSpreadOuter1" result="shadowOffsetOuter1"/><feGaussianBlur in="shadowOffsetOuter1" result="shadowBlurOuter1" stdDeviation="1"/><feComposite in="shadowBlurOuter1" in2="SourceAlpha" operator="out" result="shadowBlurOuter1"/><feColorMatrix in="shadowBlurOuter1" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.07 0"/></filter><path id="b" d="M14.23 20.46l-9.65 1.1L3 5.12 30.07 2l1.58 16.46-9.37 1.07-3.5 5.72-4.55-4.8z"/></defs><g fill="none" fill-rule="evenodd"><use fill="#000" filter="url(#a)" xlink:href="#b"/><use fill="#fff" stroke="#fff" stroke-width="2" xlink:href="#b"/></g></svg>`,
Expand Down
56 changes: 56 additions & 0 deletions packages/script/src/registry-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -1233,6 +1233,48 @@
"code": "interface ScriptStripePricingTableSlots {\n default?: () => any\n loading?: () => any\n awaitingLoad?: () => any\n error?: () => any\n}"
}
],
"tawk-to": [
{
"name": "TawkToOptions",
"kind": "const",
"code": "export const TawkToOptions = object({\n /**\n * Your Tawk.to property ID: the first path segment of the widget embed URL\n * `https://embed.tawk.to/<propertyId>/<widgetId>`.\n * @see https://dashboard.tawk.to/\n */\n propertyId: pipe(string(), minLength(1)),\n /**\n * Your Tawk.to widget ID: the second path segment of the widget embed URL\n * `https://embed.tawk.to/<propertyId>/<widgetId>`.\n * @see https://dashboard.tawk.to/\n */\n widgetId: pipe(string(), minLength(1)),\n})"
},
{
"name": "TawkToStatus",
"kind": "type",
"code": "export type TawkToStatus = 'online' | 'away' | 'offline'"
},
{
"name": "TawkToWindowType",
"kind": "type",
"code": "export type TawkToWindowType = 'inline' | 'embed'"
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{
"name": "TawkToWidgetPosition",
"kind": "type",
"code": "export type TawkToWidgetPosition = 'br' | 'bl' | 'cr' | 'cl' | 'tr' | 'tl'"
},
{
"name": "TawkToVisitor",
"kind": "interface",
"code": "export interface TawkToVisitor {\n name?: string\n email?: string\n /** Visitor phone number in E.164 format (e.g. `+15551234567`). */\n phone?: string\n /** HMAC-SHA256 signature for Tawk's Secure Mode, generated server-side by the consumer. */\n hash?: string\n}"
},
{
"name": "TawkToApi",
"kind": "interface",
"code": "export interface TawkToApi {\n /** Starts the chat. Pass `{ showWidget: true }` to also show the widget. */\n start: (options?: { showWidget?: boolean }) => void\n shutdown: () => void\n maximize: () => void\n minimize: () => void\n toggle: () => void\n popup: () => void\n showWidget: () => void\n hideWidget: () => void\n toggleVisibility: () => void\n endChat: () => void\n\n getWindowType: () => TawkToWindowType\n getStatus: () => TawkToStatus\n isChatMaximized: () => boolean\n isChatMinimized: () => boolean\n isChatHidden: () => boolean\n isChatOngoing: () => boolean\n isVisitorEngaged: () => boolean\n /** Set by Tawk once the widget has finished loading. The embed writes a boolean (`!0`), not a number. */\n onLoaded?: boolean\n /** Set by Tawk before the widget begins loading. */\n onBeforeLoaded?: boolean\n widgetPosition: () => TawkToWidgetPosition\n\n visitor?: TawkToVisitor\n setAttributes: (attributes: Record<string, string>, callback?: (error: Error | null) => void) => void\n addEvent: (event: string, metadata?: Record<string, unknown>, callback?: (error: Error | null) => void) => void\n addTags: (tags: string[], callback?: (error: Error | null) => void) => void\n removeTags: (tags: string[], callback?: (error: Error | null) => void) => void\n switchWidget: (data: { propertyId: string, widgetId: string }, callback?: (error: Error | null) => void) => void\n}"
},
{
"name": "TawkToProxyApi",
"kind": "type",
"code": "export type TawkToProxyApi = Omit<TawkToApi, 'getWindowType' | 'getStatus' | 'isChatMaximized' | 'isChatMinimized' | 'isChatHidden' | 'isChatOngoing' | 'isVisitorEngaged' | 'widgetPosition' | 'visitor' | 'onLoaded' | 'onBeforeLoaded'>"
},
{
"name": "TawkToEvents",
"kind": "interface",
"code": "export interface TawkToEvents {\n isHidden: Readonly<Ref<boolean>>\n isMinimized: Readonly<Ref<boolean>>\n isMaximized: Readonly<Ref<boolean>>\n /** The operator status Tawk reports (`getStatus()`) — not to be confused with `status`, the script's own load state. */\n chatStatus: Readonly<Ref<TawkToStatus>>\n unreadCount: Readonly<Ref<number>>\n\n // Getters, called directly against `window.Tawk_API` (see `TawkToProxyApi`'s\n // doc comment for why `proxy` can't carry these). `undefined` before the\n // widget has loaded; the four `isXxx` booleans default to `false` instead\n // since a definite \"no\" is a safe, honest answer before load.\n getWindowType: () => TawkToWindowType | undefined\n getStatus: () => TawkToStatus | undefined\n isChatMaximized: () => boolean\n isChatMinimized: () => boolean\n isChatHidden: () => boolean\n isChatOngoing: () => boolean\n isVisitorEngaged: () => boolean\n widgetPosition: () => TawkToWidgetPosition | undefined\n\n /**\n * Sets `Tawk_API.visitor` directly - assigning through `proxy.visitor` is a no-op\n * (no `set` trap). Pre-insertion only: Tawk honors `Tawk_API.visitor` until the\n * embed script is requested, so a call after that warns and does nothing - use\n * `window.Tawk_API.setAttributes()` for post-load identity changes.\n */\n setVisitor: (data: TawkToVisitor) => void\n\n onLoad: (cb: () => void) => () => void\n onBeforeLoad: (cb: () => void) => () => void\n onStatusChange: (cb: (status: TawkToStatus) => void) => () => void\n onChatMaximized: (cb: () => void) => () => void\n onChatMinimized: (cb: () => void) => () => void\n onChatHidden: (cb: () => void) => () => void\n onChatStarted: (cb: () => void) => () => void\n onChatEnded: (cb: () => void) => () => void\n onPrechatSubmit: (cb: (data: Record<string, unknown>) => void) => () => void\n onOfflineSubmit: (cb: (data: Record<string, unknown>) => void) => () => void\n onChatMessageVisitor: (cb: (message: string) => void) => () => void\n onChatMessageAgent: (cb: (message: string) => void) => () => void\n onChatMessageSystem: (cb: (message: string) => void) => () => void\n onAgentJoinChat: (cb: (data: Record<string, unknown>) => void) => () => void\n onAgentLeaveChat: (cb: (data: Record<string, unknown>) => void) => () => void\n onChatSatisfaction: (cb: (satisfaction: number) => void) => () => void\n onVisitorNameChanged: (cb: (visitorName: string) => void) => () => void\n onFileUpload: (cb: (link: string) => void) => () => void\n onTagsUpdated: (cb: (data: Record<string, unknown>) => void) => () => void\n onUnreadCountChanged: (cb: (count: number) => void) => () => void\n}"
}
],
"tiktok-pixel": [
{
"name": "StandardEvents",
Expand Down Expand Up @@ -2826,6 +2868,20 @@
"defaultValue": "'basil'"
}
],
"TawkToOptions": [
{
"name": "propertyId",
"type": "string",
"required": true,
"description": "Your Tawk.to property ID: the first path segment of the widget embed URL `https://embed.tawk.to/<propertyId>/<widgetId>`."
},
{
"name": "widgetId",
"type": "string",
"required": true,
"description": "Your Tawk.to widget ID: the second path segment of the widget embed URL `https://embed.tawk.to/<propertyId>/<widgetId>`."
}
],
"TikTokPixelOptions": [
{
"name": "id",
Expand Down
9 changes: 9 additions & 0 deletions packages/script/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
SnapTrPixelOptions,
SpeedCurveOptions,
StripeOptions,
TawkToOptions,
TikTokPixelOptions,
UmamiAnalyticsOptions,
UsercentricsOptions,
Expand Down Expand Up @@ -90,7 +91,7 @@

// -- Privacy descriptions --

export const PRIVACY_DESCRIPTIONS: Record<string, PrivacyDescription> = {

Check warning on line 94 in packages/script/src/registry.ts

View workflow job for this annotation

GitHub Actions / lint

Annotating with `Record<string, PrivacyDescription>` discards the object's known keys. Use `satisfies` to keep them
'ip-only': { label: 'IP Only', description: 'Anonymises IP addresses; other data passes through.' },
'full': { label: 'Full', description: 'All identifying data is anonymised: IP, user agent, language, screen, timezone, and hardware.' },
'heatmap': { label: 'Heatmap', description: 'IP, language, and hardware fingerprints anonymised; screen data preserved for heatmap accuracy.' },
Expand Down Expand Up @@ -174,6 +175,7 @@
m('intercom', 'Intercom', 'support', 'useScriptIntercom', { bundle: true, proxy: true }, PRIVACY_IP_ONLY),
m('crisp', 'Crisp', 'support', 'useScriptCrisp', { bundle: true }, null),
m('deskcrew', 'DeskCrew', 'support', 'useScriptDeskCrew', {}, null),
m('tawkTo', 'Tawk.to', 'support', 'useScriptTawkTo', {}, null),
// cdn
m('npm', 'NPM', 'cdn', 'useScriptNpm', { bundle: true }, null),
// utility
Expand Down Expand Up @@ -778,6 +780,13 @@
composableName: 'useScriptDeskCrew',
envDefaults: { widgetKey: '', board: '' },
}),
// Bundle/proxy: unverified for Tawk, left off rather than guessed at.
def('tawkTo', {
schema: TawkToOptions,
label: 'Tawk.to',
category: 'support',
envDefaults: { propertyId: '', widgetId: '' },
}),
// cdn
def('npm', {
schema: NpmOptions,
Expand Down
15 changes: 15 additions & 0 deletions packages/script/src/runtime/registry/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,21 @@ export const StripeOptions = object({
version: optional(union([literal('v3'), literal('acacia'), literal('basil'), literal('clover'), literal('dahlia'), string()])),
})

export const TawkToOptions = object({
/**
* Your Tawk.to property ID: the first path segment of the widget embed URL
* `https://embed.tawk.to/<propertyId>/<widgetId>`.
* @see https://dashboard.tawk.to/
*/
propertyId: pipe(string(), minLength(1)),
/**
* Your Tawk.to widget ID: the second path segment of the widget embed URL
* `https://embed.tawk.to/<propertyId>/<widgetId>`.
* @see https://dashboard.tawk.to/
*/
widgetId: pipe(string(), minLength(1)),
})

export const TikTokPixelOptions = object({
/**
* Your TikTok Pixel ID.
Expand Down
Loading
Loading