Push notifications em React Native: Firebase vs serviços nativos

1. Fundamentos das notificações push no ecossistema mobile

As notificações push são mensagens enviadas de um servidor para um aplicativo mobile, mesmo quando ele não está em execução. No ecossistema mobile, existem dois serviços principais de push: Apple Push Notification Service (APNs) para iOS e Firebase Cloud Messaging (FCM) para Android. O FCM também funciona como intermediário para APNs em dispositivos iOS.

A arquitetura básica envolve:
- Um provedor de serviços (servidor próprio ou Firebase) que envia a notificação
- Um serviço de push (APNs ou FCM) que recebe e encaminha a mensagem
- O dispositivo que exibe a notificação ao usuário

As notificações locais são geradas e exibidas pelo próprio aplicativo, sem depender de servidores externos. Já as notificações remotas dependem de infraestrutura externa para serem enviadas.

2. Firebase Cloud Messaging (FCM) no React Native

O Firebase oferece uma solução completa e integrada para push notifications. A configuração começa com a criação de um projeto no Firebase Console e a instalação das dependências necessárias.

Instalação das dependências:

npm install @react-native-firebase/app @react-native-firebase/messaging

Configuração básica no iOS (AppDelegate.m):

#import <Firebase.h>

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  [FIRApp configure];
  return [super application:application didFinishLaunchingWithOptions:launchOptions];
}

Solicitação de permissão e obtenção do token:

import messaging from '@react-native-firebase/messaging';

async function requestUserPermission() {
  const authStatus = await messaging().requestPermission();
  const enabled = 
    authStatus === messaging.AuthorizationStatus.AUTHORIZED ||
    authStatus === messaging.AuthorizationStatus.PROVISIONAL;

  if (enabled) {
    const token = await messaging().getToken();
    console.log('FCM Token:', token);
    // Enviar token para o backend
  }
}

Handlers para diferentes estados do app:

// Foreground
messaging().onMessage(async remoteMessage => {
  Alert.alert('Nova notificação', remoteMessage.notification.body);
});

// Background
messaging().setBackgroundMessageHandler(async remoteMessage => {
  console.log('Notificação em background:', remoteMessage);
});

// Quando o app é aberto pela notificação
messaging().onNotificationOpenedApp(remoteMessage => {
  navigation.navigate('Detalhes', { data: remoteMessage.data });
});

// Quando o app é aberto pela notificação (app fechado)
messaging().getInitialNotification().then(remoteMessage => {
  if (remoteMessage) {
    navigation.navigate('Detalhes', { data: remoteMessage.data });
  }
});

3. Serviços nativos: APNs e FCM sem Firebase

Para quem prefere controle total, é possível implementar push notifications usando apenas os serviços nativos, sem depender do Firebase SDK.

Configuração de APNs no iOS:

No Apple Developer Portal, é necessário:
1. Criar um Certificate Signing Request (CSR)
2. Gerar um certificado APNs no Apple Developer Console
3. Configurar o App ID com Push Notification capability
4. Instalar o certificado no servidor

Implementação direta de FCM no Android:

// AndroidManifest.xml
<service android:name=".MyFirebaseMessagingService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>

Serviço personalizado para receber notificações:

public class MyFirebaseMessagingService extends FirebaseMessagingService {
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // Tratar notificação recebida
        if (remoteMessage.getNotification() != null) {
            showNotification(remoteMessage.getNotification().getBody());
        }
    }

    @Override
    public void onNewToken(String token) {
        // Token renovado - enviar para o backend
        sendRegistrationToServer(token);
    }
}

4. Comparação prática: Firebase vs serviços nativos

Aspecto Firebase Serviços Nativos
Setup Plug-and-play, 30 minutos Horas de configuração
Manutenção Automática (Firebase cuida) Manual (certificados expiram)
Custo Gratuito (limite de 1M requisições/dia) Infraestrutura própria
Controle Limitado ao que Firebase oferece Total sobre o processo
Privacidade Dados passam pelo Google Controle total dos dados

Casos de uso ideais:
- Firebase: Startups, MVPs, aplicativos que não lidam com dados sensíveis
- Nativo: Aplicativos financeiros, de saúde, ou com requisitos de compliance

5. Tratamento de notificações em diferentes estados do app

O React Native oferece métodos específicos para lidar com notificações em cada estado do aplicativo.

Notificações em foreground:

// Exibir banner customizado
messaging().onMessage(async remoteMessage => {
  const { notification, data } = remoteMessage;

  // Exibir notificação local como fallback
  notifee.displayNotification({
    title: notification.title,
    body: notification.body,
    android: {
      channelId: 'default',
      pressAction: { id: 'default' }
    }
  });
});

Notificações em background e killed state:

// Navegação condicional baseada nos dados da notificação
messaging().onNotificationOpenedApp(remoteMessage => {
  const { type, id } = remoteMessage.data;

  switch(type) {
    case 'message':
      navigation.navigate('Chat', { conversationId: id });
      break;
    case 'order':
      navigation.navigate('OrderDetails', { orderId: id });
      break;
    default:
      navigation.navigate('Home');
  }
});

