Widget
Для разработчиков

Widget

Чат-виджет — один script-тег на странице. Он показывает кнопку в углу, открывает чат с поддержкой, ленту сообщений и историю обращений. Посетителю ничего устанавливать и регистрироваться не нужно.

Минимальная установка

index.html
<script>
(function(w, d){
  function loadWidget(token){
    var s = d.createElement('script');
    s.async = 1;
    s.src = 'https://support.forestsnet.com/widget-bundle?ws=WORKSPACE_UUID';
    if (token) s.setAttribute('data-visitor-token', token);
    d.head.appendChild(s);
  }
  // Необязательно: подписанный токен посетителя с вашего сервера
  // (один посетитель на разных устройствах, см. /docs/widget/hmac).
  // Без него достаточно вызвать loadWidget().
  fetch('/api/supporthub/token', { credentials: 'include' })
    .then(function(r){ return r.ok ? r.json() : null; })
    .then(function(data){ loadWidget(data && data.token); })
    .catch(function(){ loadWidget(); });
})(window, document);
</script>
Для автозапуска нужен только параметр ?ws= — UUID проекта. Готовый сниппет с ним есть в Настройки → Каналы → Web Widget и в Конструктор виджета → Код для вставки. Ответ /widget-bundle уже содержит адрес API, а если сервер успел получить настройки виджета — и их, так что отдельный запрос за настройками не нужен. Иначе виджет запросит их сам.

Атрибуты script-тега

Бандл читает data-* со своего script-тега. Их можно писать прямо в тег или, в сниппете выше, ставить на создаваемый элемент через s.setAttribute(…) — так уже передаётся data-visitor-token.

АтрибутЧто делает
?ws= (query)UUID проекта. Обязателен для автозапуска.
data-emailEmail посетителя. При загрузке уходит в identify и перезаписывает email контакта.
data-nameИмя посетителя. Записывается, если у контакта ещё нет имени (или стоит заглушка вроде «Гость»).
data-phoneТелефон, как есть. Сохраняется в extra_data.phone контакта, если там пусто.
data-telegram-idTelegram ID посетителя (число). Записывается в контакт, если у него ещё нет Telegram ID.
data-visitor-tokenПодписанный токен посетителя с вашего сервера: связывает посетителя на разных устройствах с одним контактом. Формат — в HMAC visitor token, секрет для подписи отдаёт GET /api/v1/widget/signing-secret.
data-localeЯзык виджета: ru или en (en-GB → en). Порядок выбора языка — ниже.
data-cookie-domainДомен cookie посетителя, например .example.com, чтобы одна беседа сохранялась между поддоменами. Важнее значения из конструктора.
data-hide-launcher="true"Скрыть плавающую кнопку. Открывайте виджет своей кнопкой через SupportHub.open().
data-api-baseАдрес API. Ответ /widget-bundle сам задаёт адрес из настроек сервера, и тот важнее, поэтому со стандартным сниппетом атрибут ни на что не влияет.

Передача данных пользователя

Если сайт уже знает посетителя, передайте данные в тег. При загрузке виджет отправит их в POST /api/webhooks/widget/{ws}/identify и создаст или обновит контакт, даже если посетитель ещё не открывал чат. Поля, которые уже известны, форма перед чатом больше не спрашивает.

index.html (SSR / Blade / EJS / Twig)
<script
  async
  src="https://support.forestsnet.com/widget-bundle?ws=WORKSPACE_UUID"
  data-email="{{ user.email }}"
  data-name="{{ user.full_name }}"
  data-phone="{{ user.phone }}"
  data-telegram-id="{{ user.telegram_id }}"
></script>

Идентификация из JavaScript

Если пользователь входит в аккаунт уже после загрузки страницы (SPA), вызовите window.SupportHub.identify(). Он доступен сразу после выполнения бандла; вызовы, сделанные до окончания запуска виджета, отправятся, как только он запустится. До загрузки бандла window.SupportHub не существует.

app.js
// После входа пользователя:
await window.SupportHub.identify({
  email: user.email,
  name: user.fullName,
  phone: user.phone,
  telegram_id: user.telegramId,
  // visitor_token: tokenFromYourServer,
});

Поля — те же, что у data-*, плюс visitor_token; вместо name можно передать full_name. Передавайте только то, что знаете: пустые поля ничего не стирают. Email перезаписывается при каждом вызове; имя, телефон и Telegram ID записываются, только если у контакта их ещё нет.

Язык виджета

Виджет говорит на двух языках: ru и en. Если в конструкторе («Брендинг → Язык виджета») выбран «Всегда русский» или «Всегда английский» (brand.language — "ru" или "en"), виджет всегда на этом языке и цепочка ниже не действует. При «Авто» ("auto", по умолчанию) язык выбирается по цепочке сверху вниз, первое подходящее значение выигрывает:

1init({ locale })язык, переданный в SupportHub.init()
2window.__SH_LOCALEглобальная переменная, которую страница задала до загрузки бандла
3data-localeатрибут script-тега
4<html lang>язык страницы (ru, en, en-GB), если тексты проекта есть на этом языке
5navigator.languageязык браузера посетителя, если тексты проекта есть на этом языке
6computed.locales.defaultосновной язык проекта, если ничего выше не подошло

