Pular para o conteúdo principal

SDK React

Pacote NPM @paulorbj/wpp-gateway-react (privado/interno) com Provider, hooks e componentes prontos para usar no frontend.

Importante: o SDK React não fala direto com o wpp-gateway. A API key fica no servidor do cliente — o SDK chama um proxy do seu backend que reenvia para wpp.ogmma.com.br. Isso evita expor ak_live_... no browser.


Arquitetura

┌──────────────┐ HTTP ┌──────────────────┐ HTTP ┌─────────────┐
│ React App │─────────►│ Seu Backend │───────►│ wpp-gateway │
│ (SDK React) │ /api/wpp │ (proxy + auth) │ X-Key │ wpp.ogmma │
└──────────────┘ └──────────────────┘ └─────────────┘

Você implementa um proxy em /api/wpp/* no seu backend (Next API route, Express handler, etc) que:

  1. Autentica seu usuário (sessão, cookie, etc).
  2. Mapeia para o accountId correto.
  3. Adiciona X-API-Key: ak_live_... (vindo do seu env).
  4. Faz fetch ao https://wpp.ogmma.com.br/v1/<path> e devolve a resposta.

Instalação

npm install @paulorbj/wpp-gateway-react

Pacote zero-dep além de React (≥18). Componentes usam CSS inline mínimo.


Provider

Wrap a árvore com <WppGatewayProvider> no topo.

import { WppGatewayProvider } from '@paulorbj/wpp-gateway-react';

export default function RootLayout({ children }) {
return (
<WppGatewayProvider baseUrl="/api/wpp">
{children}
</WppGatewayProvider>
);
}

Props

PropTipoObrigatórioDescrição
baseUrlstringsimPrefixo das URLs (relativo ou absoluto). Ex.: /api/wpp
headersobjectnãoHeaders extras (Authorization, X-Clinic-Id, etc)
fetchFnfunctionnãoOverride de fetch (edge runtime, mock, etc)
endpointsobjectnãoOverride de rotas individuais

Default endpoints

{
listPhones: 'GET /phones',
createPhone: 'POST /phones',
getPhone: 'GET /phones/:id',
deletePhone: 'DELETE /phones/:id',
qrcode: 'GET /phones/:id/qrcode',
requestPairingCode: 'POST /phones/:id/pairing-code',
pairingCode: 'GET /phones/:id/pairing-code',
reconnect: 'POST /phones/:id/reconnect',
embeddedSignupConfig: 'GET /waba/embedded-signup/config',
finishEmbeddedSignup: 'POST /waba/embedded-signup',
}

Para sobrescrever (ex.: seu proxy usa /qr-code ao invés de /qrcode):

<WppGatewayProvider
baseUrl="/api/wpp"
endpoints={{ qrcode: 'GET /phones/:id/qr-code' }}
>
...
</WppGatewayProvider>

Hooks

useGatewayClient()

Retorna o GatewayClient para chamadas imperativas.

const client = useGatewayClient();
await client.deletePhone('5f7b2e1c-...');

usePhones()

Lista phones com refresh manual.

import { usePhones } from '@paulorbj/wpp-gateway-react';

function PhonesPage() {
const { phones, loading, error, refresh } = usePhones();
if (loading) return <Spinner />;
if (error) return <Alert>{error}</Alert>;
return (
<>
{phones.map(p => <div key={p.id}>{p.label}{p.status}</div>)}
<button onClick={refresh}>Atualizar</button>
</>
);
}

Opções: { autoload?: boolean } (default true).

useQrCode(phoneId, opts?)

Polling do QR code Baileys. Para automaticamente quando stop=true (geralmente após CONNECTED).

import { useQrCode, useChannelStatus } from '@paulorbj/wpp-gateway-react';

function QrScreen({ phoneId }: { phoneId: string }) {
const { phone } = useChannelStatus(phoneId);
const connected = phone?.status === 'CONNECTED';
const { dataUrl, error } = useQrCode(phoneId, { stop: connected, intervalMs: 2000 });

if (connected) return <p>Conectado!</p>;
if (error) return <p>Erro: {error}</p>;
if (!dataUrl) return <Spinner />;
return <img src={dataUrl} alt="QR Code" width={300} />;
}

Opções: { intervalMs?: number; stop?: boolean }. Default intervalMs=2000. HTTP 425 (QR_NOT_READY) é tratado como "ainda esperando" — não vira erro.

useChannelStatus(phoneId, opts?)

Polling do status do phone. Útil para detectar transições (PENDING_QR → CONNECTED).

const { phone, error } = useChannelStatus(phoneId, { intervalMs: 3000 });

Componentes

<ConnectChannelWizard>

Wizard completo de conexão, com seletor de modo (Baileys QR, Baileys pairing, WABA embedded signup, WABA manual).

import { ConnectChannelWizard } from '@paulorbj/wpp-gateway-react';

<ConnectChannelWizard
modes={['baileys-qr', 'baileys-pairing', 'waba-embedded']}
onConnected={(phone) => router.push(`/phones/${phone.id}`)}
onCancel={() => router.back()}
/>

Props

PropTipoDescrição
modesConnectMode[]Modos habilitados. Default: todos
defaultModeConnectModeModo inicial
onConnected(phone: Phone) => voidCallback após CONNECTED
onCancel() => voidBotão "Voltar"

ConnectMode = 'baileys-qr' | 'baileys-pairing' | 'waba-embedded' | 'waba-manual'

<BaileysQRConnect>

Wizard isolado de QR.

<BaileysQRConnect
label="Atendimento Vendas"
onConnected={(phone) => console.log('conectou:', phone)}
/>

Props

PropTipoDefault
labelstring"WhatsApp"
webhookWebhookConfigundefined (sem webhook)
intervalMsnumber2000
onConnected(phone: Phone) => void
onCancel() => void

Internamente faz: POST /phones (type=BAILEYS) → useQrCode polling → detecta CONNECTED via useChannelStatus.

<BaileysPairingConnect>

Fluxo alternativo via pairing code.

<BaileysPairingConnect
label="Atendimento Vendas"
onConnected={(phone) => router.push(`/phones/${phone.id}`)}
/>

UI:

  1. Input para o usuário digitar o número (E.164).
  2. POST /phones + POST /phones/:id/pairing-code.
  3. Polling em GET /pairing-code até receber.
  4. Exibe code formatado (ex: ABCD-1234).
  5. Polling do status até CONNECTED.

<WabaEmbeddedSignup>

Botão "Conectar WhatsApp Business" que abre o SDK JS da Meta.

<WabaEmbeddedSignup
label="Comercial Acme"
onConnected={(phone) => console.log('Phone WABA criado:', phone)}
onError={(err) => alert(err.message)}
/>

UI:

  1. Botão "Conectar WhatsApp Business".
  2. Carrega config via GET /waba/embedded-signup/config.
  3. Inicializa Facebook SDK (window.FB.init).
  4. Abre popup FB.login com config_id correto.
  5. Recebe code + phone_number_id + waba_id no callback.
  6. POST /waba/embedded-signup para finalizar.

<WabaManualConnect>

Form manual para inserir credenciais Meta (alternativa ao embedded para clientes que já têm WABA).

<WabaManualConnect onConnected={(phone) => ...} />

Campos: label, phoneNumberId, businessAccountId, accessToken, verifyToken.

<PhoneList>

Lista com status badges + ações (ver QR, reconectar, deletar).

<PhoneList
onSelect={(phone) => router.push(`/phones/${phone.id}`)}
onDelete={(phone) => alert(`Deletado: ${phone.label}`)}
/>

Primitives

Componentes UI base sem styling pesado (use seu design system se quiser substituir):

import {
Button, Input, Select, Card, Stack, Alert, Spinner, StatusBadge
} from '@paulorbj/wpp-gateway-react';
  • <StatusBadge status="CONNECTED"> — badge colorido por phone status
  • <Alert variant="error" | "warning" | "info"> — caixa de aviso
  • <Spinner> — loader CSS-only
  • <Card>, <Stack> — containers
  • <Button>, <Input>, <Select> — form básicos

Tipos exportados

import type {
Phone, PhoneStatus, ChannelType,
CreatePhoneInput, CreateBaileysPhoneInput, CreateWabaManualPhoneInput,
WebhookConfig, WabaEmbeddedSignupConfig, QrCode,
GatewayClientError, // class de erro normalizada
} from '@paulorbj/wpp-gateway-react';

GatewayClientError traz .code (mapeado do AppError.code do servidor), .status (HTTP), .details.


Server proxy de referência (Next.js)

// app/api/wpp/[...path]/route.ts
const GATEWAY_BASE = 'https://wpp.ogmma.com.br';
const API_KEY = process.env.WPP_API_KEY!;

async function handler(req: Request, ctx: { params: { path: string[] } }) {
const path = ctx.params.path.join('/');
const url = `${GATEWAY_BASE}/v1/${path}${new URL(req.url).search}`;

// ... aqui você AUTENTICA o usuário e mapeia para a accountId correta.
// Em um SaaS multi-tenant, cada tenant tem sua própria API key.

const upstream = await fetch(url, {
method: req.method,
headers: {
'X-API-Key': API_KEY,
'Content-Type': req.headers.get('content-type') ?? 'application/json',
},
body: ['GET', 'HEAD'].includes(req.method) ? undefined : await req.text(),
});

return new Response(await upstream.text(), {
status: upstream.status,
headers: { 'Content-Type': upstream.headers.get('content-type') ?? 'application/json' },
});
}

export { handler as GET, handler as POST, handler as PATCH, handler as DELETE };

SSE no cliente (sem SDK)

Para receber eventos em tempo real no browser, use diretamente o EventSource no proxy:

// app/api/wpp/events/route.ts
export async function GET() {
const upstream = await fetch(`${GATEWAY_BASE}/v1/events/stream?api_key=${API_KEY}`);
return new Response(upstream.body, {
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },
});
}

No componente React:

useEffect(() => {
const es = new EventSource('/api/wpp/events');
es.addEventListener('message:received', (e) => {
const evt = JSON.parse(e.data);
// ... atualizar UI
});
return () => es.close();
}, []);