Към съдържанието

Интеграция на Widget

Интегрирайте чат уиджет (приставка) ChatHub във вашия уебсайт, за да осигурите поддръжка чрез чат в реално време за вашите клиенти.

Преглед

Уиджетът ChatHub е JavaScript компонент, който:

  • Се вгражда във всеки уебсайт
  • Предоставя интерфейс за чат в реално време
  • Свързва клиенти с оператори
  • Изисква токен за удостоверяване на оператор
  • Се зарежда като ES модул

Бърз старт

1. Вземете Operator Token

Първо, вземете токен на оператор, следвайки работния процес на автентикация:

// 1. Вземане на company token
const companyToken = await getCompanyToken(login, password);

// 2. Вземане на организация
const organizations = await getOrganizations(companyToken);
const orgId = organizations[0].id;

// 3. Вземане на оператор
const operators = await getOperators(companyToken, orgId);
const operatorId = operators[0].id;

// 4. Генериране на operator token
const operatorToken = await getOperatorToken(
  companyToken,
  operatorId,
  expiresAt
);

// 5. Валидиране на токена
const isValid = await validateToken(companyToken, operatorToken);

2. Вградете уиджета

Добавете скрипта за уиджета към вашия HTML:

<!DOCTYPE html>
<html>
<head>
    <title>Вашият Уебсайт</title>
</head>
<body>
    <!-- Съдържание на вашия уебсайт -->

    <!-- ChatHub Widget -->
    <script type="module" id="operator-chat-panel-script"
      src="https://widget.smsbat.com/operator-chat-panel/widget-script.js"
      token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."></script>
</body>
</html>

Параметри на скрипта

Атрибут Стойност Задължителен Описание
type module Да Тип на ES модула
id operator-chat-panel-script Да Уникален идентификатор на скрипта
src Widget URL Да Местоположение на скрипта за уиджета
token JWT token Да Токен за удостоверяване на оператор

Методи за интеграция

Статичен HTML

За статични уебсайтове вградете директно в HTML:

<!DOCTYPE html>
<html lang="bg">
<head>
    <meta charset="UTF-8">
    <title>Моят Уебсайт</title>
</head>
<body>
    <h1>Добре дошли в Моя Уебсайт</h1>

    <!-- ChatHub Widget -->
    <script type="module" id="operator-chat-panel-script"
      src="https://widget.smsbat.com/operator-chat-panel/widget-script.js"
      token="ВАШИЯТ_OPERATOR_TOKEN"></script>
</body>
</html>

Динамично инжектиране (JavaScript)

За приложения с една страница (Single-page applications), инжектирайте динамично:

function loadChatHubWidget(operatorToken) {
  // Проверете дали уиджетът вече е зареден
  const existing = document.getElementById('operator-chat-panel-script');
  if (existing) {
    existing.remove();
  }

  // Създаване на елемент script
  const script = document.createElement('script');
  script.type = 'module';
  script.id = 'operator-chat-panel-script';
  script.src = 'https://widget.smsbat.com/operator-chat-panel/widget-script.js';
  script.setAttribute('token', operatorToken);

  // Добавяне към тялото (body)
  document.body.appendChild(script);
}

// Употреба
const token = await getOperatorToken();
loadChatHubWidget(token);

React

import { useEffect } from 'react';

function ChatHubWidget({ operatorToken }) {
  useEffect(() => {
    if (!operatorToken) return;

    // Зареждане на уиджета
    const script = document.createElement('script');
    script.type = 'module';
    script.id = 'operator-chat-panel-script';
    script.src = 'https://widget.smsbat.com/operator-chat-panel/widget-script.js';
    script.setAttribute('token', operatorToken);

    document.body.appendChild(script);

    // Почистване при демонтиране (unmount)
    return () => {
      const existing = document.getElementById('operator-chat-panel-script');
      if (existing) {
        existing.remove();
      }
    };
  }, [operatorToken]);

  return null;
}

// Употреба
function App() {
  const [token, setToken] = useState('');

  useEffect(() => {
    async function init() {
      const operatorToken = await fetchOperatorToken();
      setToken(operatorToken);
    }
    init();
  }, []);

  return (
    <div>
      <h1>Моето Приложение</h1>
      <ChatHubWidget operatorToken={token} />
    </div>
  );
}

Vue.js

