tasq/lib/main.dart
Marc Rejohn Castillano 5979a04254 * Push Notification Setup and attempt
* Office Ordering
* Allow editing of Task and Ticket Details after creation
2026-02-24 21:06:46 +08:00

208 lines
7.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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');
final supabaseUrl = dotenv.env['SUPABASE_URL'] ?? '';
final supabaseAnonKey = dotenv.env['SUPABASE_ANON_KEY'] ?? '';
if (supabaseUrl.isEmpty || supabaseAnonKey.isEmpty) {
runApp(const _MissingConfigApp());
return;
}
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`.
final granted = await ensureNotificationPermission();
if (!granted) {
// we dont block startup, but its worth logging so developers notice. A
// real app might show a dialog pointing the user to settings.
// 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) {
// handle user tapping a notification; the payload format is up to us
final payload = response.payload;
if (payload != null && payload.startsWith('ticket:')) {
// ignore if context not mounted; we might use a navigator key in real
// app, but keep this simple for now
// TODO: navigate to ticket/task as appropriate
}
},
);
// global navigator key used for snackbars/navigation from notification
final navigatorKey = GlobalKey<NavigatorState>();
runApp(
ProviderScope(
observers: [NotificationSoundObserver()],
child: NotificationBridge(
navigatorKey: navigatorKey,
child: const TasqApp(),
),
),
);
}
class NotificationSoundObserver extends ProviderObserver {
static final AudioPlayer _player = AudioPlayer();
StreamSubscription<String?>? _tokenSub;
@override
void didUpdateProvider(
ProviderBase provider,
Object? previousValue,
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'));
NotificationService.show(
id: DateTime.now().millisecondsSinceEpoch ~/ 1000,
title: 'New notifications',
body: 'You have $next unread notifications.',
);
}
}
// 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);
}
});
}
}
}
}
class _MissingConfigApp extends StatelessWidget {
const _MissingConfigApp();
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Text(
'Missing SUPABASE_URL or SUPABASE_ANON_KEY. '
'Provide them in the .env file.',
textAlign: TextAlign.center,
),
),
),
),
);
}
}