@bfocus/react-native-widget
v0.1.0
Published
Widget de suporte do bFocus (chamados, chat ao vivo e release notes) para React Native e Expo: botão com badge, pílula de versão, banner de ciência e push.
Maintainers
Readme
@bfocus/react-native-widget
English summary. Support widget of bFocus for React Native and Expo: tickets, live chat and release notes, the same screens as the web widget, rendered in a
react-native-webview. Pure JS (no native code of its own), Expo compatible. Includes a floating button with badge, a version pill, a non-dismissible acknowledgement banner and FCM push handling (you bring the token; the package does not depend on Firebase). Install withnpm i @bfocus/react-native-widget react-native-webview @react-native-async-storage/async-storage, wrap your app in<BFocusProvider config={…}>and add<BFocusLauncher />. Only the public keybf_pk_…and theuserHash(HMAC computed on your server) go into the app. MIT license.
Widget de suporte do bFocus (chamados, chat ao vivo e release notes) para apps React Native e
Expo. O pacote mostra o mesmo embed.html / release-notes.html do widget web numa WebView e
cuida só do que é nativo: botão com badge, pílula de versão, banner de ciência, links, arquivos,
push e a consulta periódica do estado.
- JS puro, sem código nativo próprio: funciona no Expo (dev client ou Expo Go com
react-native-webview). - Protocolo: Host Protocol v1. O cabeçalho
X-bFocus-Clientvai comoreact-native/0.1.0.
Instalação
npm i @bfocus/react-native-widget react-native-webview @react-native-async-storage/async-storage
# Expo
npx expo install @bfocus/react-native-widget react-native-webview @react-native-async-storage/async-storageDependências
| Pacote | Tipo | Para quê |
|---|---|---|
| react ≥ 18 | peer | useSyncExternalStore |
| react-native ≥ 0.71 | peer | UI, Linking, AppState, BackHandler |
| react-native-webview ≥ 13 | peer | a tela do widget |
| @react-native-async-storage/async-storage | peer opcional | guarda o last_seen do "•" e o token de push. Sem ele, use storage na config; senão fica só em memória (o "•" não persiste entre aberturas do app) |
O pacote não tem outras dependências.
Antes de começar
- No bFocus, em Integrações → Widget, copie a chave pública
bf_pk_…. - Em Integrações → Apps nativos, cadastre o id do app (
app://com.empresa.app). É o bundle id no iOS e o package name no Android. - Se o tenant exige sessão verificada, o
userHashé obrigatório (veja abaixo).
appId
O React Native não expõe o id do app sem módulo nativo, então appId é obrigatório. Pegue-o
assim:
import * as Application from 'expo-application' // Expo
const appId = Application.applicationId! // "com.empresa.app"
import DeviceInfo from 'react-native-device-info' // RN puro
const appId = DeviceInfo.getBundleId()Ou escreva o valor fixo. A origem enviada é app://<appId em minúsculas>.
Uso
import { BFocus, BFocusProvider, BFocusLauncher, BFocusReleaseBadge } from '@bfocus/react-native-widget'
const config = {
publishableKey: 'bf_pk_live_…',
appId: 'com.empresa.app',
user: { externalId: 'USR-1', name: 'Ana Souza', email: '[email protected]' },
customer: { externalId: 'ACME-1', name: 'Acme Ltda' },
userHashProvider: async () => (await api.post('/bfocus/user-hash')).userHash,
product: 'erp',
onError: (code, detail) => console.warn(code, detail),
}
export default function App() {
return (
// Na raiz do app: o widget abre numa camada em tela cheia por cima de tudo.
<BFocusProvider config={config}>
<Navigation />
<BFocusLauncher /> {/* botão flutuante com badge */}
</BFocusProvider>
)
}
// Em qualquer tela:
<BFocusReleaseBadge /> // pílula "v4.2.0" com ponto de novidade; toca → histórico
BFocus.open() // lista
BFocus.open('new') // novo chamado
BFocus.open({ ticketId: 'abc' }) // um chamado
BFocus.open('chat') // chat ao vivoPrefere o próprio botão? Use BFocus.open() e o hook useBFocusBadge() (ou o callback
onBadgeChanged). O <BFocusProvider> continua necessário para mostrar a WebView.
Configuração
| Campo | Obrigatório | Padrão | Descrição |
|---|---|---|---|
| publishableKey | sim | — | bf_pk_… |
| appId | sim | — | bundle id / package name |
| user | sim | — | {externalId, name?, email?, phone?} |
| customer | sim | — | {externalId, name?, document?, email?, phone?, website?} |
| userHash | não | — | HMAC calculado no servidor |
| userHashProvider | não | — | função assíncrona que devolve o userHash. É chamada no init e de novo, uma vez, após WIDGET_USER_HASH_INVALID |
| product | não | — | slug do produto |
| audience | não | — | external | internal | both |
| locale | não | idioma do aparelho (Intl) | pt_BR | en | es (aceita pt-BR, es-AR…) |
| showReleaseNotes | não | true | false esconde o splash de novidades dentro dos chamados |
| autoShowReleaseBanner | não | true | abre sozinho o banner de ciência. Com false, use BFocus.openReleaseNotesBanner() |
| apiBaseUrl / embedBaseUrl | não | produção | homologação e testes (http:// só em 127.0.0.1/localhost) |
| pollIntervalSeconds | não | 60 | intervalo do launcher-state (mínimo 15) |
| storage | não | AsyncStorage | {getItem, setItem, removeItem} assíncrono |
| fetch | não | fetch global | para proxies e testes |
| onBadgeChanged(label) | não | — | '' | '•' | 'N' | '99+' |
| onReleaseNotesChanged({label, dot, bannerIds}) | não | — | a pílula mudou |
| onError(code, detail?) | não | — | veja Erros |
| onOpen / onClose | não | — | o widget abriu / fechou |
Os eventos também existem como BFocus.addListener('badgeChanged' | 'releaseNotesChanged' | 'error' | 'open' | 'close', cb),
que devolve { remove() }.
Métodos
| Método | Efeito |
|---|---|
| BFocus.init(config) | valida (lança BFocusConfigError na hora) e inicia. O <BFocusProvider config> chama sozinho |
| open(target?) | list (padrão), new, chat, {ticketId}, {conversationId} |
| close() | fecha, mantendo a WebView para reabrir rápido |
| openReleaseNotesHistory() | histórico de versões |
| openReleaseNotesBanner() | banner de ciência com a fila atual |
| openInBrowser(target?) | modo navegador: abre o widget em página inteira no navegador do sistema (host=browser) |
| refresh() | consulta o launcher-state agora |
| logout() | para a consulta, cancela o push, limpa o estado local e o localStorage do embed |
| registerPushToken(token, platform?) | registra o token FCM (platform padrão: Platform.OS, só android/ios) |
| handlePush(data) → boolean | true se a notificação é do bFocus; nesse caso abre o widget no item certo |
<BFocusProvider>
| Prop | Padrão | |
|---|---|---|
| config | — | chama BFocus.init (e de novo quando muda). Troca de usuário recria a WebView |
| presentation | overlay | overlay: camada em tela cheia com a WebView mantida entre aberturas. modal: Modal do RN, que fica acima de modais nativos (react-navigation presentation: 'modal'), mas recarrega a cada abertura |
| webViewProps | — | props extras das WebViews (ex.: webviewDebuggingEnabled) |
O botão voltar do Android fecha o widget. O banner de ciência abre em Modal de tela cheia que não fecha por gesto nem pelo voltar; o usuário tem de confirmar no próprio banner.
userHash (identidade verificada)
O userHash é hex(HMAC-SHA256(segredo, "v1:" + user.externalId + ":" + customer.externalId)),
calculado no seu servidor com o segredo bf_whs_…. Use os SDKs do bFocus:
// Node: @bfocus/sdk
import { signWidgetIdentity } from '@bfocus/sdk'
const userHash = signWidgetIdentity(process.env.BFOCUS_WIDGET_SECRET!, user.id, user.companyId)# Python: bfocus
from bfocus import sign_widget_identity
user_hash = sign_widget_identity(os.environ["BFOCUS_WIDGET_SECRET"], user.id, user.company_id)Exponha isso numa rota autenticada do seu backend e devolva o hash ao app pelo
userHashProvider.
Push (FCM)
O pacote não depende do Firebase: o seu app já tem o FCM e entrega o token e o data. Cadastre
a credencial FCM do app em Integrações → Apps nativos.
import messaging from '@react-native-firebase/messaging'
const token = await messaging().getToken()
await BFocus.registerPushToken(token) // espera a primeira consulta do widget
messaging().onTokenRefresh((t) => BFocus.registerPushToken(t))
// Toque na notificação (app em segundo plano ou fechado):
messaging().onNotificationOpenedApp((m) => BFocus.handlePush(m.data))
const initial = await messaging().getInitialNotification()
if (initial) BFocus.handlePush(initial.data) // funciona mesmo antes do inithandlePush aceita o mapa data ou a mensagem inteira ({data: …}). Com expo-notifications,
passe response.notification.request.content.data. Formato do data do bFocus: bfocus: "1",
type (ticket.reply | chat.message | ticket.status), ticket_id e, no chat,
conversation_id. O token é registrado de novo se o usuário mudar, e o logout() cancela o
registro.
Erros
onError(code, detail) recebe:
| Código | Quando |
|---|---|
| WIDGET_USER_HASH_INVALID | userHash recusado. Com userHashProvider, o pacote pede outro hash uma vez e repete |
| WIDGET_VERIFIED_SESSION_REQUIRED | o tenant exige userHash e ele não veio |
| WIDGET_CONFIG_FAILED | o embed não carregou a configuração (chave, origem não cadastrada, widget desligado) |
| outros 4xx do launcher-state | o código vem da API (ex.: origem app:// não cadastrada) |
| WIDGET_CONFIG_INVALID | só pelo <BFocusProvider>: a config não passou na validação |
Erros de rede e 5xx são silenciosos: o próximo ciclo tenta de novo. Sem conexão, a tela do widget mostra "Sem conexão" com "Tentar novamente".
Comportamento
- Consulta:
POST /api/v1/widget/launcher-statelogo após oinite a cada 60 s, só com o app em primeiro plano e o widget fechado. Ao fechar, consulta na hora. A primeira chamada cria o usuário no bFocus: nada mais sai antes dela (nem o push, nem a WebView). - "•" do botão: o
last_seenfica gravado por chave + usuário + cliente. Na primeira vez só grava; depois acende quando há evento novo. Com o widget aberto, o badge segue os não lidos. - Links e arquivos: a WebView só navega na origem do embed. Links externos e downloads
(URL pré-assinada) abrem no navegador do sistema. O
<input type=file>usa o seletor nativo doreact-native-webview; para a câmera no Android, declare a permissãoCAMERAno app. - Segurança da ponte: mensagens só da página na origem de
embedBaseUrle só dos tipos do protocolo. Tipo desconhecido é ignorado.
Segurança
- No app vão só a chave pública
bf_pk_…e ouserHash. Nuncabf_whs_…(segredo do widget),bf_live_…oubf_sk_…: oinitrecusa essas chaves. - A origem
app://…é só um rótulo; a proteção real é ouserHashcomrequire_verified_sessionligado no tenant.
Testes e desenvolvimento
npm install
npm run typecheck && npm run build && npm testOs testes de integração sobem widgets-native/conformance/mock-server.mjs (monorepo) e são
pulados fora dele. Defina BFOCUS_MOCK_SERVER=/caminho/mock-server.mjs para rodá-los.
Se o seu app testa com Jest e o preset do React Native, inclua o pacote no
transformIgnorePatterns (o lib/ é ESM), por exemplo
node_modules/(?!(@react-native|react-native|@bfocus/react-native-widget)/).
Licença
MIT — Copyright (c) 2026 Berni Software.