Шаги 1–3 действуют всегда. Шаги 4 и 5 — только для языков из computed.locales.supported в ответе /config: язык попадает туда, если на нём есть каждый текст, который проект менял в конструкторе (правило — на странице «Бренд и шапка»). Другие языки (fr, xx) пропускаются. Берутся первые две буквы: en-GB → en, ru-RU → ru. Чаще всего ничего делать не нужно: при <html lang="en"> виджет сам будет на английском, если тексты проекта есть на английском.

Атрибутом на script-теге
<script
  src="https://support.forestsnet.com/widget-bundle?ws=WORKSPACE_UUID"
  data-locale="en"
  async
></script>
Глобальной переменной до загрузки
<script>window.__SH_LOCALE = "en";</script>
<script src="https://support.forestsnet.com/widget-bundle?ws=WORKSPACE_UUID" async></script>
Ручной запуск через init() (SPA)
<!-- Бандл без ?ws=: сам он не запустится -->
<script>window.__SH_LOCALE = "en";</script>
<script src="https://support.forestsnet.com/widget-bundle" async
        onload="SupportHub.init({ workspace_id: 'WORKSPACE_UUID' })"></script>

<script>
  // Сменить язык на лету: убрать виджет и запустить заново
  function setWidgetLocale(lang) {
    window.__SH_LOCALE = lang; // язык виджета; тексты из конструктора переключатся вместе с ним
    window.SupportHub.destroy();
    window.SupportHub.init({ workspace_id: "WORKSPACE_UUID", locale: lang });
  }
</script>
Не вызывайте init(), если тег уже подключён с ?ws=: виджет запущен, и появится второй. applyConfig() для смены языка на живом сайте не подходит: он заменяет настройки виджета значениями по умолчанию плюс переданными полями (метод нужен для превью в конструкторе).

Тексты на двух языках

Кнопки и служебные надписи виджета всегда на языке виджета. Тексты, которые вы пишете в конструкторе виджета, можно задать для каждого языка: переключатель языка (RU / EN) стоит над разделом. Такие поля: Брендинг (заголовок, подзаголовок, название проекта), Приветствие (заголовок, подзаголовок, сообщение, кнопка, время ответа), Формы (сообщение перед чатом), Чат (подпись истории, просьба об оценке), Часы работы (офлайн, вне рабочих часов, кнопка закрытия), Системные сообщения и Email-мост (подпись и подсказка). Если текст на нужном языке пустой, показывается вариант на другом языке.

Вариант текста выбирает сам виджет — на том же языке, что и его кнопки (по цепочке выше, включая init({ locale })). Настройки приходят сразу со всеми языками (/config?locale=all): и встроенные в бандл при стандартном сниппете (?ws=), и запрошенные виджетом при запуске через init(). Так же по языкам работают ваши правки из «Тексты интерфейса»: виджет берёт правки своего языка, а у языка без правок остаются стандартные тексты.

Где хранится сессия посетителя

ID посетителя (vs_<random>) связывает анонимную сессию с контактом. Чтобы он переживал перезагрузки, закрытие вкладок и чистку отдельных хранилищ, виджет держит его в пяти местах:

#ХранилищеОсобенности
1cookie sh_visitor_<ws>1 год, Path=/, SameSite=Lax, Secure на https. Единственное хранилище, которое работает между поддоменами (через домен cookie). Safari (ITP) держит cookie, поставленные скриптом, не больше 7 дней.
2localStorage sh-visitor-<ws>Для одного origin, пока не очищены данные сайта.
3sessionStorage sh-visitor-<ws>До закрытия вкладки. Выручает, когда cookie и localStorage недоступны.
4IndexedDB sh-widgetПишется в фоне; читается при запуске, только если хранилища 1–3 пусты.
5localStorage sh-visitor-sessionСтарый ключ без привязки к проекту: читается для переноса старых посетителей, записывается, если его нет.

Порядок чтения: 1 → 2 → 3 → 5. Первое непустое значение выигрывает и копируется в остальные хранилища: если cookie пропал, при следующей загрузке ID поднимется из localStorage и снова запишется в cookie.

Вкладки одного браузера синхронизируются через BroadcastChannel('sh-widget-sync'), а где его нет — через событие storage, чтобы две одновременно открытые вкладки не завели два разных ID.

Настраивать здесь ничего не нужно. Единственная настройка — домен cookie для поддоменов (ниже).

Один посетитель на нескольких поддоменах

По умолчанию cookie ставится на текущий хост: посетитель на example.com и на app.example.com — два контакта и две беседы. Хранилища 2–5 всегда привязаны к origin, поэтому общая сессия возможна только через cookie с доменом:

  • В конструкторе: «Конструктор виджета → Расширенные → Cookie-домен» → .example.com. Сниппет на всех поддоменах остаётся тем же.
  • Атрибутом: data-cookie-domain=".example.com". Атрибут важнее значения из конструктора — удобно, если стейдж и прод на разных доменах.