<template>
  <div id="app">
    <h1>Моето Приложение</h1>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      operatorToken: ''
    };
  },
  async mounted() {
    // Вземане на operator token
    this.operatorToken = await this.fetchOperatorToken();

    // Зареждане на уиджет
    this.loadWidget();
  },
  methods: {
    async fetchOperatorToken() {
      // Вашата логика за извличане на токен
      const response = await fetch('/api/chathub/token');
      return response.text();
    },
    loadWidget() {
      if (!this.operatorToken) return;

      const script = document.createElement('script');
      script.type = 'module';
      script.id = 'operator-chat-panel-script';
      script.src = 'https://widget.smsbat.com/operator-chat-panel/widget-script.js';
      script.setAttribute('token', this.operatorToken);

      document.body.appendChild(script);
    }
  },
  beforeUnmount() {
    const script = document.getElementById('operator-chat-panel-script');
    if (script) {
      script.remove();
    }
  }
};
</script>

Angular

import { Component, OnInit, OnDestroy } from '@angular/core';

@Component({
  selector: 'app-root',
  template: '<h1>Моето Приложение</h1>'
})
export class AppComponent implements OnInit, OnDestroy {
  private operatorToken: string = '';

  async ngOnInit() {
    // Вземане на operator token
    this.operatorToken = await this.fetchOperatorToken();

    // Зареждане на уиджета
    this.loadWidget();
  }

  ngOnDestroy() {
    const script = document.getElementById('operator-chat-panel-script');
    if (script) {
      script.remove();
    }
  }

  private async fetchOperatorToken(): Promise<string> {
    const response = await fetch('/api/chathub/token');
    return response.text();
  }

  private loadWidget() {
    if (!this.operatorToken) return;

    const script = document.createElement('script');
    script.type = 'module';
    script.id = 'operator-chat-panel-script';
    script.src = 'https://widget.smsbat.com/operator-chat-panel/widget-script.js';
    script.setAttribute('token', this.operatorToken);

    document.body.appendChild(script);
  }
}

Управление на токени

server-side Генериране на токени

Никога не излагайте идентификационни данни на компанията в кода на клиента. Генерирайте токени на вашия сървър:

// Node.js Express пример
const express = require('express');
const app = express();

app.get('/api/chathub/token', async (req, res) => {
  try {
    // Първо удостоверете вашия потребител
    const userId = req.session.userId;
    if (!userId) {
      return res.status(401).json({ error: 'Неупълномощен' });
    }

    // Вземете токена на компанията (съхранява се сигурно на сървъра)
    const companyToken = process.env.CHATHUB_COMPANY_TOKEN;

    // Вземете ID на оператор за този потребител
    const operatorId = await getOperatorIdForUser(userId);

    // Генерирайте operator token
    const operatorToken = await generateOperatorToken(
      companyToken,
      operatorId
    );

    res.json({ token: operatorToken });
  } catch (error) {
    res.status(500).json({ error: 'Неуспешно генериране на токен' });
  }
});

async function generateOperatorToken(companyToken, operatorId) {
  const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 часа

  const response = await fetch(
    'https://chatapi.smsbat.com/api/operator/get-token',
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${companyToken}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        id: operatorId,
        expiresAt: expiresAt.toISOString()
      })
    }
  );

  return response.text();
}

Опресняване на токен

Приложете автоматично опресняване на токена:

class WidgetTokenManager {
  constructor() {
    this.token = null;
    this.expiresAt = null;
    this.refreshInterval = null;
  }

  async initialize() {
    await this.refreshToken();

    // Опресняване на токена 1 час преди изтичането му
    this.refreshInterval = setInterval(
      () => this.checkAndRefresh(),
      60 * 60 * 1000 // Проверка на всеки час
    );
  }

  async refreshToken() {
    const response = await fetch('/api/chathub/token');
    const data = await response.json();

    this.token = data.token;
    this.expiresAt = new Date(data.expiresAt);

    this.reloadWidget();
  }

  async checkAndRefresh() {
    const oneHour = 60 * 60 * 1000;
    const timeUntilExpiry = this.expiresAt - Date.now();

    if (timeUntilExpiry < oneHour) {
      await this.refreshToken();
    }
  }

  reloadWidget() {
    // Премахване на стария уиджет
    const existing = document.getElementById('operator-chat-panel-script');
    if (existing) {
      existing.remove();
    }

    // Зареждане на новия уиджет със свеж токен
    const script = document.createElement('script');
    script.type = 'module';
    script.id = 'operator-chat-panel-script';
    script.src = 'https://widget.smsbat.com/operator-chat-panel/widget-script.js';
    script.setAttribute('token', this.token);

    document.body.appendChild(script);
  }

  destroy() {
    if (this.refreshInterval) {
      clearInterval(this.refreshInterval);
    }
  }
}

// Употреба
const widgetManager = new WidgetTokenManager();
await widgetManager.initialize();

Множество организации

Зареждайте различни уиджети за различни организации:

