BCVOZ @bcvoz/webphone

Webphone SDK

O softphone BCVOZ como uma janela flutuante em qualquer página web. Uma linha de <script>, sem dependências, 30 kB.

versão 0.7.1 · apenas navegador

Instalação

Cole este trecho inline na página. Ele resolve a versão publicada e carrega o bundle dela:

<script>
fetch('https://data.jsdelivr.com/v1/packages/npm/@bcvoz/webphone/resolved')
  .then((r) => r.json())
  .then(({ version }) => {
    const s = document.createElement('script')
    s.src = `https://cdn.jsdelivr.net/npm/@bcvoz/webphone@${version}/dist/bcvoz.umd.js`
    s.onload  = () => BCVoz.init({ token: TOKEN_DO_USUARIO, mode: 'srcdoc' })
    s.onerror = () => console.error('BCVoz: falha ao carregar do CDN')
    document.head.appendChild(s)
  })
</script>

Não use a URL sem versão.

/npm/@bcvoz/webphone parece mais simples, mas o CDN a entrega com max-age=604800sete dias de cache no navegador do usuário. Uma correção publicada não alcança quem já carregou, e purgar o CDN não ajuda: o cache está na máquina dele.

Resolver a versão primeiro custa uma requisição de ~1 kB (com 5 min de cache) e faz o bundle vir de uma URL immutable — atualização rápida e cache perfeito ao mesmo tempo.

Para travar numa versão — integração de terceiros, política de build ou SRI:

<script src="https://cdn.jsdelivr.net/npm/@bcvoz/webphone@0.7.1/dist/bcvoz.umd.js"></script>

Por npm:

npm install @bcvoz/webphone
import BCVoz from '@bcvoz/webphone'   // default export

O build UMD expõe window.BCVoz. Não use BCVoz.default.

init(options)

Chame uma vez, com o <body> já existindo. É idempotente: chamar de novo devolve a mesma instância e ignora as novas opções. Para trocar de configuração, use destroy() antes.

OpçãoTipoPadrãoDescrição
tokenstringSessão do usuário, emitida pelo seu backend
mode'hosted' | 'srcdoc''hosted'Como o webphone é carregado
hostBasestringCDN da versãoSó em srcdoc. Deve terminar em /
hostUrlstringdomínio oficialSó em hosted
frame'bar' | 'none''bar''none' preserva 100% da UI do webphone
position'bottom-right' | …'bottom-right'Canto em que a janela nasce
openbooleanfalsefalse começa recolhido na aba
launcherbooleantrueExibir a aba lateral
launcherSide'right' | 'left''right'Borda em que a aba cola
launcherIconstring'phone-waves'waveform, headset, chat-phone
dockTop'max' | 'top-half''max'O que arrastar até o topo faz
titlestring'BCVOZ'Só com frame: 'bar'

O token nunca é literal no HTML.

Ele identifica um usuário. Gere no seu backend e injete no template, como faria com qualquer credencial de sessão.

Modos de carga

O webphone roda dentro de um iframe. O que muda entre os modos é de onde esse iframe vem — e a consequência é bem concreta.

srcdochosted
Origem do iframea do seu sitedomínio do BCVoz
De onde vêm os arquivosCDN, travados na versãodo host
Iframe de terceironãosim — sujeito a bloqueadores
Login entre domíniosstorage próprioparticionado por site
CORS dos backendssua origem precisa estar liberadaorigem fixa, nada a fazer

Como escolher.

Use 'srcdoc' se a origem do seu site já estiver na allowlist de CORS do BCVoz. Se não estiver, o webphone aparece na tela e não registra — o navegador descarta as respostas. Nesse caso use 'hosted' até a liberação.

Telefonia

Todos devolvem Promise. async

MétodoRetornoNotas
call(number, meta?){ok, phone}Número em formato livre
answer(){ok}Atende a chamada entrante
hangup(){ok}Encerra a chamada atual
mute(){ok}Alterna — não aceita argumento
hold(){ok}Alterna — não aceita argumento
sendDTMF(tone){ok}Um caractere: 0-9, *, #
transfer(to){ok}Ramal ou número de destino
getStatus()PhoneStatusEstado atual da linha
setAuth(token){ok}Troca a sessão sem recarregar
logout(){ok}Encerra a sessão

call(number, meta?)

O número aceita formato livre — 11987654321, (11) 98765-4321, com ou sem DDI. A normalização é a mesma da extensão de navegador.

await BCVoz.call('11987654321', {
  name:    'Ana Ribeiro',      // aparece no card da chamada
  crm:     'Acme Ltda',        // rótulo da origem
  photo:   'https://…/foto.jpg', // avatar
  gateway: 'meu-sistema',      // identifica a origem nos relatórios
  dealId:  '4821',
})

Trate a rejeição.