Указывайте свой домен, а не публичный суффикс вроде .co.uk — такой cookie браузер отклонит. Точка в начале не обязательна: cookie с атрибутом Domain и так действует на все поддомены.

Перенос старых посетителей автоматический: при первом заходе ID из ключа sh-visitor-session записывается в cookie с заданным доменом, и на следующем поддомене браузер найдёт ту же сессию.

JS API

init, identify, logout, on и off есть на window.SupportHub сразу после выполнения бандла, остальные методы появляются, когда виджет запустился.

МетодНазначение
init(opts)Ручной запуск. Опции: workspace_id, locale, hideLauncher, apiBase, config, mode, onTokenExpired. С ?ws= в теге виджет запускается сам.
open()Открыть панель (например, по клику на свою кнопку).
close()Свернуть панель.
isOpen()true, если панель открыта.
setTab(tab)Переключить вкладку: "home" | "chat" | "help" | "news" | "miniapp".
identify(payload)Передать данные посетителя: email, name, phone, telegram_id, visitor_token. Возвращает Promise.
logout()Пользователь вышел с вашего сайта: забыть токен и данные identify, завести новый анонимный session ID, закрыть WebSocket и вернуть виджет на главную. Возвращает Promise. См. Identify посетителей.
on(event, cb)Подписаться на событие виджета (см. «События на странице»). Возвращает функцию отписки.
off(event, cb)Снять обработчик, добавленный через on.
applyConfig(cfg)Для превью в конструкторе: заменяет настройки значениями по умолчанию плюс переданными. На сайте не нужен.
showState(s, sub?)Демо-состояния для превью; на сайте ничего не делает.
destroy()Убрать виджет: закрывает WebSocket и удаляет его элементы со страницы. После этого можно снова вызвать init().

Что происходит при загрузке

  1. Бандл определяет адрес API (window.__SH_API_BASE): его задаёт ответ /widget-bundle; без него — data-api-base, origin скрипта или облако SupportHub. Читает data-* и, если в адресе скрипта есть ?ws=, запускает виджет.
  2. Берёт настройки из window.__SH_WIDGET_CONFIG или запрашивает GET /api/webhooks/widget/{ws}/config.
  3. Находит ID посетителя в хранилищах или создаёт новый.
  4. Отправляет данные посетителя, если они есть: POST /api/webhooks/widget/{ws}/identify.
  5. Спрашивает открытую беседу: GET /api/webhooks/widget/{ws}/recent. Если она есть — открывает её и подключает WebSocket; если нет — WebSocket подключится после первого сообщения посетителя.

Глобальные переменные

  • window.SupportHub — методы выше
  • window.__SH_API_BASE — адрес API
  • window.__SH_WIDGET_CONFIG — настройки виджета, если сервер вложил их в бандл
  • window.__SH_LOCALE — язык из data-locale или заданный страницей
  • window.__SH_WIDGET_V2_MOUNTED — защита от повторного автозапуска
  • Переменные с префиксом __sh_ — внутренние

WebSocket

Когда у посетителя есть беседа, виджет подключается к wss://api.support.forestsnet.com/api/ws/widget/{visitor_id}?workspace_id={ws} (с токеном посетителя — ещё &signed_token=…). Если контакта для этого посетителя нет, сервер закрывает соединение с кодом 4401. Кадры приходят в виде {"event": "…", "data": {…}}; посетитель получает события своих тикетов:

  • message.created — новое сообщение (внутренние заметки сюда не попадают)
  • message.edited — сообщение изменено (content, edited_at)
  • message.deleted — сообщение удалено (message_id, ticket_id)
  • typing — оператор печатает (ticket_id, sender), пока в конструкторе включены индикаторы печати
  • message.seen_by_operator — оператор открыл переписку (message_id, ticket_id)
  • ticket.assigned — оператор взял тикет (operator_name, operator_avatar_url)
  • ticket.closed, ticket.reopened, ticket.transferred, ticket.updated и другие события тикета

Сервер закрывает соединение, если от клиента 90 секунд ничего не приходило, и отмечает посетителя офлайн. Чтобы держать его открытым, шлите {"type": "ping"} — сервер ответит {"type": "pong"}; {"type": "typing", "ticket_id": "…"} показывает операторам, что посетитель печатает. Отклонённый signed_token сервер закрывает кодом 4440 (истёк) или 4401 (неверная подпись): с тем же токеном подключаться бесполезно. Виджет после разрыва переподключается сам и догружает сообщения, пришедшие за время разрыва; после такого кода он ждёт новый токен.

События на странице

SupportHub.on(event, cb) сообщает странице о двух вещах. token_expired — сервер отклонил visitor_token; в cb приходит { reason: "expired" | "invalid" }, один раз на токен: получите свежий и передайте в identify (см. Identify посетителей). unread — изменилось число непрочитанных ответов, { count }: пригодится, если кнопка виджета скрыта (hideLauncher) и вы рисуете свою. Чтобы реагировать на то, что происходит в чатах, на своём сервере используйте webhooks.

Была ли страница полезной?