function loadWidgetForOrganization(organizationId) {
  return new Promise((resolve, reject) => {
    // Вземане на оператор за тази организация
    fetch(`/api/chathub/token?org=${organizationId}`)
      .then(response => response.json())
      .then(data => {
        const script = document.createElement('script');
        script.type = 'module';
        script.id = `operator-chat-panel-script-${organizationId}`;
        script.src = 'https://widget.smsbat.com/operator-chat-panel/widget-script.js';
        script.setAttribute('token', data.token);

        script.onload = () => resolve();
        script.onerror = () => reject(new Error('Неуспешно зареждане на уиджет'));

        document.body.appendChild(script);
      })
      .catch(reject);
  });
}

// Употреба
await loadWidgetForOrganization('sales');
await loadWidgetForOrganization('support');

Добри практики

Сигурност

  • ✅ Генериране на токени сървърно
  • ✅ Никога не излагайте идентификационните данни на компанията в клиентския код
  • ✅ Използвайте HTTPS за всички API заявки
  • ✅ Внедрете изтичане на токените
  • ✅ Валидирайте токените преди употреба
  • ❌ Не съхранявайте токени в localStorage без криптиране
  • ❌ Не добавяйте токени в системата за контрол на версиите (Git)

Производителност

  • ✅ Зареждайте уиджета асинхронно
  • ✅ Използвайте ES модули (модерни браузъри)
  • ✅ Внедрете кеширане на токени
  • ✅ Обработвайте грешки грациозно
  • ❌ Не блокирайте зареждането на страницата

Потребителско изживяване (UX)

  • ✅ Показвайте състояние на зареждане, докато уиджетът инициализира
  • ✅ Обработвайте мрежови грешки
  • ✅ Предоставете резервен метод (fallback) за контакт
  • ✅ Тествайте на различни браузъри и устройства

Обработка на грешки

async function loadWidgetSafely(operatorToken) {
  try {
    // Валидирайте токена първо
    const isValid = await validateToken(operatorToken);

    if (!isValid) {
      console.error('Невалиден operator token');
      showFallbackContact();
      return;
    }

    // Зареждане на уиджета
    await loadWidget(operatorToken);

  } catch (error) {
    console.error('Неуспешно зареждане на чат уиджет:', error);
    showFallbackContact();
  }
}

function showFallbackContact() {
  // Показване на алтернативен метод за контакт
  const fallback = document.createElement('div');
  fallback.innerHTML = `
    <div class="chat-fallback">
      <p>Чатът е временно недостъпен.</p>
      <p>Свържете се с нас: <a href="mailto:support@example.com">support@example.com</a></p>
    </div>
  `;
  document.body.appendChild(fallback);
}

Отстраняване на проблеми

Уиджетът не се зарежда

  1. Проверете дали operator token е валиден
  2. Уверете се, че токенът не е изтекъл
  3. Уверете се, че URL адресът на скрипта е правилен
  4. Проверете конзолата на браузъра за грешки
  5. Проверете мрежовата свързаност

Токенът е изтекъл

// Откриване на изтекъл токен и опресняване
window.addEventListener('error', async (event) => {
  if (event.message.includes('token expired')) {
    console.log('Токенът е изтекъл, опресняване...');
    await refreshWidgetToken();
  }
});

Множество инстанции на уиджета

Уверете се, че се зарежда само един уиджет в даден момент:

function loadWidgetOnce(token) {
  // Премахване на всякакви съществуващи уиджети
  const existingScripts = document.querySelectorAll(
    'script[id^="operator-chat-panel-script"]'
  );

  existingScripts.forEach(script => script.remove());

  // Зареждане на нов уиджет
  loadWidget(token);
}

Проблеми с различни източници (Cross-Origin)

Уверете се, че вашият домейн е разрешен (whitelisted). Свържете се с поддръжката, ако срещнете CORS грешки.

Тестване

Локално разработване

// Използвайте тестов токен за разработка
const isDevelopment = process.env.NODE_ENV === 'development';
const token = isDevelopment
  ? 'test-token-for-development'
  : await getProductionToken();

loadWidget(token);

Интеграционно тестване

describe('ChatHub Widget', () => {
  it('трябва да зареди уиджета с валиден токен', async () => {
    const token = await getTestToken();
    loadWidget(token);

    await waitFor(() => {
      const widget = document.getElementById('operator-chat-panel-script');
      expect(widget).toBeTruthy();
    });
  });

  it('трябва да обработи невалиден токен', async () => {
    const invalidToken = 'invalid-token';

    try {
      await loadWidget(invalidToken);
    } catch (error) {
      expect(error.message).toContain('Invalid token');
    }
  });
});

Следващи стъпки