Preparação do terminal
Antes da primeira venda, quatro coisas precisam estar em ordem: o app host instalado, o seu app confiando no host, o host confiando no seu app e o terminal ativado. As duas do meio formam uma validação mútua, e uma metade sozinha não protege a loja. Todas podem ser conduzidas a partir do seu app: ninguém precisa procurar o app do terminal.
Fluxo de primeira instalação
verifyHost ──▶ Trusted ──▶ terminalStatus()
│
┌────────────────────────┼──────────────────────────────┐
▼ ▼ ▼
UNAUTHORIZED_CALLER initialized = false initialized = true
requestAuthorization activateTerminal() pronto para vender
(aprovado no terminal) (cadastro preenchido
│ no terminal)
└──── consultar de novo ◀─┘
1. App host instalado
O app host (com.tupifintech.pos.service) é instalado no terminal pela Tupi ou pelo responsável pelo parque de terminais. Sem ele, verifyHost responde NotInstalled.
2. Seu app confere o host: verifyHost
A Intent é explícita para o pacote do host, o que impede o payload de vazar para outro app. Mas isso não prova quem respondeu. Num terminal sem o host instalado, um APK sideloadado com o mesmo applicationId poderia devolver APPROVED com um cupom plausível, e a mercadoria sairia da loja.
Por isso, confira a assinatura do host antes de liberar o pagamento:
when (val v = verifyHost(context, HOST_SHA256)) {
HostVerification.Trusted -> habilitarPagamento()
HostVerification.NotInstalled -> pedirInstalacaoDoTerminal()
is HostVerification.SignatureMismatch -> bloquearEAvisarSuporte(v.actualSha256)
}
| Resultado | Significado | O que fazer |
|---|---|---|
Trusted | O host instalado é o publicado pela Tupi | Liberar o pagamento |
NotInstalled | Não há host no terminal | Pedir a instalação; não enviar operações |
SignatureMismatch | Há um app com o pacote do host, mas assinado com outra chave | Bloquear, registrar actualSha256 e acionar o suporte |
HOST_SHA256é o SHA-256 do certificado com que a Tupi publica o host, entregue junto com o APK. Aceita o formato com ou sem os dois-pontos dokeytool, em maiúsculas ou minúsculas, sem espaços no meio.- O valor fica no seu app, e não no contrato: fixá-lo no contrato faria toda republicação do host quebrar os apps já instalados.
3. O host confere o seu app: credenciamento
Um terminal recém-instalado recusa todo mundo com UNAUTHORIZED_CALLER, de propósito: abrir por padrão transformaria cada aparelho num terminal público. O host identifica quem chama pelo pacote e pelo hash do certificado de assinatura. O pacote sozinho não prova nada num terminal onde sideload é rotina.
O credenciamento é um toque no terminal, não um formulário:
val pedido = PosIntegrationClient.requestAuthorization("PDV da loja — vendas e estornos")
pos.launch(pedido.intent)
O terminal lê o pacote e o certificado do chamador pelo próprio PackageManager e mostra a quem estiver no aparelho o nome e o ícone reais do app, mais os primeiros bytes do certificado para conferência. Quem aprova reconhece o app pela aparência. O texto passado em requestAuthorization aparece como motivo do pedido.
| Resposta | Significado | O que fazer |
|---|---|---|
APPROVED | App credenciado | Seguir com a operação original |
AUTHORIZATION_DENIED | Alguém recusou no terminal | Parar. É decisão humana: não tente de novo automaticamente |
INTERNAL_ERROR | O host não conseguiu ler o certificado do seu app | Registrar e acionar o suporte |
Dispare o pedido esperando resultado, com ActivityResultLauncher ou startActivityForResult. Com startActivity o host não identifica quem chamou e recusa o pedido, mas o seu app não recebe resposta nenhuma.
Pontos que costumam gerar chamado
request_authorizationé a única operação que um app não credenciado alcança, e é idempotente: pedir de novo já credenciado devolveAPPROVED. Não é preciso consultar antes.- Builds debug e release têm assinaturas diferentes, e cada uma precisa do seu credenciamento.
- Na rotação de chave do seu app pelo esquema de assinatura v3, a chave anterior continua reconhecida e não é preciso credenciar de novo. Se a nova assinatura não tiver vínculo com a anterior, peça o credenciamento de novo com a chave nova: as duas passam a valer.
- Revogar, na tela Aplicativos autorizados do terminal, remove todas as chaves do pacote.
- Não existe cadastro manual de pacote e hash. A tela Aplicativos autorizados lista e revoga, mas não autoriza: o handshake é o único caminho de credenciamento.
- Para conferir a impressão digital do seu APK:
keytool -printcert -jarfile seu-app.apk.
4. Terminal ativado: activateTerminal
O terminal só vende depois de ativado: vinculado ao estabelecimento e com as tabelas de pagamento carregadas. Até lá, vendas, estornos, impressão, reimpressão, consultas de transações e resolve_pending respondem TERMINAL_NOT_INITIALIZED. Credenciamento, ativação, terminal_status, recover_result e as operações de usuários locais funcionam sem ativação. Para saber antes de abrir o caixa, use terminalStatus() e confira terminal.initialized.
O seu app pode abrir a ativação:
pos.launch(PosIntegrationClient.activateTerminal().intent)
O terminal mostra, por cima do seu app, o formulário de cadastro do parceiro (por exemplo, CNPJ e código de verificação), com a indicação de qual app pediu. Quem está no aparelho preenche e confirma. Os dados do estabelecimento são digitados no terminal e nunca passam pela Intent: a requisição não tem campos, e isso é contratual.
| Situação do terminal | Resposta |
|---|---|
| Nunca ativado, e o cadastro é concluído | APPROVED, com terminal preenchido (initialized = true) |
| Nunca ativado, e quem está no aparelho cancela | CANCELLED (OPERATION_CANCELLED) |
| Já ativado | APPROVED na hora, sem formulário |
| Ativado antes, mas sem conseguir carregar as tabelas agora (por exemplo, sem rede) | TERMINAL_NOT_INITIALIZED, sem formulário |
- Exige credenciamento. Num terminal novo, o fluxo é
requestAuthorizatione, na sequência,activateTerminal: dois toques de quem está no aparelho, sem sair do seu app. - Não troca o cadastro. Com o terminal já vinculado a um estabelecimento, o formulário não abre: trocar o cadastro por Intent permitiria que um app desviasse os recebimentos para outro CNPJ. A troca de cadastro é feita só pelo app do terminal.
- Ocupa o terminal como uma venda: enquanto o formulário está aberto, outras operações recebem
TERMINAL_BUSY. Dispare comstartActivityForResulte não imponha timeout curto. - Os campos do formulário e o título dele vêm do cadastro do parceiro no portal. Se aparecer um texto inesperado no formulário, a correção é no cadastro, não no seu app.
A ativação também pode ser feita abrindo o app do terminal diretamente, como alternativa.