MobiariDocs

Notificações push

Como os apps mobile do Mobiari se registram para receber push, o que cada push carrega, como abrir a tela certa ao tocar e como os operadores ativam APNs e FCM.

Os apps para lojistas (iOS e Android, application id com.mobiari.merchant) recebem os mesmos alertas de equipe que a plataforma envia por e-mail e WhatsApp: pedidos novos e pagos, cancelamentos, estoque baixo e conversas atribuídas a você ou a ninguém. O servidor envia os pushes de iOS pelo APNs e os de Android pelo Firebase Cloud Messaging (FCM).

O push é para membros da equipe conectados ao app. Tokens de API da loja não podem registrar dispositivos.

Registrar o dispositivo

Primeiro, peça ao sistema operacional a permissão e um token (iOS: registerForRemoteNotifications e depois didRegisterForRemoteNotificationsWithDeviceToken; Android: FirebaseMessaging.getInstance().token e onNewToken). Em seguida:

POST /v1/me/devices
Authorization: Bearer <access token>
Content-Type: application/json

{
  "platform": "ios",
  "token": "5f0c1e...9a",
  "app_version": "1.4.0 (52)",
  "device_name": "iPhone de Ana",
  "locale": "pt-BR",
  "environment": "production"
}
Campo
platformios ou android.
tokeniOS: o device token do APNs em hexadecimal (o Data recebido no delegate, codificado em hex). Android: o registration token do FCM.
app_version, device_nameOpcionais, exibidos na lista de dispositivos.
localeTag BCP 47 opcional. Pushes pt-* saem em português, en-* em inglês. Sem ela, vale o idioma do perfil do usuário.
environmentSomente iOS. sandbox para builds de debug executados pelo Xcode, production (padrão) para TestFlight e App Store. Um token enviado ao gateway errado é rejeitado e o dispositivo é excluído.

A resposta é o dispositivo: 201 quando ele é novo, 200 quando o token já estava registrado (ele é atualizado). Guarde o id para a chamada de teste e para cancelar o registro.

Regras que o servidor aplica:

  • O registro é um upsert em (platform, token). Um token que estava registrado por outra conta passa para quem fez a chamada.
  • O dispositivo fica vinculado à sessão do access token (sid). Um token anterior da mesma sessão e plataforma é descartado; assim, depois que o sistema operacional troca o token, você só registra o novo.
  • Encerrar a sessão interrompe os pushes dela: POST /auth/logout, DELETE /me/sessions/{session_id}, uma troca de senha feita em outro lugar e a reutilização de refresh token excluem todos os dispositivos da sessão. Uma sessão expirada mantém o registro, mas não recebe pushes.
  • Tokens que o APNs ou o FCM informam como inválidos (app desinstalado, app errado, ambiente errado) são excluídos.

Quando chamar:

  1. Depois de cada login (a nova sessão precisa do próprio registro de dispositivo).
  2. Sempre que o sistema operacional entregar um token novo.
  3. Ao abrir o app, se o token ou o idioma do aparelho mudou desde o último registro. Chamar sem nenhuma mudança não causa problema.

Antes de sair da conta, chame DELETE /v1/me/devices/{device_id} (a chamada de logout também remove o dispositivo, mas a chamada explícita não depende de o refresh token ainda ser válido). GET /v1/me/devices lista os dispositivos do usuário; current marca o registrado pela sessão que fez a chamada e active fica false quando a sessão dele termina.

Testar

POST /v1/me/devices/{device_id}/test

Envia na hora "As notificações estão funcionando neste aparelho." (no idioma do dispositivo) e informa o que o provedor respondeu:

StatusSignificado
200Aceito: {device_id, provider: "apns" | "fcm", provider_message_id}. O usuário ainda pode estar com as notificações desativadas.
410 push_token_invalidO provedor rejeitou o token. O dispositivo foi excluído; registre de novo.
502 push_provider_errorO provedor recusou ou não respondeu; error traz o motivo.
503 push_not_configuredO push para essa plataforma não está configurado no servidor.

Preferências

Cada membro escolhe, por loja, quais alertas chegam por push aos seus dispositivos:

GET /v1/stores/{store_id}/me/push-preferences
PUT /v1/stores/{store_id}/me/push-preferences
{ "is_enabled": true, "enabled_events": ["orders.paid", "conversations.message_received"] }

available_events lista os alertas que o membro pode receber naquela loja (as permissões dele decidem). Até ele salvar, is_default é true e valem os padrões: todos os alertas de pedidos e de conversas ativados, estoque baixo desativado. is_enabled: false silencia a loja por completo. As preferências valem para todos os dispositivos do usuário.

Alerta (kind)QuandoQuem recebeRequer
orders.createdUm pedido novo (online ou venda manual)Todo membro que estiver com ele ativadoorders.read
orders.paidPagamento confirmado (uma vez por pedido, não importa quantas vezes o provedor confirme)Idemorders.read
orders.cancelledPedido canceladoIdemorders.read
inventory.low_stockUma variante chegou a 5 unidades ou menos em um local (no máximo uma vez por dia por variante e local)Ideminventory.read
conversations.startedUm cliente iniciou uma conversa e o bot de chat está desligadoO responsável, ou todos os membros quando não há responsávelconversations.read
conversations.message_receivedUm cliente escreveu e o bot de chat está desligadoO responsável, ou todos os membros quando não há responsávelconversations.read
conversations.handed_overO bot de chat transferiu a conversa para a equipeO responsável, ou todos os membros quando não há responsávelconversations.read
conversations.assignedOutra pessoa atribuiu uma conversa a vocêO novo responsávelconversations.read

Novos tipos serão adicionados. Ignore um kind que você não conhece (abra a tela inicial do app).