6. Sincronização de tokens e gerenciamento de inscrições

Gerenciar tokens de forma eficiente é crucial para o funcionamento das notificações.

Estrutura de dados no backend:

// Modelo de token
{
  userId: "user_123",
  tokens: [
    {
      token: "fcm_token_abc",
      platform: "android",
      deviceId: "device_456",
      createdAt: "2024-01-15T10:30:00Z",
      lastUsed: "2024-01-15T14:22:00Z"
    }
  ],
  topics: ["promocoes", "atualizacoes"]
}

Estratégia de limpeza de tokens inválidos:

// No servidor, ao receber erro de token inválido
async function handleInvalidToken(userId, invalidToken) {
  await db.collection('users').updateOne(
    { _id: userId },
    { $pull: { 'tokens': { token: invalidToken } } }
  );
}

// No cliente, quando token é renovado
messaging().onTokenRefresh(async newToken => {
  await api.updateToken({ 
    oldToken: currentToken, 
    newToken: newToken 
  });
  setCurrentToken(newToken);
});

7. Segurança e boas práticas em push notifications

Criptografia de payloads:

// Servidor: criptografar dados sensíveis
const encryptedData = encrypt(JSON.stringify(sensitiveData), secretKey);

const message = {
  token: userToken,
  data: {
    encrypted: encryptedData,
    iv: initializationVector
  },
  notification: {
    title: 'Notificação Segura',
    body: 'Toque para ver detalhes'
  }
};

Rate limiting e prevenção de spam:

// Middleware de rate limiting no servidor
const rateLimit = {
  windowMs: 60000, // 1 minuto
  max: 10, // máximo de notificações por minuto
  message: 'Muitas notificações enviadas. Tente novamente mais tarde.'
};

// Verificação de consentimento
function canSendNotification(userId, notificationType) {
  const userPreferences = getUserPreferences(userId);
  return userPreferences.notificationTypes.includes(notificationType);
}

8. Exemplo prático: implementação híbrida com fallback

Serviço unificado de notificações:

class NotificationService {
  async sendNotification(user, message) {
    try {
      // Tentar FCM primeiro
      await this.sendViaFCM(user.fcmToken, message);
    } catch (fcmError) {
      console.warn('FCM falhou, tentando APNs:', fcmError);

      try {
        // Fallback para APNs
        await this.sendViaAPNs(user.apnsToken, message);
      } catch (apnsError) {
        console.error('Ambos serviços falharam:', apnsError);

        // Adicionar à fila de retry
        await this.addToRetryQueue({
          userId: user.id,
          message: message,
          attempts: 1,
          maxAttempts: 3
        });
      }
    }
  }

  async sendViaFCM(token, message) {
    const response = await fetch('https://fcm.googleapis.com/fcm/send', {
      method: 'POST',
      headers: {
        'Authorization': `key=${process.env.FCM_SERVER_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        to: token,
        notification: {
          title: message.title,
          body: message.body
        },
        data: message.data
      })
    });

    if (!response.ok) throw new Error('FCM request failed');
    return response.json();
  }

  async sendViaAPNs(token, message) {
    // Implementação específica para APNs
    const apnsMessage = {
      aps: {
        alert: {
          title: message.title,
          body: message.body
        },
        sound: 'default',
        badge: 1
      },
      data: message.data
    };

    // Enviar para APNs usando certificado ou token JWT
    return await this.sendToAPNsServer(token, apnsMessage);
  }

  async processRetryQueue() {
    const failedMessages = await this.getFailedMessages();

    for (const failed of failedMessages) {
      if (failed.attempts < failed.maxAttempts) {
        await this.sendNotification(failed.userId, failed.message);
      } else {
        // Notificar administrador sobre falha persistente
        await this.notifyAdmin(failed);
      }
    }
  }
}

Testes em dispositivos reais:

// Simular cenários de teste
describe('NotificationService', () => {
  it('deve fazer fallback para APNs quando FCM falha', async () => {
    const service = new NotificationService();
    const user = {
      fcmToken: 'invalid_token',
      apnsToken: 'valid_apns_token'
    };

    const result = await service.sendNotification(user, {
      title: 'Teste',
      body: 'Mensagem de teste'
    });

    expect(result.service).toBe('apns');
  });

  it('deve adicionar à fila de retry quando ambos falham', async () => {
    const service = new NotificationService();
    const user = {
      fcmToken: 'invalid',
      apnsToken: 'invalid'
    };

    await service.sendNotification(user, { title: 'Test', body: 'Test' });

    const queue = await service.getRetryQueue();
    expect(queue.length).toBe(1);
    expect(queue[0].attempts).toBe(1);
  });
});

Conclusão

A escolha entre Firebase e serviços nativos para push notifications em React Native depende das necessidades específicas do projeto. Firebase oferece simplicidade e rapidez de implementação, ideal para startups e MVPs. Serviços nativos proporcionam controle total e privacidade, sendo mais adequados para aplicações enterprise. Uma abordagem híbrida, utilizando FCM como padrão e APNs como fallback, pode oferecer o melhor dos dois mundos, garantindo resiliência e flexibilidade.

Referências