Transforme o Whaticket em Central de Ligações Grátis!

Links para os downloads gratuitos

Bloco de Notas

				
					## Whaticket

1- INSTALAR ASTERISK COM WSS E WEBRTC
2- INSTALAR O WHATICKET
				
			

frontend\src\components\AsteriskWebphone\AsteriskWebphone.css

				
					.asterisk-webphone-modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.asterisk-webphone {
  position: relative;
  max-width: 280px;
  margin: 0 auto;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: white;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.webphone-container {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.status-indicator {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  font-size: 14px;
  padding: 8px;
  background: #f8f9fa;
  border-radius: 4px;
}

.text-positive {
  color: #4caf50;
}

.number-input {
  width: 100%;
  padding: 12px;
  font-size: 16px;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
  text-align: center;
  outline: none;
}

.number-input:focus {
  border-color: #1976d2;
}

.keypad {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.keypad-row {
  display: flex;
  justify-content: space-between;
  gap: 8px;
}

.keypad-button {
  flex: 1;
  aspect-ratio: 1;
  border: none;
  background: none;
  font-size: 24px;
  color: #1976d2;
  border-radius: 50%;
  cursor: pointer;
  transition: all 0.2s;
}

.keypad-button:hover {
  background: #f0f0f0;
}

.keypad-button:active {
  background: #e0e0e0;
  transform: scale(0.95);
}

.call-controls {
  display: flex;
  justify-content: center;
  gap: 24px;
  margin-top: 16px;
}

.call-button, .hangup-button {
  width: 56px;
  height: 56px;
  border: none;
  border-radius: 50%;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.2s;
}

.call-button {
  background: #4caf50;
  color: white;
}

.hangup-button {
  background: #f44336;
  color: white;
}

.call-button:hover:not(.disabled) {
  background: #43a047;
  transform: scale(1.05);
}

.hangup-button:hover:not(.disabled) {
  background: #e53935;
  transform: scale(1.05);
}

.call-button.disabled, .hangup-button.disabled {
  background: #e0e0e0;
  cursor: not-allowed;
  color: #9e9e9e;
}

.call-info {
  text-align: center;
  font-size: 14px;
  color: #666;
}

.disconnected-message {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  color: #f57c00;
  font-size: 14px;
  padding: 12px;
  background: #fff3e0;
  border-radius: 4px;
}

.close-webphone-btn {
  position: absolute;
  top: 10px;
  right: 16px;
  background: transparent;
  border: none;
  font-size: 2rem;
  color: #888;
  cursor: pointer;
  z-index: 10;
  transition: color 0.2s;
}

.close-webphone-btn:hover {
  color: #e53935;
}
				
			

frontend\src\components\AsteriskWebphone\AsteriskWebphone.jsx

				
					import React, { useState, useEffect, useRef, useCallback } from 'react';
import { UserAgent, Registerer, Inviter, SessionState } from 'sip.js';
import dtmf_0 from '../../assets/dtmf_0.mp3';
import dtmf_1 from '../../assets/dtmf_1.mp3';
import dtmf_2 from '../../assets/dtmf_2.mp3';
import dtmf_3 from '../../assets/dtmf_3.mp3';
import dtmf_4 from '../../assets/dtmf_4.mp3';
import dtmf_5 from '../../assets/dtmf_5.mp3';
import dtmf_6 from '../../assets/dtmf_6.mp3';
import dtmf_7 from '../../assets/dtmf_7.mp3';
import dtmf_8 from '../../assets/dtmf_8.mp3';
import dtmf_9 from '../../assets/dtmf_9.mp3';
import sound from '../../assets/dtmf_0.mp3';
import sound_loud from '../../assets/dtmf_0.mp3';
import calling from '../../assets/calling.mp3';
import './AsteriskWebphone.css';

const AsteriskWebphone = ({ isOpen, onClose }) => {
  const [phoneNumber, setPhoneNumber] = useState('');
  const [isConnected, setIsConnected] = useState(false);
  const [isCalling, setIsCalling] = useState(false);
  const [isCallingInProgress, setIsCallingInProgress] = useState(false);
  const [callStatus, setCallStatus] = useState('');
  const [callDuration, setCallDuration] = useState(null);
  const [startTime, setStartTime] = useState(null);

  const userAgentRef = useRef(null);
  const registererRef = useRef(null);
  const sessionRef = useRef(null);
  const remoteAudioRef = useRef(null);
  const ringToneRef = useRef(null);
  const callingToneRef = useRef(null);
  const dtmfRefs = useRef({});
  const timerRef = useRef(null);

  const keypadRows = [
    ['1', '2', '3'],
    ['4', '5', '6'],
    ['7', '8', '9']
  ];

  const dtmfSounds = {
    '1': dtmf_1,
    '2': dtmf_2,
    '3': dtmf_3,
    '4': dtmf_4,
    '5': dtmf_5,
    '6': dtmf_6,
    '7': dtmf_7,
    '8': dtmf_8,
    '9': dtmf_9,
    '0': dtmf_0,
    '*': sound,
    '#': sound_loud
  };

  const playRingTone = useCallback(() => {
    if (ringToneRef.current) {
      try {
        ringToneRef.current.currentTime = 0;
        ringToneRef.current.volume = 1.0;
        ringToneRef.current.loop = true;
        ringToneRef.current.play().catch(error => {
          console.error('Erro ao tocar som de toque:', error);
        });
      } catch (error) {
        console.error('Erro ao tocar som de toque:', error);
      }
    }
  }, []);

  const stopRingTone = useCallback(() => {
    if (ringToneRef.current) {
      try {
        ringToneRef.current.pause();
        ringToneRef.current.currentTime = 0;
      } catch (error) {
        console.error('Erro ao parar som de toque:', error);
      }
    }
  }, []);

  const stopCallTimer = useCallback(() => {
    if (timerRef.current) {
      clearInterval(timerRef.current);
      timerRef.current = null;
    }
  }, []);

  const startCallTimer = useCallback(() => {
    const start = Date.now();
    setStartTime(start);
    timerRef.current = setInterval(() => {
      const duration = Math.floor((Date.now() - start) / 1000);
      const minutes = Math.floor(duration / 60);
      const seconds = duration % 60;
      setCallDuration(`${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`);
    }, 1000);
  }, []);

  const setupSession = useCallback((session) => {
    session.stateChange.addListener((state) => {
      if (state === SessionState.Established) {
        const pc = session.sessionDescriptionHandler.peerConnection;
        pc.getReceivers().forEach(receiver => {
          if (receiver.track && receiver.track.kind === "audio") {
            const stream = new MediaStream([receiver.track]);
            remoteAudioRef.current.srcObject = stream;
            remoteAudioRef.current.play();
          }
        });
        setCallStatus('Em chamada');
        setIsCallingInProgress(false);
        stopRingTone();
        startCallTimer();
      }
      if (state === SessionState.Terminated) {
        setIsCalling(false);
        setCallStatus('Chamada finalizada');
        setIsCallingInProgress(false);
        stopRingTone();
        stopCallTimer();
        setCallDuration(null);
        setStartTime(null);
        if (remoteAudioRef.current) {
          remoteAudioRef.current.srcObject = null;
        }
      }
    });
  }, [stopRingTone, startCallTimer, stopCallTimer]);

  const startSip = useCallback(async () => {
    const config = {
      username: '1000',
      password: 'senha',
      server: 'astersip.astertelecom.com.br',
      port: '8089',
      transport: 'wss'
    };

    userAgentRef.current = new UserAgent({
      uri: UserAgent.makeURI(`sip:${config.username}@${config.server}`),
      transportOptions: {
        server: `wss://${config.server}:${config.port}/ws`
      },
      authorizationUsername: config.username,
      authorizationPassword: config.password,
      logLevel: "error"
    });

    userAgentRef.current.delegate = {
      onInvite: (invitation) => {
        sessionRef.current = invitation;
        setupSession(invitation);
        setIsCalling(true);
        setCallStatus('Chamada recebida');
        setPhoneNumber(invitation.remoteIdentity.uri.user);
        playRingTone();
      }
    };

    await userAgentRef.current.start();
    registererRef.current = new Registerer(userAgentRef.current);
    await registererRef.current.register();
    setIsConnected(true);
  }, [playRingTone, setupSession]);

  const makeCall = async () => {
    if (!phoneNumber || isCallingInProgress) return;

    try {
      setIsCallingInProgress(true);
      const config = {
        username: '1000',
        server: 'astersip.astertelecom.com.br'
      };
      const target = UserAgent.makeURI(`sip:${phoneNumber}@${config.server}`);
      const inviter = new Inviter(userAgentRef.current, target, {
        sessionDescriptionHandlerOptions: { constraints: { audio: true, video: false } }
      });
      sessionRef.current = inviter;
      setupSession(inviter);
      await inviter.invite();
      setIsCalling(true);
      setCallStatus('Chamando...');
    } catch (error) {
      console.error('Erro ao iniciar chamada:', error);
    } finally {
      setIsCallingInProgress(false);
    }
  };

  const acceptCall = async () => {
    if (!sessionRef.current) return;

    try {
      setIsCallingInProgress(true);
      await sessionRef.current.accept();
      setCallStatus('Conectado');
    } catch (error) {
      console.error('Erro ao aceitar chamada:', error);
    } finally {
      setIsCallingInProgress(false);
    }
  };

  const hangupCall = async () => {
    if (!sessionRef.current) return;

    try {
      if (sessionRef.current.state === SessionState.Established) {
        await sessionRef.current.bye();
      } else if (sessionRef.current.state === SessionState.Initial) {
        await sessionRef.current.reject();
      } else if (sessionRef.current.state === SessionState.Establishing) {
        if (sessionRef.current instanceof Inviter) {
          await sessionRef.current.cancel();
        } else {
          await sessionRef.current.bye();
        }
      } else {
        await sessionRef.current.bye();
      }
    } catch (error) {
      console.error('Erro ao encerrar chamada:', error);
    } finally {
      setIsCalling(false);
      setCallStatus('Chamada finalizada');
      setIsCallingInProgress(false);
      stopRingTone();
      if (remoteAudioRef.current) {
        remoteAudioRef.current.srcObject = null;
      }
      sessionRef.current = null;
    }
  };

  const playDialTone = (number) => {
    let audioRef;
    if (number === '*') {
      audioRef = 'dtmf_star';
    } else if (number === '#') {
      audioRef = 'dtmf_hash';
    } else {
      audioRef = `dtmf_${number}`;
    }
    if (dtmfRefs.current[audioRef]) {
      try {
        dtmfRefs.current[audioRef].currentTime = 0;
        dtmfRefs.current[audioRef].volume = 1.0;
        dtmfRefs.current[audioRef].play().catch(error => {
          console.error(`Erro ao tocar som DTMF ${number}:`, error);
        });
      } catch (error) {
        console.error(`Erro ao tocar som DTMF ${number}:`, error);
      }
    }
  };

  const appendNumber = (number) => {
    setPhoneNumber(prev => prev + number);
    playDialTone(number);
  };

  useEffect(() => {
    if (isOpen) {
      startSip();
    }

    return () => {
      if (userAgentRef.current) {
        try {
          userAgentRef.current.stop();
        } catch (error) {
          console.error('Erro ao parar UserAgent:', error);
        }
      }

      stopRingTone();
      stopCallTimer();

      if (sessionRef.current) {
        try {
          if (sessionRef.current.terminate) {
            sessionRef.current.terminate();
          }
          sessionRef.current = null;
        } catch (error) {
          console.error('Erro ao terminar sessão:', error);
        }
      }

      if (registererRef.current) {
        try {
          registererRef.current.stateChange.removeAllListeners();
          registererRef.current = null;
        } catch (error) {
          console.error('Erro ao limpar registrador:', error);
        }
      }

      setIsConnected(false);
      setIsCalling(false);
      setCallStatus('');
    };
  }, [isOpen, startSip, stopRingTone, stopCallTimer]);

  if (!isOpen) return null;

  return (
    <div className="asterisk-webphone-modal">
      <div className="asterisk-webphone">
        <button className="close-webphone-btn" onClick={onClose} title="Fechar">×</button>
        {isConnected ? (
          <div className="webphone-container">
            <div className="status-indicator">
              <span className="text-positive">Conectado</span>
            </div>

            <div className="phone-input">
              <input
                value={phoneNumber}
                onChange={(e) => setPhoneNumber(e.target.value)}
                type="text"
                placeholder="Digite o número"
                className="number-input"
                disabled={isCalling && callStatus === 'Chamada recebida'}
              />
            </div>

            <div className="keypad">
              {keypadRows.map((row, rowIndex) => (
                <div className="keypad-row" key={rowIndex}>
                  {row.map((number) => (
                    <button
                      key={number}
                      className="keypad-button"
                      onClick={() => appendNumber(number)}
                    >
                      {number}
                    </button>
                  ))}
                </div>
              ))}
              <div className="keypad-row">
                <button className="keypad-button" onClick={() => appendNumber('*')}>*</button>
                <button className="keypad-button" onClick={() => appendNumber('0')}>0</button>
                <button className="keypad-button" onClick={() => appendNumber('#')}>#</button>
              </div>
            </div>

            <div className="call-controls">
              {isCalling && callStatus === 'Chamada recebida' ? (
                <>
                  <button
                    className={`call-button ${isCallingInProgress ? 'disabled' : ''}`}
                    onClick={acceptCall}
                    disabled={isCallingInProgress}
                  >
                    Atender
                  </button>
                  <button
                    className={`hangup-button ${isCallingInProgress ? 'disabled' : ''}`}
                    onClick={hangupCall}
                    disabled={isCallingInProgress}
                  >
                    Recusar
                  </button>
                </>
              ) : (
                <>
                  <button
                    className={`call-button ${!isConnected || isCalling || !phoneNumber || isCallingInProgress ? 'disabled' : ''}`}
                    onClick={makeCall}
                    disabled={!isConnected || isCalling || !phoneNumber || isCallingInProgress}
                  >
                    Ligar
                  </button>
                  <button
                    className={`hangup-button ${!isCalling ? 'disabled' : ''}`}
                    onClick={hangupCall}
                    disabled={!isCalling}
                  >
                    Desligar
                  </button>
                </>
              )}
            </div>

            {isCalling && (
              <div className="call-info">
                <div className="call-status">{callStatus}</div>
                {callDuration && <div className="call-duration">{callDuration}</div>}
              </div>
            )}

            <audio ref={remoteAudioRef} autoPlay />
            <audio ref={ringToneRef} src={calling} preload="auto" />
            <audio ref={callingToneRef} src={calling} preload="auto" />

            {Object.entries(dtmfSounds).map(([key, src]) => (
              <audio
                key={key}
                ref={(el) => (dtmfRefs.current[`dtmf_${key}`] = el)}
                src={src}
                preload="auto"
              />
            ))}
          </div>
        ) : (
          <div className="disconnected-message">
            <span>Desconectado</span>
          </div>
        )}
      </div>
    </div>
  );
};

export default AsteriskWebphone;
				
			
Integre um Webphone SIP (Asterisk) no seu Whaticket: Tutorial Completo de Frontend

Integre um Webphone SIP (Asterisk) no seu Whaticket: Tutorial Completo de Frontend

Quer transformar seu Whaticket em uma central telefônica completa, permitindo que seus operadores façam e recebam ligações diretamente da interface?

Neste tutorial, vamos direto ao ponto e mostrar como integrar um webphone SIP funcional, compatível com Asterisk, FreeSwitch ou qualquer outra solução de telefonia que utilize o protocolo SIP.

O melhor de tudo? Toda a implementação é feita no frontend do seu Whaticket, com dois componentes simples que você pode copiar e colar. Em poucos minutos, você terá um botão de discagem flutuante pronto para ser usado.

Bora pra ação!

O Que Você Vai Precisar (Pré-requisitos)

Antes de começarmos, garanta que seu ambiente está preparado:

  • Whaticket Instalado: Você precisa ter sua instância do Whaticket rodando e com acesso ao código-fonte.
  • Servidor SIP Ativo: Um servidor Asterisk, FreeSwitch ou similar já configurado.
  • Credenciais do Ramal: O número do ramal, a senha e o endereço do servidor SIP que será usado.
  • Ponto Crucial: Seu servidor SIP precisa ter suporte a WebRTC e ao protocolo WSS (WebSocket Secure) ativados. Geralmente, a porta padrão para WSS é a 8089.

Passo 1: Criando os Componentes no Frontend

Primeiro, vamos adicionar os arquivos do webphone ao seu projeto.

  1. Navegue até a pasta do seu projeto Whaticket e entre no diretório do frontend: whaticket/frontend/.
  2. Dentro da pasta src/components/, crie um novo diretório chamado asterisk-webfone.
  3. Dentro desta nova pasta, adicione os dois arquivos que você baixou:
    • Webphone.js: Contém toda a lógica do componente, a interface e a conexão com o servidor SIP.
    • Webphone.css: A folha de estilos responsável por deixar o teclado e a interface com um visual profissional.

Passo 2: Instalando a Biblioteca sip.js

Nosso webphone utiliza a biblioteca sip.js para gerenciar a comunicação com o servidor. Vamos instalá-la no frontend.

  1. Abra um terminal e navegue até a pasta frontend do seu projeto.
  2. Execute o seguinte comando para instalar a dependência:
    npm install sip.js --force

    Observação: O uso do parâmetro --force ou --legacy-peer-deps pode ser necessário para resolver conflitos de dependência com as versões de pacotes usadas no Whaticket.

Passo 3: Configurando a Conexão SIP no Componente

Agora vem a parte mais importante: configurar seu ramal dentro do código.

  1. Abra o arquivo Webphone.js que você adicionou no Passo 1.
  2. Procure pela seção de configuração do SIP. Você encontrará um objeto onde precisa preencher suas credenciais:
    //...dentro do componente Webphone.js
        
    const sipConfig = {
      userName: "770", // <- SEU RAMAL AQUI
      password: "SUA_SENHA_AQUI", // <- SUA SENHA AQUI
      server: "SEU_IP_OU_DOMINIO_SIP", // <- SEU SERVIDOR AQUI
      port: 8089, // Porta WSS (padrão 8089)
      transporter: "WSS" // Protocolo WebSocket Secure
    };
  3. Altere os campos userName, password e server com os dados do seu ramal SIP. Salve o arquivo.

Passo 4: Adicionando o Botão de Acesso (Dialpad)

Com o componente criado e configurado, só falta adicionar o botão flutuante que irá abri-lo.

  1. No seu projeto, abra o arquivo frontend/src/App.js.
  2. Importe o componente do webphone no início do arquivo:
    import AsteriskWebphone from './components/asterisk-webfone/Webphone';
  3. Dentro da função App, adicione o componente para que ele seja renderizado na tela. Você pode adicioná-lo, por exemplo, antes da tag de fechamento ou em outro local estratégico.
    // ... dentro do App.js
    return (
        
    {/* ... resto do código do App.js */}
  4. Salve o arquivo. Se seu ambiente de desenvolvimento estiver rodando, o Whaticket será atualizado automaticamente e um ícone de telefone aparecerá no canto da tela, pronto para ser usado.

Dica PRO: Tornando a Configuração Dinâmica

As configurações do ramal estão fixas no código, mas você pode evoluir essa solução! Para criar um sistema realmente profissional, você pode:

  • No Backend: Alterar o Model de User para incluir os campos sipUserName, sipPassword e sipServer.
  • No Frontend: Criar uma tela de configuração no perfil do usuário para que cada um possa inserir seus próprios dados de ramal.
  • No Componente: Fazer o componente Webphone.js buscar essas informações do usuário logado, em vez de usar valores fixos.

Por que ir Além? A Visão Omnichannel e a Solução Z-PRO

Integrar um webphone SIP é um passo incrível para diferenciar seu serviço. O mercado de sistemas de multiatendimento para WhatsApp está cada vez mais concorrido. Como um de nossos revendedores nos contou, em uma feira de inovação do Sebrae, havia 12 estandes oferecendo soluções de atendimento — 11 delas eram Whaticket padrão. Apenas ele oferecia um sistema completo e diferenciado: o Z-PRO.

O Whaticket é excelente, mas o futuro está nas plataformas Omnichannel. Seus clientes precisam integrar não apenas WhatsApp, mas também:

  • Ligações SIP (como esta que você acabou de implementar)
  • Instagram e Facebook Messenger
  • SMS
  • Webchat para sites
  • APIs Oficiais e Não Oficiais
  • Um CRM completo e integrado

Nossa plataforma Z-PRO já vem com tudo isso embarcado, pronta para você revender no formato SaaS e se destacar da concorrência.

Se quiser conhecer mais sobre a plataforma que já nasceu completa, acesse o link na página de apoio.

Finalizando

Pronto! Seguindo esses passos, você integrou um webphone funcional ao seu Whaticket, abrindo um novo leque de possibilidades para seu sistema de atendimento.

Quer aprender mais sobre APIs, automações e como construir soluções robustas? Venha para a Comunidade ZDG, a maior comunidade de APIs e automações para WhatsApp do mundo, com mais de 7 mil alunos.

Qualquer dúvida é só chamar. Tamo junto!

Leve seu negócio para o próximo nível

Este tutorial é apenas a ponta do iceberg. A ZDG é uma empresa com mais de 5 anos de mercado que já ajudou mais de 7.000 assinantes com a criação de soluções de automação de atendimentos e chatbots para WhatsApp com o melhor custo-benefício do mercado

Se você que quer começar do zero e aprender por conta própria:

Conheça a Comunidade ZDG e aprenda a automatizar seus atendimentos no WhatsApp com ferramentas open-source gratuitas. Tenha acesso a cursos essenciais de integrações, infraestrutura e aos nossos exclusivos auto-instaladores de sistemas como Whaticket, Chatwoot, Typebot e N8N.

Se você que busca um sistema avançado completo:

Conheça o Sistema Z-PRO, uma plataforma de multi-atendimento completa com flowbuider nativo para criação de chatbots, integração com inteligência artificial e modo White Label para revenda no modelo SAAS. Unifique WhatsApp (oficial e não oficial), Instagram, Facebook, Telegram e até ligações PABX em um único lugar.