FCM push Notifications

This commit is contained in:
2026-02-25 23:37:08 +08:00
parent 1807dca57d
commit 6ccf820438
9 changed files with 188 additions and 47 deletions
+26 -26
View File
@@ -22,17 +22,12 @@ import 'services/notification_bridge.dart';
/// Handle messages received while the app is terminated or in background.
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
// initialize plugin in background isolate
await NotificationService.initialize();
final notification = message.notification;
if (notification != null) {
NotificationService.show(
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
title: notification.title ?? 'Notification',
body: notification.body ?? '',
payload: message.data['payload'],
);
}
// Do minimal work in background isolate. Avoid initializing Flutter
// plugins here (e.g. flutter_local_notifications) as that can hang
// the background isolate and interfere with subsequent launches.
// If you need to persist data from a background message, perform a
// lightweight HTTP call or write to a background-capable DB.
return;
}
Future<void> main() async {
@@ -58,22 +53,9 @@ Future<void> main() async {
await Supabase.initialize(url: supabaseUrl, anonKey: supabaseAnonKey);
// ensure token saved right away if already signed in
// ensure token saved shortly after startup if already signed in.
// Run this after runApp so startup is not blocked by network/token ops.
final supaClient = Supabase.instance.client;
String? initialToken;
if (!kIsWeb) {
try {
initialToken = await FirebaseMessaging.instance.getToken();
} catch (e) {
debugPrint('FCM getToken failed: $e');
initialToken = null;
}
if (initialToken != null && supaClient.auth.currentUser != null) {
debugPrint('initial FCM token for signed-in user: $initialToken');
final ctrl = NotificationsController(supaClient);
await ctrl.registerFcmToken(initialToken);
}
}
// listen for auth changes to register/unregister token accordingly
supaClient.auth.onAuthStateChange.listen((data) async {
@@ -152,6 +134,24 @@ Future<void> main() async {
),
),
);
// Post-startup: register current FCM token without blocking UI.
if (!kIsWeb) {
Future.microtask(() async {
try {
final token = await FirebaseMessaging.instance.getToken().timeout(
const Duration(seconds: 10),
);
if (token != null && supaClient.auth.currentUser != null) {
debugPrint('post-startup registering FCM token: $token');
final ctrl = NotificationsController(supaClient);
await ctrl.registerFcmToken(token);
}
} catch (e) {
debugPrint('post-startup FCM token registration failed: $e');
}
});
}
}
class NotificationSoundObserver extends ProviderObserver {
+20
View File
@@ -2,6 +2,9 @@ import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'notifications_provider.dart';
import '../utils/device_id.dart';
import 'supabase_provider.dart';
@@ -64,6 +67,23 @@ class AuthController {
}
Future<void> signOut() {
// Attempt to unregister this device's FCM token before signing out.
return _doSignOut();
}
Future<void> _doSignOut() async {
try {
final userId = _client.auth.currentUser?.id;
if (userId != null) {
try {
final token = await FirebaseMessaging.instance.getToken();
if (token != null) {
final ctrl = NotificationsController(_client);
await ctrl.unregisterFcmToken(token);
}
} catch (_) {}
}
} catch (_) {}
return _client.auth.signOut();
}
}
+13 -3
View File
@@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../utils/device_id.dart';
import '../models/notification_item.dart';
import 'profile_provider.dart';
@@ -165,10 +166,16 @@ class NotificationsController {
Future<void> registerFcmToken(String token) async {
final userId = _client.auth.currentUser?.id;
if (userId == null) return;
final res = await _client.from('fcm_tokens').insert({
final deviceId = await DeviceId.getId();
// upsert using a unique constraint on (user_id, device_id) so a single
// device keeps its token updated without overwriting other devices.
final payload = {
'user_id': userId,
'device_id': deviceId,
'token': token,
});
'created_at': DateTime.now().toUtc().toIso8601String(),
};
final res = await _client.from('fcm_tokens').upsert(payload);
if (res == null) {
debugPrint(
'registerFcmToken: null response for user=$userId token=$token',
@@ -189,11 +196,14 @@ class NotificationsController {
Future<void> unregisterFcmToken(String token) async {
final userId = _client.auth.currentUser?.id;
if (userId == null) return;
final deviceId = await DeviceId.getId();
// Prefer to delete by device_id to avoid removing other devices' tokens.
final res = await _client
.from('fcm_tokens')
.delete()
.eq('user_id', userId)
.eq('token', token);
.eq('device_id', deviceId)
.or('token.eq.$token');
if (res == null) {
debugPrint(
'unregisterFcmToken: null response for user=$userId token=$token',
+23
View File
@@ -0,0 +1,23 @@
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
class DeviceId {
static const _key = 'tasq_device_id';
/// Returns a stable UUID for this installation. Generated once and persisted
/// in `SharedPreferences`.
static Future<String> getId() async {
final prefs = await SharedPreferences.getInstance();
var id = prefs.getString(_key);
if (id != null && id.isNotEmpty) return id;
id = const Uuid().v4();
await prefs.setString(_key, id);
return id;
}
/// For testing or explicit reset.
static Future<void> reset() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_key);
}
}