expo-notifications em 2026: Push Notifications com FCM v1 e APNs no React Native
Guia prático de push notifications no React Native com expo-notifications em 2026: credenciais FCM v1 e APNs, permissões runtime, canais Android e envio pelo backend com expo-server-sdk.
O expo-notifications é a biblioteca oficial do Expo para enviar e receber push notifications no React Native. Ela usa o Expo Push Service, que encaminha as mensagens para FCM v1 no Android e APNs no iOS. Em 2026, com o Expo SDK 54, o fluxo padrão exige um development build (o Expo Go deixou de receber pushes remotas desde o SDK 53), permissões runtime no Android 13+ e canais de notificação no Android 8+. Este guia mostra a configuração completa: credenciais, obtenção do token, envio pelo backend com expo-server-sdk, tratamento em foreground/background e as principais armadilhas. Honestamente, boa parte do que quebra em produção não é o código, e sim a configuração de credenciais no EAS.
A partir do Expo SDK 53 o Expo Go não recebe mais push notifications remotas. É obrigatório um development build ou build de produção para testar.
O FCM legado foi desligado em junho de 2024. O Expo já usa FCM v1 automaticamente desde o SDK 51, mas você precisa subir o google-services.json pelas credenciais do EAS.
No Android 13+ (API 33) é necessário pedir a permissão POST_NOTIFICATIONS em runtime. Sem ela, o token é retornado, porém nenhuma notificação aparece.
Canais de notificação (Notifications.setNotificationChannelAsync) são obrigatórios no Android 8+, e cada canal define som, vibração, importância e badge de forma imutável após criado.
Envie mensagens do backend via expo-server-sdk, respeite o limite de 100 mensagens por chamada e sempre trate DeviceNotRegistered desativando tokens inválidos.
Para background handling, use Notifications.setNotificationHandler em foreground e TaskManager.defineTask com mensagens data-only para lógica em background no Android.
Pré-requisitos e limites do Expo Go em 2026
Antes de tocar em uma linha de código, entenda o que mudou. Desde o Expo SDK 53 o aplicativo Expo Go deixou de ser um cliente válido para push notifications remotas. Se você executar expo start e abrir no Expo Go, o getExpoPushTokenAsync devolve o erro "Must use physical device and development build to receive push notifications". A razão é simples: para autenticar contra APNs/FCM v1 o binário precisa carregar credenciais do seu projeto, e o Expo Go compartilha um bundle genérico.
Na prática, você vai precisar de três coisas para testar pushes em 2026:
Um development build gerado com eas build --profile development (ou um build de produção). Simulador iOS não recebe remotas, precisa de dispositivo físico. O emulador Android com Google Play Services até recebe, mas o dispositivo real elimina qualquer variável.
Credenciais configuradas no EAS Credentials: chave de APNs .p8 para iOS e arquivo google-services.json com FCM v1 habilitado para Android.
Um projectId vinculado à sua conta Expo. O token push v2 tem o formato ExponentPushToken[xxxxxxxx] e só é emitido se getExpoPushTokenAsync({ projectId }) conseguir resolver o project ID a partir de app.json (extra.eas.projectId).
Você também precisa do Node 20+ se for enviar mensagens do backend com expo-server-sdk. Para uma visão geral de builds e distribuição, veja nosso guia de EAS Update e OTA no React Native e Expo. O mesmo pipeline EAS gera os builds que você usará para testar as notificações.
Instalação e configuração do expo-notifications
Instale o pacote com npx expo. Assim ele já resolve a versão compatível com o seu SDK:
No Expo SDK 54 (setembro de 2026) a versão instalada é expo-notifications ~0.32.x. O pacote expo-device permite verificar se estamos em um dispositivo físico, e expo-constants expõe o projectId a partir de app.json.
Em seguida, declare o plugin em app.json. É ele quem injeta o entitlement de Push no iOS, o serviço FCM no AndroidManifest.xml e o ícone monocromático do Android:
O ícone monocromático (icon) é obrigatório no Android 8+. Sem ele, a notificação aparece como um quadrado branco (aprendi isso da pior forma, num TestFlight de sexta à noite). Recomenda-se um PNG 96x96 transparente com forma preenchida em branco.
Configurando FCM v1 no Android e APNs no iOS
Esse é o passo em que mais projetos travam. O Firebase Cloud Messaging Legacy HTTP API foi descontinuado em 20 de junho de 2024. Qualquer token enviado hoje sem FCM v1 recebe MismatchSenderId ou InvalidCredentials. O Expo Push Service já migrou para FCM v1 desde o SDK 51, mas você continua responsável por subir as credenciais corretas.
Passo a passo Android (FCM v1)
No Firebase Console, crie ou selecione o projeto. Adicione um app Android usando o mesmo package declarado em app.json.
Faça o download do google-services.json e salve na raiz do projeto (mesmo diretório de app.json). Adicione o caminho em expo.android.googleServicesFile.
Em Configurações do projeto → Contas de serviço → Firebase Admin SDK, gere uma nova chave privada. Isso baixa um JSON. Não commite no repositório.
Suba a chave para o EAS: eas credentials → Android → "Manage your Google Service Account Key for Push Notifications (FCM V1)" → "Upload a new service account key".
Passo a passo iOS (APNs)
No Apple Developer Portal, crie uma chave APNs .p8 com o serviço Apple Push Notifications service (APNs) habilitado.
Anote o Key ID e o Team ID. A chave .p8 só pode ser baixada uma vez.
Ative o capability Push Notifications no App ID (o EAS faz isso automaticamente quando o plugin está declarado).
Rode eas credentials → iOS → "Manage push notifications" → "Set up your project to use Push Notifications" para fazer upload da chave.
Solicitando permissões e obtendo o Expo push token
O código de registro precisa fazer três coisas em ordem: verificar se estamos em dispositivo físico, pedir permissão ao usuário e chamar getExpoPushTokenAsync. O snippet abaixo é o padrão que uso em produção. Copie e cole:
import * as Device from 'expo-device';
import * as Notifications from 'expo-notifications';
import Constants from 'expo-constants';
import { Platform } from 'react-native';
export async function registerForPushNotificationsAsync(): Promise<string | null> {
if (!Device.isDevice) {
console.warn('Push notifications requerem dispositivo físico.');
return null;
}
// Android 8+ exige um canal padrão antes do primeiro token.
if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'Padrão',
importance: Notifications.AndroidImportance.DEFAULT,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#0F172A',
});
}
const { status: existing } = await Notifications.getPermissionsAsync();
let finalStatus = existing;
if (existing !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync({
ios: {
allowAlert: true,
allowBadge: true,
allowSound: true,
allowProvisional: false,
},
});
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.warn('Usuário negou permissão de notificações.');
return null;
}
const projectId =
Constants.expoConfig?.extra?.eas?.projectId ??
Constants.easConfig?.projectId;
if (!projectId) {
throw new Error('projectId ausente em app.json (extra.eas.projectId).');
}
const { data: token } = await Notifications.getExpoPushTokenAsync({
projectId,
});
return token; // ExponentPushToken[xxxxxxxxxxxx]
}
No Android 13+ (API 33) a chamada requestPermissionsAsync gera o prompt runtime da permissão POST_NOTIFICATIONS. Se o usuário negar, o token continua sendo emitido, porém nenhuma notificação será exibida. Sempre trate finalStatus !== 'granted' como "desabilitado". No iOS, o Provisional Authorization (allowProvisional: true) permite entregar notificações silenciosamente ao Notification Center sem alerta. É útil para o primeiro contato, mas atrapalha engagement se abusado.
Canais de notificação no Android 8+
Canais são obrigatórios no Android 8.0 (API 26) e superiores. Cada canal representa uma categoria de notificação (por exemplo, "Mensagens", "Promoções", "Alertas críticos") e o usuário pode desabilitar canais individualmente nas configurações do sistema. As propriedades de um canal (som, vibração, importância, badge) não podem ser modificadas depois de criado. Para alterar, você precisa deletar o canal e criar outro com nome novo.
Ao enviar do backend, especifique o canal com o campo channelId no payload. Assim o usuário consegue silenciar promoções sem perder mensagens críticas. Se você usa canais dinâmicos por conversa (comum em apps de mensageria), crie o canal on-demand e reutilize o mesmo identifier em pushes subsequentes. O Android agrupa automaticamente.
Enviando push do backend com expo-server-sdk
O envio pelo Expo Push Service é feito via HTTP POST para https://exp.host/--/api/v2/push/send. Você pode chamar direto com fetch, mas o expo-server-sdk-node cuida de chunking (limite de 100 mensagens por request), backoff em 429 e leitura de tickets/receipts.
Depois de 15 minutos, consulte os receipts pelo ticketId. É lá que erros como DeviceNotRegistered (desinstalado), MessageTooBig (payload > 4KB) e MessageRateExceeded aparecem:
const ticketIds = tickets
.filter((t): t is Extract<ExpoPushTicket, { status: 'ok' }> => t.status === 'ok')
.map((t) => t.id);
const receiptChunks = expo.chunkPushNotificationReceiptIds(ticketIds);
for (const chunk of receiptChunks) {
const receipts = await expo.getPushNotificationReceiptsAsync(chunk);
for (const [id, receipt] of Object.entries(receipts)) {
if (receipt.status === 'error') {
if (receipt.details?.error === 'DeviceNotRegistered') {
await disableTokenInDb(id); // remova do seu DB
}
}
}
}
Tratando notificações em foreground e background
Diferente do que muitos esperam, notificações não são exibidas automaticamente quando o app está em foreground. Você precisa registrar um handler que decide o que fazer com o payload recebido. Essa chamada normalmente vai em App.tsx ou em um _layout.tsx raiz do Expo Router:
Os campos shouldShowBanner e shouldShowList substituíram o antigo shouldShowAlert a partir do expo-notifications ~0.30 (SDK 53). O banner é o pop-up transitório, e a list é o Notification Center persistente. No Android, essas flags viram um heads-up se a importância do canal for HIGH.
Para reagir a taps do usuário (abrir uma tela específica quando ele toca na notificação), combine addNotificationResponseReceivedListener com o Expo Router:
import { useEffect } from 'react';
import { router } from 'expo-router';
import * as Notifications from 'expo-notifications';
export function useNotificationTap() {
useEffect(() => {
const sub = Notifications.addNotificationResponseReceivedListener((response) => {
const data = response.notification.request.content.data as { screen?: string; id?: string };
if (data?.screen === 'chat' && data.id) {
router.push(`/chat/${data.id}`);
}
});
return () => sub.remove();
}, []);
}
Para background handling puro no Android (executar código quando chega uma data-only message sem UI), registre uma task com TaskManager, a mesma abordagem descrita no nosso guia de tarefas em segundo plano com expo-background-task. No iOS, background remote notifications exigem content-available: 1 no payload APNs e a flag UIBackgroundModes: ["remote-notification"]. Vale lembrar que o iOS ainda pode adiar ou descartar a entrega se o dispositivo estiver no Low Power Mode.
Notificações locais e agendadas
Nem toda notificação precisa passar por um servidor. Notifications.scheduleNotificationAsync agenda notificações locais (funciona em Expo Go também) para lembretes, timers e reengajamento offline:
No SDK 54, o formato do trigger mudou. Em vez de passar objetos crus com hour/minute no root, você especifica um type discriminado (CALENDAR, TIME_INTERVAL, DATE, DAILY, WEEKLY, MONTHLY, YEARLY). Migrações do SDK 51/52 costumam quebrar aqui, então confira a coluna "Trigger" do changelog de expo-notifications.
Para categorias com ações (botões inline como "Responder" ou "Arquivar"), registre categorias com setNotificationCategoryAsync e referencie o categoryIdentifier no payload. É a mesma API usada pelo Expo Router quando você combina push com deep links protegidos, combinação natural com a arquitetura descrita em nossa migração para a Nova Arquitetura do React Native, onde os TurboModules do expo-notifications reduzem o overhead da bridge legada em cerca de 30%.
Por que minhas notificações não chegam?
Sete causas cobrem 90% dos casos de push que "não chegam". Percorra a lista antes de abrir issue no repositório. Eu mesmo já perdi uma tarde inteira num app de produção por causa da linha 3 dessa tabela (chave APNs de sandbox subida como produção).
Sintoma
Causa provável
Como corrigir
Erro Must use physical device...
Rodando em Expo Go ou simulador iOS
Gerar development build com eas build --profile development
Token gerado, mas nenhum push chega no Android
Permissão POST_NOTIFICATIONS negada ou canal desabilitado
Verificar Settings do sistema; recriar canal com nome novo
Receipt retorna MismatchSenderId
google-services.json antigo ou de outro projeto Firebase
Baixar novo google-services.json e rodar eas build novamente
Push chega no dev build mas some em produção
Chave APNs de sandbox usada em Release
Reenviar .p8 no EAS Credentials, marcar "Production"
Notificação silenciosa (sem som/banner)
Handler não chamado em foreground
Registrar setNotificationHandler antes do primeiro render
Erro DeviceNotRegistered
App desinstalado ou token expirado
Remover token do banco e reemitir no próximo login
Notificação chega no debug mas não em release
ProGuard removendo classes do FCM
Manter regras oficiais do expo-notifications no proguard-rules.pro
Um debug rápido: envie um push de teste pela Expo Push Tool, cole o token, envie e observe o response. Se retornar ticket.status === 'ok' mas nenhuma notificação aparece, o problema está no cliente (canal/permissão). Se retornar erro, o problema está nas credenciais.
Perguntas frequentes
Push notifications funcionam no Expo Go em 2026?
Não. Desde o Expo SDK 53 (2025) o Expo Go deixou de emitir tokens Expo Push válidos para mensagens remotas. Notificações locais e agendadas (scheduleNotificationAsync) continuam funcionando no Expo Go, mas para push remoto você precisa de um development build gerado via eas build --profile development.
Preciso migrar para FCM v1 se uso o Expo Push Service?
O Expo já usa FCM v1 automaticamente desde o SDK 51. Você só precisa garantir que o google-services.json foi gerado a partir de um projeto Firebase com a API HTTP v1 habilitada, e que a Service Account key foi feita upload no EAS Credentials. O FCM Legacy foi desligado em 20 de junho de 2024.
Qual a diferença entre notificações locais e remotas?
Locais são agendadas pelo próprio dispositivo (sem servidor) via scheduleNotificationAsync, ótimas para lembretes offline. Remotas partem do seu backend, passam pelo Expo Push Service e chegam via APNs/FCM v1. São usadas para chats, alertas e conteúdo dinâmico.
Como pedir permissão de notificação no Android 13?
A partir do Android 13 (API 33) você precisa chamar Notifications.requestPermissionsAsync() em runtime. A instalação sozinha não concede permissão. O prompt do sistema aparece apenas uma vez; se o usuário negar, direcione-o para as configurações do sistema com Linking.openSettings().
Qual o limite de tamanho do payload no expo-notifications?
O payload total (incluindo title, body e data) não pode exceder 4KB por mensagem, limite compartilhado por APNs e FCM v1. Você pode enviar até 100 mensagens por request ao Expo Push Service; use expo.chunkPushNotifications para dividir automaticamente.
Como testar push notifications sem publicar o app?
Use a Expo Push Tool no navegador: cole o ExponentPushToken[...] gerado pelo dispositivo, escreva título/corpo e envie. Alternativamente, execute curl -X POST https://exp.host/--/api/v2/push/send com o mesmo payload JSON.
Guia prático de EAS Update em 2026: fingerprinting nativo, canais e branches, rollouts progressivos, rollback seguro, conformidade com a diretriz 4.5.4 da App Store e integração com GitHub Actions e Sentry.
Aprenda a implementar tarefas em segundo plano no React Native usando expo-background-task do Expo SDK 53. Guia prático com exemplos de código, configuração por plataforma e soluções para erros comuns.