.claude/skills/implement-firebase-notifications/SKILL.md
Implements or audits Firebase Cloud Messaging (FCM) push notifications for Flutter (iOS + Android). Covers NotificationService, APNs token relay, Info.plist, Runner.entitlements, AndroidManifest, background handler, DI registration, permission request, foreground display, topic subscription, and Firebase Console APNs key upload. Use when implementing push notifications, debugging notifications not arriving on iOS/Android, auditing notification setup, or adding FCM topic subscriptions. Activate even when the user says 'notifications are not working', 'push not arriving on iPhone', 'FCM token is null', 'set up Firebase Messaging', 'silent push notifications', 'send a notification to all users', 'notify users when something happens', or 'background notifications' without explicitly mentioning FCM, APNs, or firebase_messaging.
npx skillsauth add andrelucassvt/CleanMacForDevsWeb implement-firebase-notificationsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Implementa ou audita o fluxo completo de push notifications via Firebase Cloud Messaging (FCM) seguindo a arquitetura do projeto.
FirebaseAppDelegateProxyEnabled = false no Info.plist — REMOVA a chave se existir. Com o default (true), o Firebase faz swizzling automático do token APNs.registerForRemoteNotifications manual nem delegates de UNUserNotificationCenter.@pragma('vm:entry-point') para sobreviver ao tree-shaking em release; obrigatório await Firebase.initializeApp() no início.FirebaseAppDelegateProxyEnabled = false.FirebaseMessaging.instance e NotificationService → registerLazySingleton. Inicialize no AppInitializer após setupDependencies.┌──────────┐ initialize() ┌─────────────────────┐ requestPermission ┌──────────┐
│ App │ ─────────────→ │ NotificationService │ ──────────────────→ │ iOS / │
│Initializer│ │ (firebase_messaging) │ │ Android │
└──────────┘ └─────────────────────┘ └──────────┘
│ │
│ getToken() │
↓ │
┌─────────────┐ APNs token │
│ FCM Token │ ←────────────────────────────┘
└─────────────┘
│
↓
Firebase Console → envia push → dispositivo
Fluxo iOS (ponto crítico):
AppDelegate
├─ registerForRemoteNotifications() → solicita token APNs ao sistema
├─ didRegisterForRemoteNotificationsWithDeviceToken → recebe token APNs do sistema
│ └─ Messaging.messaging().apnsToken = deviceToken → repassa ao Firebase
└─ Firebase usa token APNs para gerar FCM token
Antes de implementar, faça TODAS as perguntas abaixo em uma única mensagem:
1. O app já tem Firebase configurado? (firebase_core, google-services.json, GoogleService-Info.plist)
- SIM → pular configuração base
- NÃO → configurar Firebase primeiro
2. Precisa exibir notificações em foreground? (app aberto)
- SIM → usar flutter_local_notifications ou mostrar SnackBar/Dialog
- NÃO → apenas log/silencioso
3. Precisa de tópicos (topics) para segmentação?
- SIM → quais tópicos? (ex: "news", "promotions", "alerts")
- NÃO → apenas notificação geral via token
4. O que deve acontecer quando o usuário toca na notificação?
- Abrir tela específica (deep link) → qual rota?
- Apenas abrir o app na tela principal
5. Precisa enviar o FCM token para um back-end?
- SIM → qual endpoint?
- NÃO → apenas local
6. O FirebaseAppDelegateProxyEnabled está como `false` no Info.plist?
(Se sim, REMOVER a chave — nunca deve ser false neste projeto)
Verifique CADA item abaixo. Se qualquer um estiver faltando, as notificações NÃO funcionam no iOS:
.p8) criada — ou certificado APNs (.p12)
.p12 (development + production)Remote notifications marcado<!-- Background modes (obrigatório) -->
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
⛔ REGRA: NUNCA use FirebaseAppDelegateProxyEnabled = false.
Se essa chave existir no Info.plist, remova-a. O valor padrão (true) permite que o Firebase faça o swizzling automático do token APNs, eliminando a necessidade de repasse manual no AppDelegate. Usar false é a causa #1 de push notifications não funcionarem no iOS.
<!-- ❌ NUNCA adicione isso -->
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>
<!-- ✅ Remova a chave ou, se precisar ser explícito: -->
<key>FirebaseAppDelegateProxyEnabled</key>
<true/>
<key>aps-environment</key>
<string>development</string>
Nota: Para builds de Release/TestFlight/App Store, o Xcode automaticamente substitui
developmentporproductiondurante o archive. Manterdevelopmentno source.
ios/Runner/GoogleService-Info.plistPRODUCT_BUNDLE_IDENTIFIER do projeto<!-- Permissão (Android 13+ / API 33+) -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- Dentro de <application>: canal padrão de notificação -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="high_importance_channel"/>
android/app/google-services.jsonpackage_name no arquivo coincide com o applicationId do build.gradlecom.google.gms.google-services aplicadofirebase-messaging resolvida via Flutter plugin (pubspec)Como FirebaseAppDelegateProxyEnabled deve ser true (ou ausente), o Firebase faz o swizzling automático. O AppDelegate fica mínimo:
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Não é necessário:
import FirebaseMessaging / import UserNotificationsapplication.registerForRemoteNotifications()didRegisterForRemoteNotificationsWithDeviceTokenUNUserNotificationCenter.current().delegate = selfO Firebase cuida de tudo automaticamente via method swizzling.
// lib/common/services/notification_service.dart
import 'dart:developer';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
/// Handler de mensagens em background/terminated (deve ser top-level).
@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
log('NotificationService: background message received: ${message.messageId}');
}
class NotificationService {
NotificationService(this._messaging);
final FirebaseMessaging _messaging;
Future<void> initialize() async {
// Registra o handler de background
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
// Solicita permissão (iOS / Android 13+)
final settings = await _messaging.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
);
log(
'NotificationService: permission status: '
'${settings.authorizationStatus}',
);
// Foreground messages
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
log(
'NotificationService: foreground message: ${message.messageId} | '
'title: ${message.notification?.title}',
);
// TODO: exibir notificação local se necessário
});
// App aberto a partir de notificação (background → foreground)
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
log(
'NotificationService: opened from notification: '
'${message.messageId}',
);
// TODO: navegar para tela específica se necessário
});
// App aberto a partir de notificação (terminated → foreground)
final initialMessage = await _messaging.getInitialMessage();
if (initialMessage != null) {
log(
'NotificationService: launched from notification: '
'${initialMessage.messageId}',
);
// TODO: navegar para tela específica se necessário
}
// Log do token em debug
if (!kReleaseMode) {
final token = await getToken();
log('NotificationService: FCM token: $token');
}
}
/// Retorna o token FCM do dispositivo.
Future<String?> getToken() async {
try {
return await _messaging.getToken();
} catch (e) {
log('NotificationService: failed to get token: $e');
return null;
}
}
/// Escuta quando o token FCM é renovado.
void onTokenRefresh(void Function(String token) onToken) {
_messaging.onTokenRefresh.listen(onToken);
}
/// Inscreve o dispositivo em um tópico FCM.
Future<void> subscribeToTopic(String topic) async {
await _messaging.subscribeToTopic(topic);
log('NotificationService: subscribed to topic: $topic');
}
/// Remove inscrição de um tópico FCM.
Future<void> unsubscribeFromTopic(String topic) async {
await _messaging.unsubscribeFromTopic(topic);
log('NotificationService: unsubscribed from topic: $topic');
}
}
// No app_injector.dart
import 'package:firebase_messaging/firebase_messaging.dart';
// FirebaseMessaging instance
inject.registerLazySingleton<FirebaseMessaging>(
() => FirebaseMessaging.instance,
);
// NotificationService
inject.registerLazySingleton<NotificationService>(
() => NotificationService(inject()),
);
// No app_initializer.dart, APÓS setupDependencies:
await AppInjector.inject.get<NotificationService>().initialize();
dependencies:
firebase_core: ^3.x.x
firebase_messaging: ^16.x.x
Rodar o app em debug → verificar no console:
NotificationService: permission status: authorized (iOS) ou authorized (Android)NotificationService: FCM token: <token> — se for null, há problema na configuraçãoFirebase Console → Messaging → New campaign → Test message:
| Sintoma | Causa provável | Solução |
|---|---|---|
| FCM token é null | Token APNs não chegou ao Firebase | Verificar se FirebaseAppDelegateProxyEnabled existe como false no Info.plist — se sim, REMOVER a chave |
| FCM token existe mas push não chega | APNs key não configurada no Firebase Console | Enviar .p8 com Key ID e Team ID corretos |
| permission status: denied | Usuário negou permissão | Redirecionar para Settings do iOS |
| Funciona em debug mas não em TestFlight | aps-environment incorreto ou provisioning profile sem Push | Verificar entitlements e capability no Xcode |
| Messaging.messaging() crash | Falta import FirebaseMessaging no AppDelegate | Adicionar import |
| Sintoma | Causa provável | Solução |
|---|---|---|
| FCM token é null | Google Play Services ausente ou desatualizado | Verificar no emulador/device |
| Push chega mas sem som/vibração | Canal de notificação sem importância alta | Verificar default_notification_channel_id |
| permission status: denied (Android 13+) | Permissão POST_NOTIFICATIONS não concedida | Solicitar via requestPermission() |
FirebaseAppDelegateProxyEnabled = false no Info.plist
null no iOS → push NUNCA chegatrue já é o correto). NUNCA use false neste projetoAPNs key não enviada no Firebase Console
.p8 em Project Settings → Cloud Messaging → AppleCapability Push Notifications não adicionada no Xcode
aps-environment → iOS rejeita o registro@pragma('vm:entry-point') ausente no background handler
@pragma('vm:entry-point') antes da funçãoFirebase.initializeApp() ausente no background handler
await Firebase.initializeApp() no início do handlerTestar push apenas com simulador iOS
Última atualização: 28 de março de 2026
testing
Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, edit, or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy. Activate even when the user says 'create a skill for X', 'the skill is not triggering', 'improve this skill description', 'the agent is not using the skill', 'add a skill to teach the agent how to do X', 'this skill is wrong', or 'update the skill' without explicitly mentioning evals or benchmark.
development
Implements Flutter reusable widgets following the project architecture. Use whenever creating or modifying widgets in presentation/<feature>/widgets/, presentation/<feature>/content/, or common/widgets/. Covers StatelessWidget vs StatefulWidget decision, Entity as parameter, i18n, dispose, componentization rules, and when to access the Cubit via context.read. Activate even when the user says 'extract this to a widget', 'create a list item widget', 'build a reusable card', 'factor out this UI block', 'create a component for this', or 'this View is getting too big' without explicitly mentioning StatelessWidget or reusable components.
tools
Implements Flutter View screens following the project architecture. Use whenever creating or modifying a View (StatefulWidget + Cubit + BlocBuilder), adding a new screen, wiring up BlocBuilder/BlocConsumer/BlocListener, setting up SafeArea, or navigating from the View. Covers State, Cubit, View file, route, DI registration, and common mistakes. Activate even when the user just says "create a screen" or "add a new page", without explicitly mentioning Cubit or BLoC.
testing
Implements Flutter Cubit and State (View Model layer) following the project architecture. Use whenever creating or modifying a Cubit or State class, adding an async method to a Cubit, handling form submission or validation, implementing debounce search, managing loading/error/navigation states, or wiring a Cubit to a Repository or StorageService. Covers sealed States, async patterns with Result<T>, CRUD Cubits, local persistence via StorageService, navigation states, debounce, and common mistakes. Activate even when the user says "add a method", "handle the loading state", or "save locally" without explicitly mentioning Cubit or BLoC.