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.

expo-notifications 2026: Push FCM v1 + APNs

Atualizado: 15 de setembro de 2026

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:

  1. 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.
  2. Credenciais configuradas no EAS Credentials: chave de APNs .p8 para iOS e arquivo google-services.json com FCM v1 habilitado para Android.
  3. 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:

npx expo install expo-notifications expo-device expo-constants

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:

{
  "expo": {
    "name": "MinhaApp",
    "slug": "minha-app",
    "plugins": [
      [
        "expo-notifications",
        {
          "icon": "./assets/notification-icon.png",
          "color": "#0F172A",
          "defaultChannel": "default",
          "sounds": ["./assets/notification.wav"]
        }
      ]
    ],
    "ios": {
      "bundleIdentifier": "com.suaempresa.minhaapp",
      "infoPlist": {
        "UIBackgroundModes": ["remote-notification"]
      }
    },
    "android": {
      "package": "com.suaempresa.minhaapp",
      "googleServicesFile": "./google-services.json"
    }
  }
}

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)

  1. No Firebase Console, crie ou selecione o projeto. Adicione um app Android usando o mesmo package declarado em app.json.
  2. 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.
  3. 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.
  4. 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)

  1. No Apple Developer Portal, crie uma chave APNs .p8 com o serviço Apple Push Notifications service (APNs) habilitado.
  2. Anote o Key ID e o Team ID. A chave .p8 só pode ser baixada uma vez.
  3. Ative o capability Push Notifications no App ID (o EAS faz isso automaticamente quando o plugin está declarado).
  4. 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.

await Notifications.setNotificationChannelAsync('messages', {
  name: 'Mensagens',
  description: 'Mensagens diretas de outros usuários.',
  importance: Notifications.AndroidImportance.HIGH,
  sound: 'notification.wav',
  vibrationPattern: [0, 250, 250, 250],
  lightColor: '#3B82F6',
  lockscreenVisibility: Notifications.AndroidNotificationVisibility.PUBLIC,
  bypassDnd: false,
});

await Notifications.setNotificationChannelAsync('promotions', {
  name: 'Promoções',
  importance: Notifications.AndroidImportance.LOW,
  showBadge: false,
});

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.

// server/pushSender.ts
import { Expo, ExpoPushMessage, ExpoPushTicket } from 'expo-server-sdk';

const expo = new Expo({
  accessToken: process.env.EXPO_ACCESS_TOKEN, // opcional, mas recomendado
  useFcmV1: true, // default em 2026
});

export async function sendPush(
  tokens: string[],
  title: string,
  body: string,
  data: Record<string, unknown> = {}
): Promise<ExpoPushTicket[]> {
  const messages: ExpoPushMessage[] = tokens
    .filter((t) => Expo.isExpoPushToken(t))
    .map((to) => ({
      to,
      sound: 'default',
      title,
      body,
      data,
      channelId: 'messages',
      priority: 'high',
      ttl: 3600,
    }));

  const chunks = expo.chunkPushNotifications(messages);
  const tickets: ExpoPushTicket[] = [];

  for (const chunk of chunks) {
    try {
      const receipts = await expo.sendPushNotificationsAsync(chunk);
      tickets.push(...receipts);
    } catch (err) {
      console.error('Falha ao enviar chunk:', err);
    }
  }

  return tickets;
}

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:

Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowBanner: true,
    shouldShowList: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
  }),
});

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:

await Notifications.scheduleNotificationAsync({
  content: {
    title: 'Hora de treinar',
    body: 'Você marcou 30 minutos de exercício para agora.',
    sound: 'default',
    data: { screen: 'workout' },
  },
  trigger: {
    type: Notifications.SchedulableTriggerInputTypes.CALENDAR,
    hour: 18,
    minute: 30,
    repeats: true,
  },
});

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).

SintomaCausa provávelComo corrigir
Erro Must use physical device...Rodando em Expo Go ou simulador iOSGerar development build com eas build --profile development
Token gerado, mas nenhum push chega no AndroidPermissão POST_NOTIFICATIONS negada ou canal desabilitadoVerificar Settings do sistema; recriar canal com nome novo
Receipt retorna MismatchSenderIdgoogle-services.json antigo ou de outro projeto FirebaseBaixar novo google-services.json e rodar eas build novamente
Push chega no dev build mas some em produçãoChave APNs de sandbox usada em ReleaseReenviar .p8 no EAS Credentials, marcar "Production"
Notificação silenciosa (sem som/banner)Handler não chamado em foregroundRegistrar setNotificationHandler antes do primeiro render
Erro DeviceNotRegisteredApp desinstalado ou token expiradoRemover token do banco e reemitir no próximo login
Notificação chega no debug mas não em releaseProGuard removendo classes do FCMManter 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.

Sobre o Autor Editorial Team

Our team of expert writers and editors.