Uma API só para pagar, imprimir e escanear em terminais de POS brasileiros.
O ecossistema de maquininha em React Native é fragmentado: cada fabricante tem SDK próprio, com nome de método diferente, código de erro cru e nenhum contrato em comum. Se você troca de PagBank para Sunmi, reescreve a camada inteira. Este pacote existe para isso não acontecer.
O pacote não tem dependência nenhuma. Você instala só o SDK do fabricante que usa:
npm install react-native-pos-br
# PagBank / PagSeguro (PlugPag SmartPOS)
npm install @lucasmaffei/react-native-plugpag
# Sunmi
npm install @mitsuharu/react-native-sunmi-printer-libraryOs dois são peerDependencies opcionais. Quem só usa Sunmi nunca baixa o nativo do PagBank.
Registre os adapters uma vez, na inicialização do app:
import { registerAdapter } from 'react-native-pos-br';
import { PagBankAdapter } from 'react-native-pos-br/pagbank';
import { SunmiAdapter } from 'react-native-pos-br/sunmi';
registerAdapter(PagBankAdapter);
registerAdapter(SunmiAdapter);Depois é só usar os hooks. Eles não sabem o fabricante:
import { usePayment, usePrinter, useCapabilities } from 'react-native-pos-br';
function Checkout() {
const { capabilities } = useCapabilities();
const { pay, progress, busy, error } = usePayment();
const { print } = usePrinter();
async function cobrar() {
const result = await pay({
amountInCents: 1050, // R$ 10,50 — sempre centavos inteiros
method: 'credit',
reference: 'pedido-42',
});
if (result?.status !== 'approved') return;
await print([
{ type: 'text', value: 'MEU BAR', align: 'center', bold: true },
{ type: 'divider' },
{ type: 'columns', cells: ['Cerveja', 'R$ 10,50'], widths: [3, 1] },
{ type: 'qrcode', value: `https://meubar.app/n/${result.acquirer.hostNsu}` },
{ type: 'feed', lines: 2 },
{ type: 'cut' },
]);
}
return (
<Button
title={busy ? traduzir(progress.stage) : 'Cobrar'}
onPress={cobrar}
disabled={busy || !capabilities.includes('payment')}
/>
);
}Porque os aparelhos não são da mesma categoria, e uma interface única de "terminal" obrigaria um deles a mentir:
| PagBank (PlugPag) | Sunmi | |
|---|---|---|
payment |
✅ | ❌ (o pagamento vem de outro adquirente) |
printer |
✅ | ✅ |
scanner |
❌ | ✅ |
nfc |
✅ | ❌ |
Cada adapter declara só o que implementa, e useCapabilities() diz o que existe neste aparelho. Você esconde o botão de estorno em vez de descobrir na mão do operador que a função não existe.
Valor sempre em centavos inteiros. amountInCents: 10.5 é recusado com INVALID_ARGUMENT antes de chegar no hardware. Float em dinheiro é bug esperando a hora.
Erro normalizado. O PlugPag devolve número sem documentação consistente; o Sunmi rejeita com string. Aqui tudo chega como PosError com um code estável — e o código cru do fabricante fica em rawCode, para o seu suporte:
import { isPosError } from 'react-native-pos-br';
if (isPosError(error) && error.code === 'PRINTER_OUT_OF_PAPER') {
avisarOperador('Troque a bobina');
}Códigos: NOT_AVAILABLE, NOT_ACTIVATED, ABORTED, DECLINED, INVALID_ARGUMENT, PRINTER_OUT_OF_PAPER, PRINTER_OVERHEATED, PRINTER_NOT_READY, HARDWARE, TIMEOUT, UNKNOWN.
Cancelamento não é recusa. Cliente que aperta "cancelar" gera PosError('ABORTED'), não um resultado declined. São coisas diferentes no seu relatório.
Toque duplo não cobra duas vezes. usePayment serializa: a segunda chamada durante uma transação em andamento devolve null sem tocar no terminal.
Impressão declarativa e portável. Você descreve o cupom; cada adapter renderiza com o que tem. columns no Sunmi usa a API nativa de colunas; no PagBank, que só imprime string, degrada para texto monoespacado alinhado. cut é ignorado em terminal sem guilhotina. Nada falha silenciosamente por falta de recurso.
Peer ausente não derruba o app. Se o SDK do fabricante não está instalado, isAvailable() devolve false e as operações lançam NOT_AVAILABLE com a instrução de instalação — em vez de estourar na importação.
| Export | O que é |
|---|---|
registerAdapter(adapter) |
Registra um adapter. Chame na inicialização |
useCapabilities() |
{ capabilities, loading, refresh } — o que existe neste aparelho |
usePayment({ adapterId? }) |
{ pay, refund, abort, progress, result, error, busy } |
usePrinter({ adapterId? }) |
{ print, status, checkStatus, error, busy } |
useScanner({ adapterId? }) |
{ scan, value, error, busy } |
resolve(capability, adapterId?) |
Acesso imperativo, sem React |
PosError / isPosError |
Erro normalizado |
renderColumns(cells, widths?, aligns?, total?) |
Colunas monoespacadas, útil fora do pacote também |
adapterId só é necessário quando dois adapters oferecem a mesma capability — por exemplo impressora do PagBank e do Sunmi no mesmo aparelho.
| Fabricante | Status | Peer |
|---|---|---|
| PagBank / PagSeguro | ✅ v1 | @lucasmaffei/react-native-plugpag |
| Sunmi | ✅ v1 | @mitsuharu/react-native-sunmi-printer-library |
| Stone | 🙋 quer? manda PR | — |
| Gertec | 🙋 quer? manda PR | — |
| Elgin | 🙋 quer? manda PR | — |
Não preciso ter o seu hardware para aceitar o seu adapter. O contrato é executável: o pacote exporta uma suíte de conformidade que o seu adapter tem que passar. Ver ADAPTERS.md.
0.1.0, e vale ser honesto sobre o que isso significa:
- O núcleo é TypeScript puro, com 130 testes e ~91% de cobertura, incluindo os adapters com os SDKs nativos simulados.
- A validação em hardware físico ainda não foi feita. Os adapters foram escritos a partir da API pública de cada SDK e da experiência de três apps de POS em produção, mas o v1 sai antes da migração desses apps.
Se você rodar em terminal de verdade, abra uma issue contando o que funcionou e o que não — é o feedback mais valioso para este pacote agora.
MIT © Lucas Maffei