call() rejeita com número inválido ou webphone ainda carregando. Sem .catch(), o clique do usuário falha em silêncio e ninguém descobre por quê.

PhoneStatus devolve { ready, inCall, phase, number, incoming, muted, held }.

Janela

MétodoDescrição
show() / hide() / toggle()Exibe, recolhe, alterna
reveal()Abre; se já aberta, traz para a vista e destaca
minimize(force?)Colapsa para a barra de título
move(x, y)Posiciona em coordenadas da viewport
resize(w, h)Redimensiona, respeitando os limites
dock(zone)Encaixa numa região — ver abaixo
destroy()Remove o widget e desfaz os listeners

Zonas de dock(): 'left', 'right', 'left-half', 'right-half', 'top-half', 'bottom-half', 'top', 'bottom', 'max', 'float'.

O usuário também encaixa arrastando: as laterais dão altura cheia, a borda de baixo dá a metade inferior, o topo maximiza e os cantos dão meia tela. Uma prévia mostra o resultado antes de soltar. Redimensionando, chegar perto de uma borda completa até ela.

Aba lateral

Com a janela recolhida, o webphone fica acessível por uma aba colada na lateral — arrastável na vertical, com a posição preservada entre sessões. O ícone reflete o estado da linha e pulsa em vermelho numa chamada entrante.

MétodoDescrição
setLauncherSide('right' | 'left')Troca a borda em runtime
setLauncherIcon(nome)phone-waves, waveform, headset, chat-phone

Eventos

const off = BCVoz.on('call:incoming', ({ number }) => {
  // abrir a ficha do cliente, por exemplo
})

off()   // remove o listener — útil ao desmontar um componente
EventoPayloadQuando
ready{ version }A ponte conectou; aceita comandos
state{ state }connecting, ready, error
call:dialingCallInfoChamada de saída iniciada
call:incomingCallInfoEntrante — a janela se abre sozinha
call:answeredCallInfoAtendida
call:endedCallInfoEncerrada
resize{ width, height, dock }Redimensionada ou encaixada
open / closeJanela exibida ou recolhida
revealreveal() foi chamado
error{ message }Falha na ponte ou no carregamento

CallInfo: { id, number, direction }, com direction em 'inbound' ou 'outbound'.

Não existe call:failed.

Uma chamada que não completa chega como call:ended — o estado interno do webphone não distingue desligar de falhar.

Para depurar, on('*', ({ event, payload }) => …) recebe tudo. Evite deixar em produção.

Propriedades

PropriedadeTipoDescrição
versionstringVersão do SDK carregado
isOpenbooleanA janela está visível
geometryobject | null{x, y, width, height, dock}; null antes do init

Receitas

Click-to-call numa lista

document.querySelectorAll('[data-fone]').forEach((el) => {
  el.addEventListener('click', () => {
    BCVoz.call(el.dataset.fone, {
      name: el.dataset.nome,
      crm:  el.dataset.empresa,
      gateway: 'meu-sistema',
    }).catch((e) => alert('Não foi possível ligar: ' + e.message))
  })
})

Registrar o atendimento ao fim da chamada

BCVoz.on('call:ended', ({ number, direction }) => {
  fetch('/api/atendimentos', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ number, direction }),
  })
})

Carregar por JavaScript

Quando não há como escrever a <script> no HTML — SPA, Tag Manager:

function carregar() {
  if (window.BCVoz) return Promise.resolve(window.BCVoz)
  if (window.__bpCarregando) return window.__bpCarregando
  window.__bpCarregando = new Promise((ok, erro) => {
    const s = document.createElement('script')
    s.src = 'https://cdn.jsdelivr.net/npm/@bcvoz/webphone@0.7'
    s.async = true
    s.onload  = () => window.BCVoz ? ok(window.BCVoz)
      : erro(new Error('carregou mas a API não apareceu'))
    s.onerror = () => erro(new Error('falhou (rede, bloqueador ou CSP)'))
    document.head.appendChild(s)
  })
  return window.__bpCarregando
}

Requisitos da sua página

O microfone é concedido à sua origem.

No modo srcdoc o usuário autoriza o microfone para o seu domínio, não para o BCVoz — uma vez por site. A favor: a permissão fica no domínio que ele já conhece.

Diagnóstico

Quando algo não funciona, siga nesta ordem — cada passo elimina uma camada:

VerificaçãoSe falhar
window.BCVoz existe?O script não carregou: rede, CSP ou bloqueador
O evento ready disparou?A ponte não conectou com o webphone
state veio 'error'?Registro SIP: token inválido, ou origem fora do CORS
A UI aparece sem imagens?Os assets não resolveram — confira o hostBase

Os erros do webphone ficam noutro console.

Ele roda dentro de um iframe. Nas devtools, troque o contexto do console para o do frame — o console da página principal não mostra nada do que acontece lá dentro.