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 | |
|---|---|
platform | ios ou android. |
token | iOS: o device token do APNs em hexadecimal (o Data recebido no delegate, codificado em hex). Android: o registration token do FCM. |
app_version, device_name | Opcionais, exibidos na lista de dispositivos. |
locale | Tag BCP 47 opcional. Pushes pt-* saem em português, en-* em inglês. Sem ela, vale o idioma do perfil do usuário. |
environment | Somente 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:
- Depois de cada login (a nova sessão precisa do próprio registro de dispositivo).
- Sempre que o sistema operacional entregar um token novo.
- 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}/testEnvia na hora "As notificações estão funcionando neste aparelho." (no idioma do dispositivo) e informa o que o provedor respondeu:
| Status | Significado |
|---|---|
200 | Aceito: {device_id, provider: "apns" | "fcm", provider_message_id}. O usuário ainda pode estar com as notificações desativadas. |
410 push_token_invalid | O provedor rejeitou o token. O dispositivo foi excluído; registre de novo. |
502 push_provider_error | O provedor recusou ou não respondeu; error traz o motivo. |
503 push_not_configured | O 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) | Quando | Quem recebe | Requer |
|---|---|---|---|
orders.created | Um pedido novo (online ou venda manual) | Todo membro que estiver com ele ativado | orders.read |
orders.paid | Pagamento confirmado (uma vez por pedido, não importa quantas vezes o provedor confirme) | Idem | orders.read |
orders.cancelled | Pedido cancelado | Idem | orders.read |
inventory.low_stock | Uma variante chegou a 5 unidades ou menos em um local (no máximo uma vez por dia por variante e local) | Idem | inventory.read |
conversations.started | Um cliente iniciou uma conversa e o bot de chat está desligado | O responsável, ou todos os membros quando não há responsável | conversations.read |
conversations.message_received | Um cliente escreveu e o bot de chat está desligado | O responsável, ou todos os membros quando não há responsável | conversations.read |
conversations.handed_over | O bot de chat transferiu a conversa para a equipe | O responsável, ou todos os membros quando não há responsável | conversations.read |
conversations.assigned | Outra pessoa atribuiu uma conversa a você | O novo responsável | conversations.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:
| Chave | Presente em |
|---|---|
kind | Sempre. Um dos tipos acima, ou test. |
store_id | Sempre, exceto em test. |
order_id | orders.* |
conversation_id | conversations.* |
product_id, variant_id | inventory.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:
kind | Abrir |
|---|---|
orders.created, orders.paid, orders.cancelled | O pedido: GET /stores/{store_id}/orders/{order_id} |
conversations.* | A conversa: GET /stores/{store_id}/conversations/{conversation_id} |
inventory.low_stock | O estoque da variante: GET /stores/{store_id}/variants/{variant_id}/stock (ou o produto, product_id) |
test | Nada em especial (a tela inicial) |
| qualquer outro | A 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_P8 | Conteú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_PATH | Alternativa a APNS_KEY_P8: caminho do arquivo .p8 dentro do contêiner. |
APNS_KEY_ID | O id de 10 caracteres da chave. |
APNS_TEAM_ID | O team id do Apple Developer. |
APNS_TOPIC | O bundle id do app. Padrão com.mobiari.merchant. |
FCM_SERVICE_ACCOUNT_JSON | O 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_PATH | Alternativa: caminho do arquivo JSON dentro do contêiner. |
FCM_PROJECT_ID | O 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.
Caixa de entrada em tempo real
Os dois WebSockets por trás da caixa de entrada compartilhada e do chat da loja virtual. URLs, autenticação, todos os eventos, heartbeats, reconexões e como recuperar o que ficou para trás via REST.
Admin API
Merchant API: what the Mobiari admin and mobile apps use, open to your own integrations, scripts and back-office tools.