tasq/lib/main.dart

250 lines
8.5 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:flutter/foundation.dart' show kIsWeb;
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:pdfrx/pdfrx.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 {
// 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 {
WidgetsFlutterBinding.ensureInitialized();
// The flag optionally hides annoying WASM warnings in your Chrome dev console
pdfrxFlutterInitialize(dismissPdfiumWasmWarnings: true);
// 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 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;
// listen for auth changes to register/unregister token accordingly
supaClient.auth.onAuthStateChange.listen((data) async {
final event = data.event;
if (kIsWeb) {
debugPrint(
'auth state change $event on web: skipping FCM token handling',
);
return;
}
String? token;
try {
token = await FirebaseMessaging.instance.getToken();
} catch (e) {
debugPrint('FCM getToken failed during auth change: $e');
token = null;
}
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);
}
});
if (!kIsWeb) {
// 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(),
),
),
);
// 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 {
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
if (!kIsWeb) {
FirebaseMessaging.instance
.getToken()
.then((token) {
if (token != null) {
debugPrint('profile observer registering token: $token');
controller.registerFcmToken(token);
}
})
.catchError((e) {
debugPrint('getToken error: $e');
return null;
});
_tokenSub = FirebaseMessaging.instance.onTokenRefresh.listen((token) {
debugPrint('token refreshed: $token');
controller.registerFcmToken(token);
});
}
} else {
// signed out: unregister whatever token we currently have
_tokenSub?.cancel();
if (!kIsWeb) {
FirebaseMessaging.instance
.getToken()
.then((token) {
if (token != null) {
debugPrint('profile observer unregistering token: $token');
controller.unregisterFcmToken(token);
}
})
.catchError((e) {
debugPrint('getToken error: $e');
return null;
});
}
}
}
}
}
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,
),
),
),
),
);
}
}