Pular para o conteúdo principal

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)
}
ResultadoSignificadoO que fazer
TrustedO host instalado é o publicado pela TupiLiberar o pagamento
NotInstalledNão há host no terminalPedir a instalação; não enviar operações
SignatureMismatchHá um app com o pacote do host, mas assinado com outra chaveBloquear, 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 do keytool, 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.

RespostaSignificadoO que fazer
APPROVEDApp credenciadoSeguir com a operação original
AUTHORIZATION_DENIEDAlguém recusou no terminalParar. É decisão humana: não tente de novo automaticamente
INTERNAL_ERRORO host não conseguiu ler o certificado do seu appRegistrar 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 devolve APPROVED. 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 terminalResposta
Nunca ativado, e o cadastro é concluídoAPPROVED, com terminal preenchido (initialized = true)
Nunca ativado, e quem está no aparelho cancelaCANCELLED (OPERATION_CANCELLED)
Já ativadoAPPROVED 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 é requestAuthorization e, 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 com startActivityForResult e 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.