* Push Notification Setup and attempt

* Office Ordering
* Allow editing of Task and Ticket Details after creation
This commit is contained in:
2026-02-24 21:06:46 +08:00
parent cc6fda0e79
commit 5979a04254
25 changed files with 1130 additions and 91 deletions
+102 -2
View File
@@ -1,18 +1,45 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'firebase_options.dart';
import 'providers/profile_provider.dart';
import 'models/profile.dart';
import 'app.dart';
import 'providers/notifications_provider.dart';
import 'utils/app_time.dart';
import 'utils/notification_permission.dart';
import 'services/notification_service.dart';
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'],
);
}
}
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// initialize Firebase before anything that uses messaging
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
await dotenv.load(fileName: '.env');
AppTime.initialize(location: 'Asia/Manila');
@@ -27,6 +54,29 @@ Future<void> main() async {
await Supabase.initialize(url: supabaseUrl, anonKey: supabaseAnonKey);
// ensure token saved right away if already signed in
final supaClient = Supabase.instance.client;
final initialToken = await FirebaseMessaging.instance.getToken();
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 {
final event = data.event;
final token = await FirebaseMessaging.instance.getToken();
debugPrint('auth state change $event, token=$token');
if (token == null) return;
final ctrl = NotificationsController(supaClient);
if (event == AuthChangeEvent.signedIn) {
await ctrl.registerFcmToken(token);
} else if (event == AuthChangeEvent.signedOut) {
await ctrl.unregisterFcmToken(token);
}
});
// on Android 13+ we must request POST_NOTIFICATIONS at runtime; without it
// notifications are automatically denied and cannot be reenabled from the
// system settings. The helper uses `permission_handler`.
@@ -37,6 +87,20 @@ Future<void> main() async {
// debugPrint('notification permission not granted');
}
// request FCM permission (iOS/Android13+) and handle foreground messages
await FirebaseMessaging.instance.requestPermission();
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
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'],
);
}
});
// initialize the local notifications plugin so we can post alerts later
await NotificationService.initialize(
onDidReceiveNotificationResponse: (response) {
@@ -50,16 +114,23 @@ Future<void> main() async {
},
);
// global navigator key used for snackbars/navigation from notification
final navigatorKey = GlobalKey<NavigatorState>();
runApp(
ProviderScope(
observers: [NotificationSoundObserver()],
child: const TasqApp(),
child: NotificationBridge(
navigatorKey: navigatorKey,
child: const TasqApp(),
),
),
);
}
class NotificationSoundObserver extends ProviderObserver {
static final AudioPlayer _player = AudioPlayer();
StreamSubscription<String?>? _tokenSub;
@override
void didUpdateProvider(
@@ -68,12 +139,12 @@ class NotificationSoundObserver extends ProviderObserver {
Object? newValue,
ProviderContainer container,
) {
// play sound + show OS notification on unread-count increase
if (provider == unreadNotificationsCountProvider) {
final prev = previousValue as int?;
final next = newValue as int?;
if (prev != null && next != null && next > prev) {
_player.play(AssetSource('tasq_notification.wav'));
// also post a system notification so the user sees it outside the app
NotificationService.show(
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
title: 'New notifications',
@@ -81,6 +152,35 @@ class NotificationSoundObserver extends ProviderObserver {
);
}
}
// when profile changes, register or unregister tokens
if (provider == currentProfileProvider) {
final profile = newValue as Profile?;
final controller = container.read(notificationsControllerProvider);
if (profile != null) {
// signed in: save current token and keep listening for refreshes
FirebaseMessaging.instance.getToken().then((token) {
if (token != null) {
debugPrint('profile observer registering token: $token');
controller.registerFcmToken(token);
}
});
_tokenSub = FirebaseMessaging.instance.onTokenRefresh.listen((token) {
debugPrint('token refreshed: $token');
controller.registerFcmToken(token);
});
} else {
// signed out: unregister whatever token we currently have
_tokenSub?.cancel();
FirebaseMessaging.instance.getToken().then((token) {
if (token != null) {
debugPrint('profile observer unregistering token: $token');
controller.unregisterFcmToken(token);
}
});
}
}
}
}