initial commit: push everything
This commit is contained in:
402
lib/providers/notifications/notificationservce.dart
Normal file
402
lib/providers/notifications/notificationservce.dart
Normal file
@@ -0,0 +1,402 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:nearle/helpers/http_overrides.dart';
|
||||
|
||||
// Top-level background handler required by Firebase Messaging
|
||||
@pragma('vm:entry-point')
|
||||
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||
HttpOverrides.global = MyHttpOverrides();
|
||||
try {
|
||||
await Firebase.initializeApp();
|
||||
} catch (_) {}
|
||||
|
||||
await NotificationServce.display(message);
|
||||
}
|
||||
|
||||
class NotificationServce {
|
||||
static final FirebaseMessaging _firebaseMessaging =
|
||||
FirebaseMessaging.instance;
|
||||
static final FlutterLocalNotificationsPlugin _notificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
static final AudioPlayer _player = AudioPlayer();
|
||||
static String _channelId = 'Nearle';
|
||||
static bool _isPlaying = false;
|
||||
|
||||
static const AndroidNotificationChannel channel = AndroidNotificationChannel(
|
||||
'Nearle',
|
||||
'Nearle Notification',
|
||||
description: 'Channel for Nearle notifications',
|
||||
importance: Importance.max,
|
||||
playSound: true,
|
||||
enableVibration: true,
|
||||
showBadge: true,
|
||||
);
|
||||
|
||||
static Future<void> initialize(BuildContext context) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final alreadyInit = prefs.getBool('notifications_init_done') ?? false;
|
||||
if (alreadyInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
await FirebaseMessaging.instance.requestPermission(
|
||||
alert: true,
|
||||
badge: true,
|
||||
sound: true,
|
||||
);
|
||||
|
||||
final existing = (prefs.getString('order_alert_sound') ?? '').trim();
|
||||
if (existing.isEmpty) {
|
||||
await prefs.setString('order_alert_sound', 'assets/audio/alert-1.mp3');
|
||||
}
|
||||
await prefs.setBool('notifications_init_done', true);
|
||||
} catch (_) {}
|
||||
|
||||
await _notificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>()
|
||||
?.createNotificationChannel(channel);
|
||||
|
||||
await _applyChannelSoundFromPrefs();
|
||||
|
||||
const InitializationSettings initializationSettings =
|
||||
InitializationSettings(
|
||||
android: AndroidInitializationSettings('@mipmap/ic_launcher'),
|
||||
iOS: DarwinInitializationSettings(
|
||||
requestSoundPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestAlertPermission: true,
|
||||
defaultPresentSound: true,
|
||||
defaultPresentBadge: true,
|
||||
defaultPresentBanner: true,
|
||||
defaultPresentAlert: true,
|
||||
defaultPresentList: true,
|
||||
),
|
||||
);
|
||||
|
||||
await _notificationsPlugin.initialize(
|
||||
initializationSettings,
|
||||
onDidReceiveNotificationResponse: (NotificationResponse response) async {},
|
||||
);
|
||||
|
||||
RemoteMessage? initialMessage =
|
||||
await _firebaseMessaging.getInitialMessage();
|
||||
if (initialMessage != null) {
|
||||
await _handleInitialMessage(initialMessage);
|
||||
}
|
||||
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) async {
|
||||
await _handleMessage(message);
|
||||
});
|
||||
|
||||
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) async {
|
||||
await _handleMessageOpenedApp(message);
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Removed duplicated background handler (this was breaking your notifications)
|
||||
|
||||
static Future<void> _handleInitialMessage(RemoteMessage message) async {
|
||||
if (message.notification != null) {
|
||||
await display(message);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _handleMessage(RemoteMessage message) async {
|
||||
if (message.notification != null) {
|
||||
await display(message);
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _handleMessageOpenedApp(RemoteMessage message) async {}
|
||||
|
||||
static Future<void> _applyChannelSoundFromPrefs() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
String sel = (prefs.getString('order_alert_sound') ?? '').trim();
|
||||
if (sel.isEmpty) {
|
||||
sel = 'assets/audio/alert-1.mp3';
|
||||
}
|
||||
|
||||
final fileName = sel.split('/').last;
|
||||
final base = fileName.split('.').first;
|
||||
final rawName = base.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_');
|
||||
|
||||
final androidImpl = _notificationsPlugin
|
||||
.resolvePlatformSpecificImplementation<
|
||||
AndroidFlutterLocalNotificationsPlugin>();
|
||||
|
||||
if (androidImpl != null) {
|
||||
_channelId = 'Nearle_$rawName';
|
||||
|
||||
final custom = AndroidNotificationChannel(
|
||||
_channelId,
|
||||
'Nearle Notification',
|
||||
description: 'Channel for Nearle notifications',
|
||||
importance: Importance.max,
|
||||
playSound: true,
|
||||
sound: RawResourceAndroidNotificationSound(rawName),
|
||||
enableVibration: true,
|
||||
showBadge: true,
|
||||
);
|
||||
|
||||
await androidImpl.createNotificationChannel(custom);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static Future<String?> _downloadAndSaveImage(
|
||||
String imageUrl, String fileName) async {
|
||||
try {
|
||||
final directory = await getTemporaryDirectory();
|
||||
final filePath = '${directory.path}/$fileName';
|
||||
final response = await http.get(Uri.parse(imageUrl));
|
||||
if (response.statusCode == 200) {
|
||||
final file = File(filePath);
|
||||
await file.writeAsBytes(response.bodyBytes);
|
||||
return filePath;
|
||||
}
|
||||
return null;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static String? _extractImageUrl(RemoteMessage message) {
|
||||
String? imageUrl = message.data['image'] as String?;
|
||||
imageUrl ??= message.notification?.android?.imageUrl;
|
||||
imageUrl ??= message.notification?.apple?.imageUrl;
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
static Future<void> _playSelectedSound({int times = 1}) async {
|
||||
try {
|
||||
if (_isPlaying) return;
|
||||
|
||||
_isPlaying = true;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
String selected = (prefs.getString('order_alert_sound') ?? '').trim();
|
||||
if (selected.isEmpty) {
|
||||
selected = 'assets/audio/alert-1.mp3';
|
||||
}
|
||||
|
||||
final rel = selected.startsWith('assets/')
|
||||
? selected.replaceFirst('assets/', '')
|
||||
: selected;
|
||||
|
||||
await _player.stop();
|
||||
await _player.setReleaseMode(ReleaseMode.stop);
|
||||
|
||||
for (int i = 0; i < times; i++) {
|
||||
await _player.play(AssetSource(rel));
|
||||
try {
|
||||
await _player.onPlayerComplete.first;
|
||||
} catch (_) {}
|
||||
if (i < times - 1) {
|
||||
await Future.delayed(const Duration(milliseconds: 120));
|
||||
}
|
||||
}
|
||||
} catch (_) {} finally {
|
||||
_isPlaying = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Lightweight local notification helper for in-app events (no FCM message).
|
||||
static Future<void> showLocalNotification({
|
||||
required String title,
|
||||
required String body,
|
||||
bool playSound = true,
|
||||
String? payload,
|
||||
}) async {
|
||||
try {
|
||||
final id = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
|
||||
final notificationDetails = NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
_channelId,
|
||||
'Nearle Notification',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
playSound: playSound,
|
||||
enableVibration: true,
|
||||
channelShowBadge: true,
|
||||
ongoing: false,
|
||||
autoCancel: true,
|
||||
),
|
||||
iOS: DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: playSound,
|
||||
presentList: true,
|
||||
presentBanner: true,
|
||||
),
|
||||
);
|
||||
|
||||
await _notificationsPlugin.show(
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
notificationDetails,
|
||||
payload: payload,
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static Future<void> display(RemoteMessage message) async {
|
||||
final id = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
final payload = jsonEncode({'id': id.toString(), 'data': message.data});
|
||||
|
||||
final appState = WidgetsBinding.instance.lifecycleState;
|
||||
final bool isForeground = appState == AppLifecycleState.resumed;
|
||||
|
||||
if (isForeground) {
|
||||
await _playSelectedSound(times: 5);
|
||||
}
|
||||
|
||||
NotificationDetails notificationDetails;
|
||||
|
||||
final imageUrl = _extractImageUrl(message);
|
||||
String? persistedImageUrl = imageUrl;
|
||||
String? persistedImagePath;
|
||||
|
||||
if (imageUrl != null && imageUrl.isNotEmpty) {
|
||||
final imagePath = await _downloadAndSaveImage(
|
||||
imageUrl,
|
||||
'notification_image.jpg',
|
||||
);
|
||||
persistedImagePath = imagePath;
|
||||
|
||||
notificationDetails = NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
_channelId,
|
||||
'Nearle Notification',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
playSound: !isForeground,
|
||||
enableVibration: true,
|
||||
fullScreenIntent: true,
|
||||
channelShowBadge: true,
|
||||
ongoing: false,
|
||||
autoCancel: true,
|
||||
styleInformation: imagePath != null
|
||||
? BigPictureStyleInformation(FilePathAndroidBitmap(imagePath))
|
||||
: const DefaultStyleInformation(true, true),
|
||||
),
|
||||
iOS: const DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
presentList: true,
|
||||
presentBanner: true,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
notificationDetails = NotificationDetails(
|
||||
android: AndroidNotificationDetails(
|
||||
_channelId,
|
||||
'Nearle Notification',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
icon: '@mipmap/ic_launcher',
|
||||
playSound: !isForeground,
|
||||
enableVibration: true,
|
||||
fullScreenIntent: true,
|
||||
channelShowBadge: true,
|
||||
ongoing: false,
|
||||
autoCancel: true,
|
||||
),
|
||||
iOS: const DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
presentList: true,
|
||||
presentBanner: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isForeground) {
|
||||
final ctx = Get.context;
|
||||
if (ctx != null) {
|
||||
final title =
|
||||
message.notification?.title ?? message.data['title'] ?? 'Nearle';
|
||||
final body = message.notification?.body ?? message.data['body'] ?? '';
|
||||
|
||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||
SnackBar(
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: const TextStyle(fontWeight: FontWeight.w700)),
|
||||
if (body.isNotEmpty) Text(body),
|
||||
],
|
||||
),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
margin: const EdgeInsets.all(16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await _notificationsPlugin.show(
|
||||
id,
|
||||
message.notification?.title ?? message.data['title'] ?? 'Nearle',
|
||||
message.notification?.body ?? message.data['body'] ?? 'Notification',
|
||||
notificationDetails,
|
||||
payload: payload,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final nowIso = DateTime.now().toIso8601String();
|
||||
|
||||
final title =
|
||||
message.notification?.title ?? message.data['title'] ?? 'Nearle';
|
||||
final body =
|
||||
message.notification?.body ?? message.data['body'] ?? '';
|
||||
|
||||
final entry = {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'body': body,
|
||||
'time': nowIso,
|
||||
'data': message.data,
|
||||
if (persistedImageUrl != null) 'imageUrl': persistedImageUrl,
|
||||
if (persistedImagePath != null) 'imagePath': persistedImagePath,
|
||||
};
|
||||
|
||||
final existingRaw = prefs.getString('notifications_log');
|
||||
List<dynamic> list = [];
|
||||
|
||||
if (existingRaw != null && existingRaw.isNotEmpty) {
|
||||
try {
|
||||
list = jsonDecode(existingRaw) as List<dynamic>;
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
list.insert(0, entry);
|
||||
|
||||
if (list.length > 100) list = list.sublist(0, 100);
|
||||
|
||||
await prefs.setString('notifications_log', jsonEncode(list));
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user