npm package: @growhats/webphone
Description: Webphone widget para chamadas VoIP WhatsApp via GroWHats — drop-in replacement para @wavoip/wavoip-webphone
Author: GroWHats
Latest version: 1.0.0

# @growhats/webphone

Biblioteca para realizar chamadas VoIP WhatsApp via GroWHats. Disponibiliza uma interface customizavel e isolada do projeto onde esta instalada, com gravacao full-duplex automatica e persistencia contra fechamento de pagina.

Drop-in replacement para [@wavoip/wavoip-webphone](https://www.npmjs.com/package/@wavoip/wavoip-webphone) — API 100% compativel.

## Instalacao

```bash
# pnpm
pnpm add @growhats/webphone

# npm
npm install @growhats/webphone

# yarn
yarn add @growhats/webphone
```

Via CDN:

```html
<script src="https://cdn.jsdelivr.net/npm/@growhats/webphone@latest/dist/growhats-webphone.umd.js"></script>
```

## Getting Started

**Pacote instalado:**

```typescript
import { GrowhatsWebphone } from "@growhats/webphone";

const api = await GrowhatsWebphone.render({
  serverUrl: "https://sua-api.com",
  accountId: 1,
  inboxId: 81,
  apiKey: "sua-api-key",
});

GrowhatsWebphone.destroy(); // remover widget
```

**Via CDN:**

```typescript
const api = await GrowhatsWebphone.render({
  serverUrl: "https://sua-api.com",
  accountId: 1,
  inboxId: 81,
  apiKey: "sua-api-key",
});

// Tambem acessivel via window
window.growhatsWebphone.render(config);
window.wavoip.call.start("5511999999999"); // compatibilidade Wavoip
```

> **Importante:** Sempre use `await` ao chamar `render()` para evitar comportamento inesperado.

## WebphoneAPI

`render()` retorna uma `Promise<WebphoneAPI>` e tambem expoe `window.wavoip` e `window.growhatsWebphone`:

```typescript
type WebphoneAPI = {
  call: CallAPI;
  device: DeviceAPI;
  notifications: NotificationsAPI;
  widget: WidgetAPI;
  theme: ThemeAPI;
  position: PositionAPI;
  settings: SettingsAPI;
  recording: RecordingAPI;
  on<K extends keyof WebphoneEvents>(event: K, cb: WebphoneEvents[K]): void;
  off<K extends keyof WebphoneEvents>(event: K, cb: WebphoneEvents[K]): void;
  destroy(): void;
};
```

---

## CallAPI

```typescript
type CallAPI = {
  start(
    to: string,
    config?: { fromTokens?: string[]; displayName?: string }
  ): Promise<CallOutgoing>;

  startCall(to: string, fromTokens?: string[]): Promise<CallOutgoing>; // Deprecated

  getCallActive(): CallActive | undefined;

  getCallOutgoing(): CallOutgoing | undefined;

  getOffers(): CallOffer[];

  setInput(to: string): void;

  onOffer(cb: (offer: CallOffer) => void): void;
};
```

### CallOffer

Representa uma chamada recebida aguardando acao:

```typescript
type CallOffer = {
  call_id: string;
  from: string;
  displayName?: string;

  accept(): Promise<CallActive>;
  reject(): Promise<{ err: string | null }>;

  onAcceptedElsewhere(cb: () => void): void;
  onRejectedElsewhere(cb: () => void): void;
  onUnanswered(cb: () => void): void;
  onEnd(cb: () => void): void;
  onStatus(cb: (status: CallStatus) => void): void;
};
```

### CallActive

Chamada em andamento (atendida):

```typescript
type CallActive = {
  id: string;
  type: "official" | "unofficial";
  device_token: string;
  direction: "INCOMING" | "OUTGOING";
  status: CallStatus;
  connection_status: "disconnected" | "connected" | "connecting";
  muted: boolean;
  peer: {
    phone: string;
    displayName: string | null;
    profilePicture: string | null;
    muted: boolean;
  };

  mute(): Promise<{ err: string | null }>;
  unmute(): Promise<{ err: string | null }>;
  end(): Promise<{ err: string | null }>;

  onError(cb: (err: string) => void): void;
  onPeerMute(cb: () => void): void;
  onPeerUnmute(cb: () => void): void;
  onEnd(cb: () => void): void;
  onStats(cb: (stats: CallStats) => void): void;
  onConnectionStatus(cb: (status: string) => void): void;
  onStatus(cb: (status: CallStatus) => void): void;
};
```

### CallOutgoing

Chamada de saida (aguardando atendimento):

```typescript
type CallOutgoing = {
  id: string;
  type: "official" | "unofficial";
  device_token: string;
  direction: "OUTGOING";
  status: CallStatus;
  muted: boolean;
  peer: {
    phone: string;
    displayName: string | null;
    profilePicture: string | null;
    muted: boolean;
  };

  mute(): Promise<{ err: string | null }>;
  unmute(): Promise<{ err: string | null }>;
  end(): Promise<{ err: string | null }>;

  onPeerAccept(cb: () => void): void;
  onPeerReject(cb: () => void): void;
  onUnanswered(cb: () => void): void;
  onEnd(cb: () => void): void;
  onStatus(cb: (status: CallStatus) => void): void;
};
```

### CallStatus

```typescript
type CallStatus =
  | "RINGING"
  | "CALLING"
  | "NOT_ANSWERED"
  | "ACTIVE"
  | "ENDED"
  | "REJECTED"
  | "FAILED"
  | "DISCONNECTED"
  | "DEVICE_RESTARTING";
```

### CallStats

```typescript
type CallStats = {
  rtt: {
    client: { min: number; max: number; avg: number };
    whatsapp: { min: number; max: number; avg: number };
  };
  tx: { total: number; total_bytes: number; loss: number };
  rx: { total: number; total_bytes: number; loss: number };
};
```

---

## DeviceAPI

```typescript
type DeviceAPI = {
  get(): Device[];
  getDevices(): Device[];                               // Deprecated

  add(token: string, persist?: boolean): void;
  addDevice(token: string, persist?: boolean): void;    // Deprecated

  remove(token: string): void;
  removeDevice(token: string): void;                    // Deprecated

  enable(token: string): void;
  enableDevice(token: string): void;                    // Deprecated

  disable(token: string): void;
  disableDevice(token: string): void;                   // Deprecated
};
```

`persist: true` salva o dispositivo no Local Storage do navegador entre sessoes. `disable()` encerra a conexao do webphone com o dispositivo enquanto o dispositivo continua rodando.

### Device

```typescript
type Device = {
  token: string;
  status: DeviceStatus;
  qrcode?: string;
  contact?: { phone?: string; name?: string };

  onStatus(cb: (status: DeviceStatus) => void): void;
  onQRCode(cb: (qr: string) => void): void;
  onContact(cb: (contact: { phone?: string; name?: string }) => void): void;
};

type DeviceStatus =
  | "disconnected"
  | "close"
  | "connecting"
  | "open"
  | "restarting"
  | "hibernating"
  | "BUILDING"
  | "EXTERNAL_INTEGRATION_ERROR";
```

---

## NotificationsAPI

```typescript
type NotificationsAPI = {
  get(): Notification[];
  getNotifications(): Notification[];                               // Deprecated

  add(notification: Partial<Notification>): void;
  addNotification(notification: Partial<Notification>): void;       // Deprecated

  remove(id: Date): void;
  removeNotification(id: Date): void;                               // Deprecated

  clear(): void;
  clearNotifications(): void;                                       // Deprecated

  read(): void;
  readNotifications(): void;                                        // Deprecated
};

type Notification = {
  id: Date;
  type: "INFO" | "CALL_FAILED";
  message: string;
  detail?: string;
  token?: string;
  isRead: boolean;
  isHidden: boolean;
  created_at: Date;
};
```

---

## RecordingAPI

Gravacao automatica full-duplex (ambos os lados da conversa). Quando habilitada via config, grava automaticamente do momento que a chamada e atendida ate quando e encerrada. Suporta multiplas chamadas simultaneas isoladas.

Se o usuario fechar a pagina (F5 / fechar aba), a gravacao e salva automaticamente via `navigator.sendBeacon`.

```typescript
type RecordingAPI = {
  /** Se a gravacao esta habilitada via config */
  enabled: boolean;

  /** Verifica se uma chamada especifica esta sendo gravada */
  isRecording(callId?: string): boolean;

  /** Retorna IDs de todas as chamadas sendo gravadas */
  getActiveRecordings(): string[];

  /** Busca a URL de uma gravacao finalizada no servidor */
  getUrl(callId: string): Promise<string | null>;
};

type RecordingResult = {
  url: string;
  callId: string;
  durationSeconds: number;
  startedAt: string;
  endedAt: string;
};
```

As gravacoes sao armazenadas no servidor organizadas por empresa e data:

```
call-recordings/{accountId}/{YYYY}/{MM}/{DD}/{callId}.webm
```

---

## WidgetAPI

```typescript
type WidgetAPI = {
  isOpen: boolean;

  open(): void;

  close(): void;

  toggle(): void;

  buttonPosition: {
    value: { x: number; y: number };
    set(
      position:
        | "top-left"
        | "top-right"
        | "bottom-left"
        | "bottom-right"
        | "center"
        | { x: number; y: number }
    ): void;
  };
};
```

---

## ThemeAPI

```typescript
type ThemeAPI = {
  value: "light" | "dark" | "system";
  set(theme: "light" | "dark" | "system"): void;
  setTheme(theme: "light" | "dark" | "system"): void;  // Deprecated
};
```

---

## PositionAPI

```typescript
type PositionAPI = {
  value: { x: number; y: number };
  set(
    position:
      | "top-left"
      | "top-right"
      | "bottom-left"
      | "bottom-right"
      | "center"
      | { x: number; y: number }
  ): void;
};
```

---

## Configuracao Inicial (WebphoneConfig)

```typescript
GrowhatsWebphone.render(config?: WebphoneConfig);

type WebphoneConfig = {
  /** URL base do servidor (obrigatorio) */
  serverUrl: string;

  /** ID da conta/empresa (obrigatorio) */
  accountId: number;

  /** ID do inbox WhatsApp para chamadas (obrigatorio) */
  inboxId: number;

  /** Chave de autenticacao (enviada como X-Api-Key) */
  apiKey?: string;

  /** Gravacao automatica de chamadas */
  recording?: {
    /** Habilitar gravacao automatica full-duplex (default: false) */
    enabled: boolean;
  };

  /** Tema: "light" | "dark" | "system" (default: "dark") */
  theme?: "light" | "dark" | "system";

  /** Posicao do botao flutuante (default: "bottom-right") */
  position?:
    | "top-left"
    | "top-right"
    | "bottom-left"
    | "bottom-right"
    | "center"
    | { x: number; y: number };

  /** Exibir botao flutuante do widget (default: true) */
  showWidgetButton?: boolean;

  /** Iniciar com o painel aberto (default: false) */
  startOpen?: boolean;

  /** Reconectar automaticamente em caso de queda (default: true) */
  autoReconnect?: boolean;

  /** Elemento container para montar o widget (default: document.body) */
  container?: HTMLElement;

  /** Configuracao da barra de status */
  statusBar?: {
    showNotificationsIcon?: boolean;
    showSettingsIcon?: boolean;
  };

  /** Configuracao do menu de configuracoes */
  settingsMenu?: {
    showDevices?: boolean;
    showAddDevices?: boolean;
    showEnableDevices?: boolean;
    showRemoveDevices?: boolean;
  };
};
```

---

## SettingsAPI (Runtime)

Apos o `render()`, controle as configuracoes em tempo real via `api.settings`:

```typescript
type SettingsAPI = {
  showNotifications: boolean;
  setShowNotifications(show: boolean): void;

  showSettings: boolean;
  setShowSettings(show: boolean): void;

  showDevices: boolean;
  setShowDevices(show: boolean): void;

  showAddDevices: boolean;
  setShowAddDevices(show: boolean): void;

  showEnableDevices: boolean;
  setShowEnableDevices(show: boolean): void;

  showRemoveDevices: boolean;
  setShowRemoveDevices(show: boolean): void;

  showWidgetButton: boolean;
  setShowWidgetButton(show: boolean): void;
};
```

---

## Eventos

```typescript
type WebphoneEvents = {
  /** Chamada recebida */
  "call:incoming": (offer: CallOffer) => void;

  /** Chamada iniciada (atendida ou discando) */
  "call:started": (call: CallActive) => void;

  /** Chamada encerrada */
  "call:ended": (callId: string, reason: string) => void;

  /** Status da chamada alterado */
  "call:status": (callId: string, status: CallStatus) => void;

  /** Status da conexao com servidor */
  "connection:status": (connected: boolean) => void;

  /** Gravacao iniciada automaticamente */
  "recording:started": (callId: string) => void;

  /** Gravacao finalizada e salva no servidor */
  "recording:stopped": (callId: string, result: RecordingResult | null) => void;

  /** Erro */
  error: (error: Error) => void;
};
```

### Exemplo completo:

```typescript
const api = await GrowhatsWebphone.render({
  serverUrl: "https://sua-api.com",
  accountId: 1,
  inboxId: 81,
  recording: { enabled: true },
});

api.on("connection:status", (connected) => {
  console.log("Conexao:", connected ? "online" : "offline");
});

api.on("call:incoming", (offer) => {
  console.log("Chamada recebida de:", offer.from);
  offer.accept(); // ou offer.reject()
});

api.on("call:started", (call) => {
  console.log("Em chamada:", call.peer.phone, call.direction);
});

api.on("call:ended", (callId, reason) => {
  console.log("Chamada encerrada:", callId, reason);
});

api.on("recording:started", (callId) => {
  console.log("Gravando chamada:", callId);
});

api.on("recording:stopped", (callId, result) => {
  if (result) {
    console.log("Gravacao salva:", result.url, result.durationSeconds + "s");
  }
});

api.on("error", (err) => {
  console.error("Erro:", err.message);
});
```

---

## Compatibilidade com Wavoip

A API e 100% compativel com `@wavoip/wavoip-webphone`. O global `window.wavoip` e exposto automaticamente apos `render()`:

```typescript
// Todos os metodos da Wavoip funcionam:
window.wavoip.call.startCall("5511999999999", [tokenDispositivo]);
window.wavoip.device.addDevice(token);
window.wavoip.device.enableDevice(token);
window.wavoip.device.disableDevice(token);
window.wavoip.device.removeDevice(token);
window.wavoip.notifications.getNotifications();
window.wavoip.notifications.addNotification({ type: "INFO", message: "Teste" });
window.wavoip.settings.setShowWidgetButton(false);
window.wavoip.settings.setShowNotifications(false);
window.wavoip.settings.setShowSettings(false);
window.wavoip.theme.setTheme("dark");
window.wavoip.widget.toggle();
```

### Guia de migracao

Para migrar de `@wavoip/wavoip-webphone` para `@growhats/webphone`:

1. Substitua o import/script:

```diff
- import WavoipWebphone from "@wavoip/wavoip-webphone"
+ import { GrowhatsWebphone } from "@growhats/webphone"
```

```diff
- <script src="https://cdn.jsdelivr.net/npm/@wavoip/wavoip-webphone/dist/index.umd.min.js"></script>
+ <script src="https://cdn.jsdelivr.net/npm/@growhats/webphone/dist/growhats-webphone.umd.js"></script>
```

2. Adicione os parametros obrigatorios no `render()`:

```diff
- WavoipWebphone.render()
+ GrowhatsWebphone.render({
+   serverUrl: "https://sua-api.com",
+   accountId: 1,
+   inboxId: 81,
+ })
```

3. Todo o restante (`window.wavoip.*`) continua funcionando sem alteracao.

## Licenca

MIT