import 'data/datasources/ws_client.dart'; import 'logic/auth_provider.dart'; import 'logic/contact_provider.dart'; import 'core/theme_manager.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:chepuhagram/domain/services/crypto_service.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'dart:convert'; import 'package:chepuhagram/presentation/screens/chat_screen.dart'; import 'package:chepuhagram/presentation/screens/contacts_screen.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'presentation/screens/splash_screen.dart'; final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); final GlobalKey navigatorKey = GlobalKey(); final RouteObserver routeObserver = RouteObserver(); int? currentActiveChatContactId; RemoteMessage? initialMessage; // Ключ для SharedPreferences const String _notificationLaunchKey = 'notification_launch_data'; const String _lastHandledNotificationLaunchPayloadKey = 'notification_last_handled_payload'; Future _onSelectNotification( NotificationResponse notificationResponse, ) async { final payload = notificationResponse.payload; if (payload != null) { try { final data = jsonDecode(payload); final senderId = int.tryParse(data['sender_id']?.toString() ?? ''); if (senderId != null) { print('Notification selected, payload sender_id=$senderId'); final context = navigatorKey.currentContext; final prefs = await SharedPreferences.getInstance(); final canonicalPayload = jsonEncode(data); if (context == null) { final lastHandled = prefs.getString( _lastHandledNotificationLaunchPayloadKey, ); if (lastHandled != canonicalPayload) { await prefs.setString(_notificationLaunchKey, canonicalPayload); await prefs.setString( _lastHandledNotificationLaunchPayloadKey, canonicalPayload, ); } print( 'Navigator context is null, saved notification payload to SharedPreferences', ); } else { await prefs.remove(_notificationLaunchKey); } _navigateToChat(senderId); } else { print( 'Notification payload has invalid sender_id: ${data['sender_id']}', ); } } catch (e) { print('Error parsing notification payload: $e'); } } } void _navigateToChat(int senderId) { print('Navigating to chat with senderId: $senderId'); final context = navigatorKey.currentContext; if (context != null) { final contactProvider = Provider.of( context, listen: false, ); // Check if contacts are loaded if (contactProvider.contacts.isEmpty) { print('Contacts not loaded yet, navigating to contacts screen first'); // Navigate to contacts screen and pass the senderId to navigate to chat later Navigator.push( context, MaterialPageRoute( builder: (_) => ContactsScreen(targetChatId: senderId), ), ); return; } try { final contact = contactProvider.contacts.firstWhere( (c) => c.id == senderId, orElse: () => throw Exception('Contact not found'), ); print('Found contact: ${contact.username}, navigating to chat'); currentActiveChatContactId = senderId; // Устанавливаем активный чат Navigator.push( context, MaterialPageRoute(builder: (_) => ChatScreen(contact: contact)), ); } catch (e) { print( 'Contact with id $senderId not found, navigating to contacts screen', ); // Contact not found, go to contacts screen Navigator.push( context, MaterialPageRoute(builder: (_) => const ContactsScreen()), ); } } else { print('Navigator context is null'); } } void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); await Future.delayed(const Duration(milliseconds: 500)); initialMessage = await FirebaseMessaging.instance.getInitialMessage(); print('Initial message from main() after delay: $initialMessage'); // Сохраняем информацию в SharedPreferences для надежности final prefs = await SharedPreferences.getInstance(); if (initialMessage != null) { print('App launched from notification: ${initialMessage!.data}'); print('Message type: ${initialMessage!.data['type']}'); print('Sender ID: ${initialMessage!.data['sender_id']}'); final payloadString = jsonEncode(initialMessage!.data); final lastHandled = prefs.getString( _lastHandledNotificationLaunchPayloadKey, ); if (lastHandled != payloadString) { // Сохраняем данные уведомления await prefs.setString(_notificationLaunchKey, payloadString); await prefs.setString( _lastHandledNotificationLaunchPayloadKey, payloadString, ); print('Saved notification data to SharedPreferences'); } else { print('InitialMessage payload already handled earlier, skipping'); } } else { print('No initial message - app launched normally'); // Очищаем сохраненные данные, если приложение запущено нормально await prefs.remove(_notificationLaunchKey); } // Initialize local notifications const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); final InitializationSettings initializationSettings = InitializationSettings( android: initializationSettingsAndroid, ); await flutterLocalNotificationsPlugin.initialize( initializationSettings, onDidReceiveNotificationResponse: _onSelectNotification, ); // Если приложение было запущено из локального уведомления, сохраним payload final notificationAppLaunchDetails = await flutterLocalNotificationsPlugin .getNotificationAppLaunchDetails(); if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) { final payload = notificationAppLaunchDetails?.notificationResponse?.payload; print('App launched from local notification, payload: $payload'); if (payload != null && payload.isNotEmpty) { try { final lastHandled = prefs.getString( _lastHandledNotificationLaunchPayloadKey, ); if (lastHandled != payload) { final data = jsonDecode(payload); await prefs.setString(_notificationLaunchKey, jsonEncode(data)); await prefs.setString( _lastHandledNotificationLaunchPayloadKey, payload, ); print('Saved local notification launch payload to SharedPreferences'); } else { print('Local notification payload already handled earlier, skipping'); } } catch (e) { print('Failed to save notification launch payload: $e'); } } } // Create notification channel for Android 8+ const AndroidNotificationChannel channel = AndroidNotificationChannel( 'chat_id', // id 'Messages', // title description: 'Chat messages notifications', // description importance: Importance.high, ); await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin >() ?.createNotificationChannel(channel); FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); runApp( MultiProvider( providers: [ Provider(create: (_) => CryptoService()), Provider(create: (_) => SocketService()), ChangeNotifierProvider(create: (_) => AuthProvider()), ChangeNotifierProvider(create: (_) => ThemeProvider()), Provider(create: (_) => SocketService()), ChangeNotifierProvider( create: (context) => ContactProvider(context.read()), ), ], child: const MyApp(), ), ); } @pragma('vm:entry-point') Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { print("Фоновый пуш получен: ${message.data}"); if (message.data['type'] == 'enc_message') { try { // Initialize notifications for background const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); const InitializationSettings initializationSettings = InitializationSettings(android: initializationSettingsAndroid); await flutterLocalNotificationsPlugin.initialize(initializationSettings); // Create notification channel const AndroidNotificationChannel channel = AndroidNotificationChannel( 'chat_id', 'Messages', description: 'Chat messages notifications', importance: Importance.high, ); await flutterLocalNotificationsPlugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin >() ?.createNotificationChannel(channel); // Try to decrypt String notificationText = 'New encrypted message'; try { // 1. Инициализируем крипто-сервис final crypto = CryptoService(); // 2. Достаем ключи (они должны быть в SecureStorage) final myPrivKey = await crypto.getPrivateKey(); print('Private key retrieved: ${myPrivKey != null}'); if (myPrivKey == null) { print('Private key not found, showing encrypted message'); notificationText = 'Encrypted message: ${message.data['content']?.substring(0, 50) ?? 'N/A'}...'; } else { // 3. Расшифровываем final sharedSecret = await crypto.deriveSharedSecret( myPrivKey, message.data['public_key'], ); final decryptedText = await crypto.decryptMessage( message.data['content'], sharedSecret, ); notificationText = decryptedText; } } catch (e) { print('Decryption failed: $e'); notificationText = 'Failed to decrypt: ${e.toString()}'; } final senderId = int.tryParse( message.data['sender_id']?.toString() ?? '', ); // 4. Показываем локальное уведомление final String groupKey = 'ru.chepuhagram.app.$senderId'; await flutterLocalNotificationsPlugin.show( senderId!, '', '', NotificationDetails( android: AndroidNotificationDetails( 'Messages', 'Новые сообщения', groupKey: groupKey, setAsGroupSummary: true, importance: Importance.high, priority: Priority.high, groupAlertBehavior: GroupAlertBehavior.all, ), ), ); await flutterLocalNotificationsPlugin.show( message.hashCode, message.data['username'] ?? 'Unknown', notificationText, NotificationDetails( android: AndroidNotificationDetails( 'chat_id', 'Messages', groupKey: groupKey, importance: Importance.high, priority: Priority.high, showWhen: true, ), ), payload: jsonEncode({ 'type': 'enc_message', 'sender_id': message.data['sender_id'], 'timestamp': message.data['timestamp'] ?? DateTime.now().toIso8601String(), }), ); print('Notification shown successfully'); } catch (e) { print('Error processing background message: $e'); } } else { print('Message type is not enc_message: ${message.data['type']}'); } } class MyApp extends StatefulWidget { const MyApp({super.key}); @override State createState() => _MyAppState(); } class _MyAppState extends State with WidgetsBindingObserver { @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { // Закрываем сокет, как только приложение сворачивается. if (state == AppLifecycleState.paused || state == AppLifecycleState.inactive || state == AppLifecycleState.detached) { try { context.read().closeRealtime(); } catch (_) {} return; } // На возврате в приложение — пробуем переподключиться (если есть токен). if (state == AppLifecycleState.resumed) { try { context.read().initRealtime(); } catch (_) {} } } @override Widget build(BuildContext context) { final themeProvider = context.watch(); return MaterialApp( title: 'Chepuhagram', debugShowCheckedModeBanner: false, themeAnimationDuration: const Duration(milliseconds: 300), themeAnimationCurve: Curves.easeInOut, theme: themeProvider.themeData, themeMode: themeProvider.themeMode, navigatorKey: navigatorKey, navigatorObservers: [routeObserver], // Начальный экран home: const SplashScreen(), ); } }