feat: implement FCM push notifications via firebase_messaging

- Add firebase_core + firebase_messaging to pubspec.yaml
- Initialize Firebase in main.dart before runApp
- NotificationBloc: request permission, get FCM token, register with
  backend (PATCH /users/me/fcm-token), show local notification on
  foreground messages, refresh token on rotation
- app_view.dart: call initializePushNotifications() on authenticated state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-18 12:43:41 -05:00
co-authored by Claude Sonnet 4.6
parent 5e92d2b908
commit 79f350e85f
4 changed files with 79 additions and 4 deletions
+7 -1
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:prosappco/blocs/notification_bloc/notification_bloc.dart';
import 'package:prosappco/screens/authentication/welcome_screen.dart';
import 'package:prosappco/screens/home/home_screen.dart';
import 'package:prosappco/screens/splash/splash_screen.dart';
@@ -36,7 +37,12 @@ class MyAppView extends StatelessWidget {
secondaryContainer: Colors.red,
),
),
home: BlocBuilder<AuthenticationBloc, AuthenticationState>(
home: BlocConsumer<AuthenticationBloc, AuthenticationState>(
listener: (context, state) {
if (state.status == AuthenticationStatus.authenticated) {
context.read<NotificationBloc>().initializePushNotifications();
}
},
builder: (context, state) {
return getScreen(state);
},
@@ -1,14 +1,22 @@
import 'dart:convert';
import 'dart:developer';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:user_repository/user_repository.dart';
import 'package:prosappco/constansts.dart' show API_BASE_URL;
part 'notification_event.dart';
part 'notification_state.dart';
// Stub: Firebase messaging removed. Background handler is no longer needed.
Future<void> firebaseMessagingBackgroundHandler(dynamic message) async {}
// Handles FCM messages when app is terminated/background
@pragma('vm:entry-point')
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// Local notification shown by the system tray automatically for data-only
// messages; nothing extra needed here.
}
class NotificationBloc extends Bloc<NotificationEvent, NotificationState> {
final Future<void> Function() requestLocalNotificationPermission;
@@ -31,6 +39,60 @@ class NotificationBloc extends Bloc<NotificationEvent, NotificationState> {
emit(state.copyWith(status: event.status));
}
// Call once after login to register the FCM token and listen for foreground messages.
Future<void> initializePushNotifications() async {
try {
final messaging = FirebaseMessaging.instance;
final settings = await messaging.requestPermission();
add(NotificationStatusChanged(
settings.authorizationStatus == AuthorizationStatus.authorized ||
settings.authorizationStatus == AuthorizationStatus.provisional
? AuthorizationStatus.authorized
: AuthorizationStatus.denied,
));
final token = await messaging.getToken();
if (token != null) await _registerToken(token);
// Refresh token (e.g. after reinstall)
messaging.onTokenRefresh.listen(_registerToken);
// Show local notification when app is in foreground
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
final n = message.notification;
if (n != null) {
showLocalNotification(
id: message.hashCode,
title: n.title,
body: n.body,
data: jsonEncode(message.data),
);
}
});
} catch (e) {
log('NotificationBloc.initializePushNotifications error: $e');
}
}
Future<void> _registerToken(String token) async {
try {
final prefs = await SharedPreferences.getInstance();
final jwt = prefs.getString('token');
if (jwt == null) return;
await http.patch(
Uri.parse('$API_BASE_URL/users/me/fcm-token'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $jwt',
},
body: jsonEncode({'token': token}),
);
log('FCM token registered');
} catch (e) {
log('NotificationBloc._registerToken error: $e');
}
}
void requestPermission() async {
try {
await requestLocalNotificationPermission();
+5
View File
@@ -1,3 +1,5 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -16,6 +18,9 @@ import 'simple_bloc_observer.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler);
// Inicializacion de notificaciones locales
await LocalNotifications.initializeLocalNotifications();
+2
View File
@@ -10,6 +10,8 @@ environment:
dependencies:
animate_do: ^3.0.2
firebase_core: ^3.0.0
firebase_messaging: ^15.0.0
animated_splash_screen: ^1.3.0
chat_repository:
path: packages/chat_repository