import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'core/constants/app_constants.dart'; import 'core/cache/cache_refresh_coordinator.dart'; import 'core/di/injection.dart'; import 'core/notifications/local_notification_service.dart'; import 'core/notifications/reminder_notification_callbacks.dart'; import 'core/notifications/ios_notification_payload_bridge.dart'; import 'core/router/app_router.dart'; import 'core/startup/auth_session_bootstrapper.dart'; import 'core/theme/app_theme.dart'; import 'features/auth/presentation/bloc/auth_bloc.dart'; import 'features/auth/presentation/bloc/auth_event.dart'; import 'features/auth/presentation/bloc/auth_state.dart'; import 'features/calendar/data/services/calendar_service.dart'; import 'features/calendar/data/services/calendar_repository.dart'; import 'features/calendar/reminders/reminder_queue_manager.dart'; import 'features/calendar/reminders/ui/reminder_overlay.dart'; import 'features/calendar/ui/calendar_state_manager.dart'; import 'features/chat/presentation/bloc/chat_bloc.dart'; import 'features/settings/data/services/settings_user_cache.dart'; import 'features/todo/data/todo_repository.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await configureDependencies(); await AppConstants.init(); final rootNavigatorKey = GlobalKey(); await sl().initialize(); final authBloc = sl(); authBloc.add(AuthStarted()); final cacheRefreshCoordinator = CacheRefreshCoordinator( minInterval: const Duration(minutes: 5), onRefresh: () { final selected = sl().selectedDate; unawaited( sl().getDayEvents(selected, forceRefresh: true), ); unawaited( sl().getMonthEvents( DateTime(selected.year, selected.month, 1), forceRefresh: true, ), ); unawaited(sl().getPendingTodos(forceRefresh: true)); unawaited(sl().getProfile(forceRefresh: true)); }, ); WidgetsBinding.instance.addObserver(cacheRefreshCoordinator); final prefs = await SharedPreferences.getInstance(); final payloadBridge = IOSNotificationPayloadBridge(prefs); final pendingPayload = await payloadBridge.getPendingPayload(); final queueManager = ReminderQueueManager(); if (pendingPayload != null) { queueManager.enqueueFromClick(pendingPayload); await payloadBridge.clearPendingPayload(); } final linksyAppKey = GlobalKey(); ReminderNotificationCallbacks.onNotificationPayloadReceived = (payload) async { await payloadBridge.setPendingPayload(payload); queueManager.enqueueFromClick(payload); WidgetsBinding.instance.addPostFrameCallback((_) { final state = linksyAppKey.currentState; if (state != null && state is _LinksyAppState) { state.showReminderOverlayFromNotification(); } }); }; runApp( LinksyApp( key: linksyAppKey, authBloc: authBloc, rootNavigatorKey: rootNavigatorKey, sessionBootstrapper: AuthSessionBootstrapper( calendarService: sl(), notificationService: sl(), ), queueManager: queueManager, payloadBridge: payloadBridge, ), ); WidgetsBinding.instance.addPostFrameCallback((_) { unawaited( ReminderNotificationCallbacks.bindResponseHandler( sl().handleNotificationResponse, ), ); }); } class LinksyApp extends StatefulWidget { final AuthBloc authBloc; final GlobalKey rootNavigatorKey; final AuthSessionBootstrapper sessionBootstrapper; final ReminderQueueManager queueManager; final IOSNotificationPayloadBridge payloadBridge; const LinksyApp({ super.key, required this.authBloc, required this.rootNavigatorKey, required this.sessionBootstrapper, required this.queueManager, required this.payloadBridge, }); @override State createState() => _LinksyAppState(); } class _LinksyAppState extends State with WidgetsBindingObserver { OverlayEntry? _reminderOverlay; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _checkAndShowReminderOverlay(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { _checkAndShowReminderOverlay(); } } Future _checkAndShowReminderOverlay() async { if (widget.queueManager.currentPayload != null) { _showReminderOverlay(); return; } final pendingPayload = await widget.payloadBridge.getPendingPayload(); if (pendingPayload != null) { widget.queueManager.enqueueFromClick(pendingPayload); await widget.payloadBridge.clearPendingPayload(); _showReminderOverlay(); } } void _showReminderOverlay() { if (_reminderOverlay != null) return; _reminderOverlay = OverlayEntry( builder: (context) => Positioned.fill( child: Material( color: Colors.black54, child: ReminderOverlay( queueManager: widget.queueManager, onComplete: _onReminderComplete, onSnooze: _onSnooze, onArchive: _onArchive, ), ), ), ); Overlay.of(context).insert(_reminderOverlay!); } void showReminderOverlayFromNotification() { if (widget.queueManager.currentPayload != null) { _showReminderOverlay(); } } void _onReminderComplete() { _reminderOverlay?.remove(); _reminderOverlay = null; if (!widget.queueManager.isEmpty) { _showReminderOverlay(); } } Future _onSnooze(int minutes) async { final payload = widget.queueManager.currentPayload; if (payload == null) return; await sl().cancelEventReminder(payload.eventId); final event = await sl().getEventById(payload.eventId); if (event != null) { final snoozeTime = DateTime.now().add(Duration(minutes: minutes)); await sl().scheduleReminderAt( event, snoozeTime, ); } } Future _onArchive() async { final payload = widget.queueManager.currentPayload; if (payload == null) return; try { await sl().archiveEvent(payload.eventId); } catch (_) { // archive failed, continue anyway } } @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider.value(value: widget.authBloc), BlocProvider(create: (_) => ChatBloc(apiClient: sl())), ], child: BlocListener( listenWhen: (previous, current) => previous != current, listener: (context, state) { unawaited(widget.sessionBootstrapper.syncForAuthState(state)); }, child: MaterialApp.router( title: 'Linksy', debugShowCheckedModeBanner: false, theme: AppTheme.light, routerConfig: createAppRouter(widget.authBloc), ), ), ); } }