O que um push carrega

Todo push tem título e corpo localizados, além de dados para deep link:

ChavePresente em
kindSempre. Um dos tipos acima, ou test.
store_idSempre, exceto em test.
order_idorders.*
conversation_idconversations.*
product_id, variant_idinventory.low_stock

Todos os valores são strings (UUIDs).

iOS (APNs). As chaves de dados ficam no nível raiz do payload, ao lado de aps:

{
  "aps": {
    "alert": { "title": "Pedido #1042 pago", "subtitle": "Loja Azul", "body": "R$ 1.234,56 · Ana Souza" },
    "sound": "default",
    "badge": 3,
    "thread-id": "store-0199a0c4-..."
  },
  "kind": "orders.paid",
  "store_id": "0199a0c4-...",
  "order_id": "0199b7e2-..."
}

subtitle é o nome da loja. thread-id agrupa os pushes de uma conversa (conv-<conversation id>) e os demais alertas de uma loja (store-<store id>). Os pushes usam apns-push-type: alert, prioridade 10, e expiram depois de uma hora.

Android (FCM). Uma mensagem notification com os dados em data:

{
  "notification": { "title": "Order #1042 paid", "body": "R$1,234.56 · Ana Souza" },
  "data": { "kind": "orders.paid", "store_id": "0199a0c4-...", "order_id": "0199b7e2-..." },
  "android": {
    "priority": "high",
    "ttl": "3600s",
    "collapse_key": "order-paid-0199b7e2-...",
    "notification": { "channel_id": "orders", "tag": "order-paid-0199b7e2-...", "notification_count": 3, "sound": "default" }
  }
}

Crie três canais de notificação no app para que os usuários possam silenciá-los separadamente: orders, conversations e inventory. Enquanto um canal não existir, o Android mostra o push no canal padrão do app.

Agrupamento. Pushes sobre a mesma conversa compartilham uma chave de agrupamento (apns-collapse-id / collapse_key e tag = conv-<conversation id>): uma sequência de mensagens deixa uma única notificação com o texto mais recente. Estoque baixo é agrupado por variante; alertas de pedido, por pedido e tipo.

Badge. O badge é o número de conversas abertas ou pendentes com mensagens não lidas que estão atribuídas ao usuário ou a ninguém, na loja a que o push se refere (o lado unread de GET /stores/{store_id}/conversations/counts, restrito às suas e às sem responsável). Ele é omitido para membros sem conversations.read. Recalcule-o pelo endpoint de contadores quando o app abrir.

Tratando um toque

Leia kind e store_id dos dados do push. Se o store_id não for a loja selecionada no momento no app, troque para ela primeiro (o usuário pode pertencer a várias lojas) e depois:

kindAbrir
orders.created, orders.paid, orders.cancelledO pedido: GET /stores/{store_id}/orders/{order_id}
conversations.*A conversa: GET /stores/{store_id}/conversations/{conversation_id}
inventory.low_stockO estoque da variante: GET /stores/{store_id}/variants/{variant_id}/stock (ou o produto, product_id)
testNada em especial (a tela inicial)
qualquer outroA tela inicial

Se o usuário perdeu o acesso à loja ou o registro não existe mais, a chamada responde 403 ou 404; nesse caso, mostre a tela inicial da loja.

Para operadores

O push fica desligado até as credenciais serem configuradas. Sem nenhuma, o backend registra push notifications disabled uma vez na inicialização e todo o resto funciona como antes; POST /me/devices/{id}/test responde 503 push_not_configured. Cada provedor é ativado de forma independente: configure só o APNs e os dispositivos Android simplesmente não recebem nada (e vice-versa). Uma chave que não pode ser interpretada desativa aquele provedor com um erro no log de inicialização; o backend sobe mesmo assim.

Variável
APNS_KEY_P8Conteúdo da chave de autenticação do APNs (AuthKey_XXXXXXXXXX.p8, PEM PKCS#8). Em um arquivo env, coloque em uma única linha com \n no lugar das quebras de linha.
APNS_KEY_PATHAlternativa a APNS_KEY_P8: caminho do arquivo .p8 dentro do contêiner.
APNS_KEY_IDO id de 10 caracteres da chave.
APNS_TEAM_IDO team id do Apple Developer.
APNS_TOPICO bundle id do app. Padrão com.mobiari.merchant.
FCM_SERVICE_ACCOUNT_JSONO JSON da chave de uma conta de serviço do Firebase (Configurações do projeto, Contas de serviço, "Gerar nova chave privada"), minificado em uma linha.
FCM_SERVICE_ACCOUNT_PATHAlternativa: caminho do arquivo JSON dentro do contêiner.
FCM_PROJECT_IDO id do projeto no Firebase. O padrão é o project_id da conta de serviço.

O APNs usa autenticação por token: o backend assina um JWT ES256 com a chave e fala HTTP/2 com api.push.apple.com (ou api.sandbox.push.apple.com para dispositivos registrados com environment: sandbox). Uma única chave funciona nos dois gateways e em todos os apps do time. O FCM usa a API HTTP v1; o backend troca um JWT assinado com a conta de serviço por um access token OAuth2 e o reutiliza até ele expirar.

As entregas passam pela fila de jobs (tabela jobs, kind push.deliver): um job por dispositivo, com novas tentativas em backoff exponencial em caso de 5xx do provedor, 429 e erros de rede, e enviado para a dead letter depois de 5 tentativas ou em erros que uma nova tentativa não resolve (credenciais inválidas, payload rejeitado). Um job de push na dead letter com InvalidProviderToken ou THIRD_PARTY_AUTH_ERROR indica que as credenciais estão erradas.

Nesta página