feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل
قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ. الخريطة: backend · payment_server · loction_server · ride_server · passenger_server · docker · dashboard · stress_test → الجذر siro_rider → apps/rider siro_driver → apps/driver siro_admin → dashboards/admin siro_service → dashboards/service android_bot → apps/android_bot socialBot → apps/socialBot نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب) لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً: كل ما يلي يصير فرقاً مقروءاً مقابل المصدر. لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز، سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh (ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و dashboards/transit-web). ⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة: 1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر): كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner. 2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist) يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً. 3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع → يجب ضمّ الحزم داخله أسوة بـ apps/rider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9909d9b4c1
commit
4d8414c96b
@@ -0,0 +1,50 @@
|
||||
// import 'dart:convert';
|
||||
// import 'package:googleapis_auth/auth_io.dart';
|
||||
|
||||
// class AccessTokenManager {
|
||||
// static final AccessTokenManager _instance = AccessTokenManager._internal();
|
||||
// late final String serviceAccountJsonKey;
|
||||
// AccessToken? _accessToken;
|
||||
// DateTime? _expiryDate;
|
||||
|
||||
// AccessTokenManager._internal();
|
||||
|
||||
// factory AccessTokenManager(String jsonKey) {
|
||||
// if (_instance._isServiceAccountKeyInitialized()) {
|
||||
// // Prevent re-initialization
|
||||
// return _instance;
|
||||
// }
|
||||
// _instance.serviceAccountJsonKey = jsonKey;
|
||||
// return _instance;
|
||||
// }
|
||||
|
||||
// bool _isServiceAccountKeyInitialized() {
|
||||
// try {
|
||||
// serviceAccountJsonKey; // Access to check if initialized
|
||||
// return true;
|
||||
// } catch (e) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Future<String> getAccessToken() async {
|
||||
// if (_accessToken != null && DateTime.now().isBefore(_expiryDate!)) {
|
||||
// return _accessToken!.data;
|
||||
// }
|
||||
// try {
|
||||
// final serviceAccountCredentials = ServiceAccountCredentials.fromJson(
|
||||
// json.decode(serviceAccountJsonKey));
|
||||
// final client = await clientViaServiceAccount(
|
||||
// serviceAccountCredentials,
|
||||
// ['https://www.googleapis.com/auth/firebase.messaging'],
|
||||
// );
|
||||
|
||||
// _accessToken = client.credentials.accessToken;
|
||||
// _expiryDate = client.credentials.accessToken.expiry;
|
||||
// client.close();
|
||||
// return _accessToken!.data;
|
||||
// } catch (e) {
|
||||
// throw Exception('Failed to obtain access token');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,784 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:siro_rider/views/home/HomePage/trip_monitor/trip_link_monitor.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:siro_rider/controller/functions/toast.dart';
|
||||
import 'package:siro_rider/views/widgets/elevated_btn.dart';
|
||||
import 'package:siro_rider/views/widgets/error_snakbar.dart';
|
||||
|
||||
import '../../constant/box_name.dart';
|
||||
import '../../constant/colors.dart';
|
||||
import '../../constant/style.dart';
|
||||
import '../../main.dart';
|
||||
import '../../print.dart';
|
||||
import '../../views/Rate/rate_captain.dart';
|
||||
import '../../views/home/map_page_passenger.dart';
|
||||
import '../../views/home/profile/promos_passenger_page.dart';
|
||||
import 'package:siro_rider/controller/voice_call_controller.dart';
|
||||
import '../home/map/ride_lifecycle_controller.dart';
|
||||
import '../home/map/ride_state.dart';
|
||||
import 'local_notification.dart';
|
||||
import '../../views/widgets/mydialoug.dart';
|
||||
|
||||
class FirebaseMessagesController extends GetxController {
|
||||
final fcmToken = FirebaseMessaging.instance;
|
||||
|
||||
List<String> tokens = [];
|
||||
List dataTokens = [];
|
||||
late String driverID;
|
||||
late String driverToken;
|
||||
NotificationSettings? notificationSettings;
|
||||
|
||||
Future<void> getNotificationSettings() async {
|
||||
// Get the current notification settings
|
||||
NotificationSettings? notificationSettings =
|
||||
await FirebaseMessaging.instance.getNotificationSettings();
|
||||
'Notification authorization status: ${notificationSettings.authorizationStatus}';
|
||||
|
||||
// Call the update function if needed
|
||||
update();
|
||||
}
|
||||
|
||||
Future<void> requestFirebaseMessagingPermission() async {
|
||||
FirebaseMessaging messaging = FirebaseMessaging.instance;
|
||||
|
||||
// Check if the platform is Android
|
||||
if (Platform.isAndroid) {
|
||||
// Request permission for Android
|
||||
await messaging.requestPermission();
|
||||
} else if (Platform.isIOS) {
|
||||
// Request permission for iOS
|
||||
NotificationSettings settings = await messaging.requestPermission(
|
||||
alert: true,
|
||||
announcement: true,
|
||||
badge: true,
|
||||
carPlay: true,
|
||||
criticalAlert: true,
|
||||
provisional: false,
|
||||
sound: true,
|
||||
);
|
||||
messaging.setForegroundNotificationPresentationOptions(
|
||||
alert: true, badge: true, sound: true);
|
||||
}
|
||||
}
|
||||
|
||||
NotificationController notificationController =
|
||||
Get.isRegistered<NotificationController>()
|
||||
? Get.find<NotificationController>()
|
||||
: Get.put(NotificationController());
|
||||
|
||||
Future getToken() async {
|
||||
fcmToken.getToken().then((token) {
|
||||
// Log.print('fcmToken: ${token}');
|
||||
box.write(BoxName.tokenFCM, (token.toString()));
|
||||
});
|
||||
// 🔹 الاشتراك في topic العام
|
||||
await fcmToken.subscribeToTopic("passengers");
|
||||
Log.print("Subscribed to 'passengers' topic ✅");
|
||||
|
||||
// اشتراك شخصي بـ passenger_{id} لاستقبال إشعارات العضوية في مواصلاتي
|
||||
final passengerId = box.read(BoxName.passengerID);
|
||||
if (passengerId != null && passengerId.toString().isNotEmpty) {
|
||||
await fcmToken.subscribeToTopic('passenger_${passengerId}');
|
||||
Log.print("Subscribed to 'passenger_${passengerId}' topic ✅");
|
||||
}
|
||||
|
||||
FirebaseMessaging.onMessage.listen((RemoteMessage message) {
|
||||
// If the app is in the background or terminated, show a system tray message
|
||||
RemoteNotification? notification = message.notification;
|
||||
AndroidNotification? android = notification?.android;
|
||||
// if (notification != null && android != null) {
|
||||
if (message.data.isNotEmpty) {
|
||||
fireBaseTitles(message);
|
||||
}
|
||||
});
|
||||
|
||||
FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
|
||||
if (message.data.isNotEmpty && message.notification != null) {
|
||||
fireBaseTitles(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> fireBaseTitles(RemoteMessage message) async {
|
||||
// [!! تعديل !!]
|
||||
// اقرأ "النوع" من حمولة البيانات، وليس من العنوان
|
||||
String category = message.data['category'] ?? '';
|
||||
|
||||
final mapCtrl = Get.isRegistered<RideLifecycleController>()
|
||||
? Get.find<RideLifecycleController>()
|
||||
: null;
|
||||
// اقرأ العنوان (للعرض)
|
||||
String title = message.data['title'] ?? message.notification?.title ?? '';
|
||||
String body = message.data['body'] ?? message.notification?.body ?? '';
|
||||
|
||||
if (category == 'ORDER') {
|
||||
// <-- مثال: كان 'Order'.tr
|
||||
Log.print('message: ${message}');
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'Order');
|
||||
}
|
||||
}
|
||||
|
||||
// ... داخل معالج الإشعارات في تطبيق الراكب ...
|
||||
else if (category == 'Accepted Ride') {
|
||||
if (Platform.isAndroid) {
|
||||
String notifTitle = 'Ride Accepted ✅'.tr;
|
||||
String notifBody = 'The captain is on their way to you now.'.tr;
|
||||
notificationController.showNotification(notifTitle, notifBody, 'ding');
|
||||
}
|
||||
if (mapCtrl != null) {
|
||||
Map<String, dynamic>? driverInfoMap;
|
||||
|
||||
// 2. معالجة driver_info (تأتي كـ String JSON من PHP)
|
||||
if (message.data['driver_info'] != null) {
|
||||
try {
|
||||
String rawJson = message.data['driver_info'];
|
||||
// 🔥 فك التشفير: تحويل الـ String إلى Map
|
||||
driverInfoMap = jsonDecode(rawJson);
|
||||
} catch (e) {
|
||||
Log.print("❌ Error decoding FCM driver_info: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// 3. تمرير البيانات الجاهزة للكنترولر
|
||||
await mapCtrl.processRideAcceptance(
|
||||
driverData: driverInfoMap,
|
||||
source: "FCM",
|
||||
);
|
||||
}
|
||||
} else if (category == 'Promo') {
|
||||
// <-- كان 'Promo'.tr
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'promo');
|
||||
}
|
||||
Get.to(const PromosPassengerPage());
|
||||
} else if (category == 'Trip Monitoring') {
|
||||
// <-- كان 'Trip Monitoring'.tr
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'iphone_ringtone');
|
||||
}
|
||||
var myListString = message.data['passengerList'];
|
||||
var myList = jsonDecode(myListString) as List<dynamic>;
|
||||
Get.to(() => TripMonitor(), arguments: {
|
||||
'rideId': myList[0].toString(),
|
||||
'driverId': myList[1].toString(),
|
||||
});
|
||||
} else if (category == 'token change') {
|
||||
// <-- كان 'token change'.tr
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'cancel');
|
||||
}
|
||||
box.remove(BoxName.jwt);
|
||||
} else if (category == 'Driver Is Going To Passenger') {
|
||||
// <-- كان 'Driver Is Going To Passenger'
|
||||
Get.find<RideLifecycleController>().isDriverInPassengerWay = true;
|
||||
Get.find<RideLifecycleController>().update();
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'tone1');
|
||||
}
|
||||
} else if (category == 'MSG_FROM_PASSENGER') {
|
||||
// <-- كان 'message From passenger'
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'ding');
|
||||
}
|
||||
MyDialog().getChatDialog(
|
||||
title.isNotEmpty ? title : 'message From passenger'.tr,
|
||||
body,
|
||||
() {},
|
||||
);
|
||||
update();
|
||||
} else if (category == 'message From Driver') {
|
||||
// <-- كان 'message From Driver'
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'ding');
|
||||
}
|
||||
MyDialog().getChatDialog(
|
||||
title.isNotEmpty ? title : 'message From Driver'.tr,
|
||||
body,
|
||||
() {},
|
||||
);
|
||||
update();
|
||||
} else if (category == 'Trip is Begin') {
|
||||
// <-- كان 'Trip is Begin'
|
||||
if (Platform.isAndroid) {
|
||||
String notifTitle = 'Trip Begun 🚀'.tr;
|
||||
String notifBody = 'Have a safe and comfortable trip.'.tr;
|
||||
notificationController.showNotification(notifTitle, notifBody, 'ding');
|
||||
}
|
||||
Log.print('[FCM] استقبل إشعار "TRIP_BEGUN".');
|
||||
// استدعاء الحارس
|
||||
mapCtrl!.processRideBegin(source: "FCM");
|
||||
} else if (category == 'Hi ,I will go now') {
|
||||
// <-- كان 'Hi ,I will go now'.tr
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(title, body, 'ding');
|
||||
}
|
||||
update();
|
||||
} else if (category == "Arrive Ride") {
|
||||
if (Platform.isAndroid) {
|
||||
String notifTitle = 'Driver is outside 🚖'.tr;
|
||||
String notifBody = 'The captain has arrived at your location.'.tr;
|
||||
notificationController.showNotification(notifTitle, notifBody, 'ding');
|
||||
}
|
||||
// استدعاء الحارس
|
||||
mapCtrl!.processDriverArrival("FCM");
|
||||
} else if (category == 'Driver Finish Trip') {
|
||||
if (Platform.isAndroid) {
|
||||
String notifTitle = 'Trip Finished 🏁'.tr;
|
||||
String notifBody = 'Please pay and rate the driver.'.tr;
|
||||
notificationController.showNotification(notifTitle, notifBody, 'ding');
|
||||
}
|
||||
List<dynamic> driverList = [];
|
||||
|
||||
// ✅ معالجة آمنة للبيانات
|
||||
var rawData = message.data['DriverList'];
|
||||
if (rawData != null && rawData.isNotEmpty) {
|
||||
try {
|
||||
driverList = jsonDecode(rawData) as List<dynamic>;
|
||||
} catch (e) {
|
||||
Log.print("❌ Error decoding DriverList: $e");
|
||||
}
|
||||
}
|
||||
|
||||
if (driverList.isNotEmpty) {
|
||||
Get.find<RideLifecycleController>()
|
||||
.processRideFinished(driverList, source: "FCM");
|
||||
}
|
||||
} else if (category == 'Finish Monitor') {
|
||||
// <-- كان "Finish Monitor".tr
|
||||
Get.defaultDialog(
|
||||
titleStyle: AppStyle.title,
|
||||
title: 'Trip finished '.tr,
|
||||
middleText: '',
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok'.tr,
|
||||
onPressed: () {
|
||||
Get.offAll(() => const MapPagePassenger());
|
||||
}));
|
||||
} else if (category == 'Cancel Trip from driver') {
|
||||
Log.print("🔔 FCM: Ride Cancelled by Driver received.");
|
||||
|
||||
// لا داعي لكتابة منطق التنظيف هنا، الكنترولر يتكفل بكل شيء
|
||||
if (Get.isRegistered<RideLifecycleController>()) {
|
||||
// استدعاء الحارس (سيتجاهل الأمر إذا كان السوكيت قد سبقه)
|
||||
Get.find<RideLifecycleController>()
|
||||
.processRideCancelledByDriver(message.data, source: "FCM");
|
||||
}
|
||||
|
||||
// إشعار محلي (اختياري، لأن الديالوج سيظهر)
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(
|
||||
'Trip Cancelled'.tr, 'The driver cancelled the trip.'.tr, 'cancel');
|
||||
}
|
||||
}
|
||||
// ... (باقي الحالات مثل Call Income, Call End, إلخ) ...
|
||||
// ... بنفس الطريقة ...
|
||||
|
||||
else if (category == 'incoming_call') {
|
||||
final sessionId = message.data['session_id'];
|
||||
final callerName = message.data['caller_name'];
|
||||
final rideId = message.data['ride_id'];
|
||||
if (sessionId != null && callerName != null && rideId != null) {
|
||||
Get.find<VoiceCallController>().receiveCall(
|
||||
sessionIdVal: sessionId.toString(),
|
||||
remoteNameVal: callerName.toString(),
|
||||
rideIdVal: rideId.toString(),
|
||||
);
|
||||
}
|
||||
} else if (category == 'Order Applied') {
|
||||
if (Platform.isAndroid) {
|
||||
notificationController.showNotification(
|
||||
'The order Accepted by another Driver'.tr,
|
||||
'We regret to inform you that another driver has accepted this order.'
|
||||
.tr,
|
||||
'order');
|
||||
}
|
||||
}
|
||||
}
|
||||
// Future<void> fireBaseTitles(RemoteMessage message) async {
|
||||
// if (message.notification!.title! == 'Order'.tr) {
|
||||
// Log.print('message: ${message}');
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification(
|
||||
// 'Order'.tr, message.notification!.body!, 'Order');
|
||||
// }
|
||||
// } else // ... داخل معالج الإشعارات في تطبيق الراكب ...
|
||||
|
||||
// if (message.notification!.title! == 'Accepted Ride') {
|
||||
// // ...
|
||||
|
||||
// // انظر هنا: قمنا بتغيير "passengerList" إلى "driverList"
|
||||
// var driverListJson = message.data['driverList'];
|
||||
|
||||
// // تأكد من أن البيانات ليست null قبل المتابعة
|
||||
// if (driverListJson != null) {
|
||||
// var myList = jsonDecode(driverListJson) as List<dynamic>;
|
||||
// Log.print('myList: ${myList}');
|
||||
|
||||
// final controller = Get.find<RideLifecycleController>();
|
||||
|
||||
// // استدعاء الدالة الموحدة الجديدة التي أنشأناها
|
||||
// await controller.processRideAcceptance(
|
||||
// driverIdFromFCM: myList[0].toString(),
|
||||
// rideIdFromFCM: myList[3].toString());
|
||||
// } else {
|
||||
// Log.print(
|
||||
// '❌ خطأ فادح: إشعار "Accepted Ride" وصل بدون بيانات (driverList is null)');
|
||||
// }
|
||||
// } else if (message.notification!.title! == 'Promo'.tr) {
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification(
|
||||
// 'Promo', 'Show latest promo'.tr, 'promo');
|
||||
// }
|
||||
// Get.to(const PromosPassengerPage());
|
||||
// } else if (message.notification!.title! == 'Trip Monitoring'.tr) {
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification(
|
||||
// 'Trip Monitoring'.tr, '', 'iphone_ringtone');
|
||||
// }
|
||||
// var myListString = message.data['DriverList'];
|
||||
// var myList = jsonDecode(myListString) as List<dynamic>;
|
||||
// Get.toNamed('/tripmonitor', arguments: {
|
||||
// 'rideId': myList[0].toString(),
|
||||
// 'driverId': myList[1].toString(),
|
||||
// });
|
||||
// } else if (message.notification!.title! == 'token change'.tr) {
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification(
|
||||
// 'token change'.tr, 'token change'.tr, 'cancel');
|
||||
// }
|
||||
// // GoogleSignInHelper.signOut();
|
||||
// } else if (message.notification!.title! == 'Driver Is Going To Passenger') {
|
||||
// Get.find<RideLifecycleController>().isDriverInPassengerWay = true;
|
||||
// Get.find<RideLifecycleController>().update();
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification('Driver is Going To You'.tr,
|
||||
// 'Please stay on the picked point.'.tr, 'tone1');
|
||||
// }
|
||||
// // Get.snackbar('Driver is Going To Passenger', '',
|
||||
// // backgroundColor: AppColor.greenColor);
|
||||
// } else if (message.notification!.title! == 'message From passenger') {
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification(
|
||||
// 'message From passenger'.tr, ''.tr, 'ding');
|
||||
// }
|
||||
// passengerDialog(message.notification!.body!);
|
||||
|
||||
// update();
|
||||
// } else if (message.notification!.title! == 'message From Driver') {
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification(
|
||||
// 'message From Driver'.tr, ''.tr, 'ding');
|
||||
// }
|
||||
// passengerDialog(message.notification!.body!);
|
||||
|
||||
// update();
|
||||
// } else // (هذا الكود في معالج الإشعارات لديك)
|
||||
// if (message.notification!.title! == 'Trip is Begin') {
|
||||
// Log.print('[FCM] استقبل إشعار "Trip is Begin".');
|
||||
|
||||
// // (تم حذف الإشعار المحلي من هنا، نُقل إلى الدالة الموحدة)
|
||||
|
||||
// final controller = Get.find<RideLifecycleController>();
|
||||
|
||||
// // استدعاء حارس البوابة الجديد والآمن
|
||||
// controller.processRideBegin();
|
||||
|
||||
// // (تم حذف كل الأوامر التالية من هنا)
|
||||
// // Get.find<RideLifecycleController>().getBeginRideFromDriver();
|
||||
// // box.write(BoxName.passengerWalletTotal, '0');
|
||||
// // update();
|
||||
// } else if (message.notification!.title! == 'Hi ,I will go now'.tr) {
|
||||
// // Get.snackbar('Hi ,I will go now', '',
|
||||
// // backgroundColor: AppColor.greenColor);
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification(
|
||||
// 'Passenger come to you'.tr, 'Hi ,I will go now'.tr, 'ding');
|
||||
// }
|
||||
// update();
|
||||
// } // ... داخل معالج الإشعارات (FCM Handler) ...
|
||||
// if (message.notification!.title! == 'Hi ,I Arrive your site'.tr) {
|
||||
// final controller = Get.find<RideLifecycleController>();
|
||||
|
||||
// // 1. التأكد أننا في الحالة الصحيحة (السائق كان في الطريق)
|
||||
// if (controller.currentRideState.value == RideState.driverApplied) {
|
||||
// Log.print('[FCM] السائق وصل. تغيير الحالة إلى driverArrived');
|
||||
|
||||
// // 2. تغيير الحالة فقط!
|
||||
// controller.currentRideState.value = RideState.driverArrived;
|
||||
// }
|
||||
// } else if (message.notification!.title! == "Cancel Trip from driver") {
|
||||
// Get.back();
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification("Cancel Trip from driver".tr,
|
||||
// "We will look for a new driver.\nPlease wait.".tr, 'cancel');
|
||||
// }
|
||||
// Get.defaultDialog(
|
||||
// title: "The driver canceled your ride.".tr,
|
||||
// middleText: "We will look for a new driver.\nPlease wait.".tr,
|
||||
// confirm: MyElevatedButton(
|
||||
// kolor: AppColor.greenColor,
|
||||
// title: 'Ok'.tr,
|
||||
// onPressed: () async {
|
||||
// Get.back();
|
||||
// await Get.find<RideLifecycleController>()
|
||||
// .reSearchAfterCanceledFromDriver();
|
||||
// },
|
||||
// ),
|
||||
// cancel: MyElevatedButton(
|
||||
// title: 'Cancel'.tr,
|
||||
// kolor: AppColor.redColor,
|
||||
// onPressed: () {
|
||||
// Get.offAll(() => const MapPagePassenger());
|
||||
// },
|
||||
// )
|
||||
// // Get.find<RideLifecycleController>()
|
||||
// // .searchNewDriverAfterRejectingFromDriver();
|
||||
// );
|
||||
// } else if (message.notification!.title! == 'Driver Finish Trip'.tr) {
|
||||
// // الخطوة 1: استقبل البيانات وتحقق من وجودها
|
||||
// final rawData = message.data['DriverList'];
|
||||
// List<dynamic> driverList = []; // ابدأ بقائمة فارغة كإجراء وقائي
|
||||
|
||||
// // الخطوة 2: قم بفك تشفير البيانات بأمان
|
||||
// if (rawData != null && rawData is String) {
|
||||
// try {
|
||||
// driverList = jsonDecode(rawData);
|
||||
// Log.print('Successfully decoded DriverList: $driverList');
|
||||
// } catch (e) {
|
||||
// Log.print('Error decoding DriverList JSON: $e');
|
||||
// // اترك القائمة فارغة في حالة حدوث خطأ
|
||||
// }
|
||||
// } else {
|
||||
// Log.print('Error: DriverList data is null or not a String.');
|
||||
// }
|
||||
|
||||
// // الخطوة 3: استخدم البيانات فقط إذا كانت القائمة تحتوي على العناصر المطلوبة
|
||||
// // هذا يمنع خطأ "RangeError" إذا كانت القائمة أقصر من المتوقع
|
||||
// if (driverList.length >= 4) {
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification(
|
||||
// "Driver Finish Trip".tr,
|
||||
// '${'you will pay to Driver'.tr} ${driverList[3].toString()} \$', // تم تحسين طريقة عرض النص
|
||||
// 'tone1');
|
||||
// }
|
||||
|
||||
// Get.find<AudioRecorderController>().stopRecording();
|
||||
|
||||
// if ((double.tryParse(
|
||||
// box.read(BoxName.passengerWalletTotal).toString()) ??
|
||||
// 0) <
|
||||
// 0) {
|
||||
// box.write(BoxName.passengerWalletTotal, 0);
|
||||
// }
|
||||
|
||||
// Get.find<RideLifecycleController>().tripFinishedFromDriver();
|
||||
|
||||
// NotificationController().showNotification(
|
||||
// 'Don’t forget your personal belongings.'.tr,
|
||||
// 'Please make sure you have all your personal belongings and that any remaining fare, if applicable, has been added to your wallet before leaving. Thank you for choosing the Siro app'
|
||||
// .tr,
|
||||
// 'ding');
|
||||
|
||||
// Get.to(() => RateDriverFromPassenger(), arguments: {
|
||||
// 'driverId': driverList[0].toString(),
|
||||
// 'rideId': driverList[1].toString(),
|
||||
// 'price': driverList[3].toString()
|
||||
// });
|
||||
// } else {
|
||||
// Log.print(
|
||||
// 'Error: Decoded driverList does not have enough elements. Received: $driverList');
|
||||
// // هنا يمكنك عرض رسالة خطأ للمستخدم إذا لزم الأمر
|
||||
// }
|
||||
// } else if (message.notification!.title! == "Finish Monitor".tr) {
|
||||
// Get.defaultDialog(
|
||||
// titleStyle: AppStyle.title,
|
||||
// title: 'Trip finished '.tr,
|
||||
// middleText: '',
|
||||
// confirm: MyElevatedButton(
|
||||
// title: 'Ok'.tr,
|
||||
// onPressed: () {
|
||||
// Get.offAll(() => const MapPagePassenger());
|
||||
// }));
|
||||
// }
|
||||
// // else if (message.notification!.title! == "Trip Monitoring".tr) {
|
||||
// // Get.to(() => const TripMonitor());
|
||||
// // }
|
||||
// else if (message.notification!.title! == 'Call Income') {
|
||||
// try {
|
||||
// var myListString = message.data['DriverList'];
|
||||
// var driverList = jsonDecode(myListString) as List<dynamic>;
|
||||
// // if (Platform.isAndroid) {
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification(
|
||||
// 'Call Income'.tr,
|
||||
// message.notification!.body!,
|
||||
// 'iphone_ringtone',
|
||||
// );
|
||||
// }
|
||||
// // }
|
||||
// // Assuming GetMaterialApp is initialized and context is valid for navigation
|
||||
// // Get.to(() => PassengerCallPage(
|
||||
// // channelName: driverList[1].toString(),
|
||||
// // token: driverList[0].toString(),
|
||||
// // remoteID: driverList[2].toString(),
|
||||
// // ));
|
||||
// } catch (e) { Log.print("Error occurred: $e"); }
|
||||
// } else if (message.notification!.title! == 'Call Income from Driver'.tr) {
|
||||
// try {
|
||||
// var myListString = message.data['DriverList'];
|
||||
// var driverList = jsonDecode(myListString) as List<dynamic>;
|
||||
// // if (Platform.isAndroid) {
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification(
|
||||
// 'Call Income'.tr,
|
||||
// message.notification!.body!,
|
||||
// 'iphone_ringtone',
|
||||
// );
|
||||
// }
|
||||
// // Assuming GetMaterialApp is initialized and context is valid for navigation
|
||||
// // Get.to(() => PassengerCallPage(
|
||||
// // channelName: driverList[1].toString(),
|
||||
// // token: driverList[0].toString(),
|
||||
// // remoteID: driverList[2].toString(),
|
||||
// // ));
|
||||
// } catch (e) { Log.print("Error occurred: $e"); }
|
||||
// } else if (message.notification!.title! == 'Call End'.tr) {
|
||||
// try {
|
||||
// var myListString = message.data['DriverList'];
|
||||
// var driverList = jsonDecode(myListString) as List<dynamic>;
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification(
|
||||
// 'Call End'.tr,
|
||||
// message.notification!.body!,
|
||||
// 'ding',
|
||||
// );
|
||||
// }
|
||||
// // Assuming GetMaterialApp is initialized and context is valid for navigation
|
||||
// // Get.off(const CallPage());
|
||||
// } catch (e) { Log.print("Error occurred: $e"); }
|
||||
// } else if (message.notification!.title! == 'Driver Cancelled Your Trip') {
|
||||
// // Get.snackbar(
|
||||
// // 'You will be pay the cost to driver or we will get it from you on next trip'
|
||||
// // .tr,
|
||||
// // 'message',
|
||||
// // backgroundColor: AppColor.redColor);
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification(
|
||||
// 'Driver Cancelled Your Trip'.tr,
|
||||
// 'you will pay to Driver you will be pay the cost of driver time look to your Siro Wallet'
|
||||
// .tr,
|
||||
// 'cancel');
|
||||
// }
|
||||
// box.write(BoxName.parentTripSelected, false);
|
||||
// box.remove(BoxName.tokenParent);
|
||||
|
||||
// Get.find<RideLifecycleController>().restCounter();
|
||||
// Get.offAll(() => const MapPagePassenger());
|
||||
// }
|
||||
// // else if (message.notification!.title! == 'Order Applied') {
|
||||
// // Get.snackbar(
|
||||
// // "The order has been accepted by another driver."
|
||||
// // .tr, // Corrected grammar
|
||||
// // "Be more mindful next time to avoid dropping orders."
|
||||
// // .tr, // Improved sentence structure
|
||||
// // backgroundColor: AppColor.yellowColor,
|
||||
// // snackPosition: SnackPosition.BOTTOM,
|
||||
// // );
|
||||
// // }
|
||||
|
||||
// else if (message.notification!.title! == 'Order Applied'.tr) {
|
||||
// if (Platform.isAndroid) {
|
||||
// notificationController.showNotification(
|
||||
// 'The order Accepted by another Driver'.tr,
|
||||
// 'We regret to inform you that another driver has accepted this order.'
|
||||
// .tr,
|
||||
// 'order');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
SnackbarController? driverAppliedTripSnakBar() {
|
||||
mySnackbarInfo('Driver Applied the Ride for You'.tr);
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<dynamic> passengerDialog(String message) {
|
||||
return Get.defaultDialog(
|
||||
barrierDismissible: false,
|
||||
title: message.tr,
|
||||
titleStyle: AppStyle.title,
|
||||
middleTextStyle: AppStyle.title,
|
||||
middleText: message.tr,
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Ok'.tr,
|
||||
onPressed: () {
|
||||
// Get.find<FirebaseMessagesController>().sendNotificationToPassengerToken(
|
||||
// 'Hi ,I will go now'.tr,
|
||||
// 'I will go now'.tr,
|
||||
// Get.find<RideLifecycleController>().driverToken, []);
|
||||
// Get.find<RideLifecycleController>()
|
||||
// .startTimerDriverWaitPassenger5Minute();
|
||||
|
||||
Get.back();
|
||||
}));
|
||||
}
|
||||
|
||||
Future<dynamic> driverFinishTripDialoge(List<dynamic> driverList) {
|
||||
return Get.defaultDialog(
|
||||
title: 'Driver Finish Trip'.tr,
|
||||
content: const DriverTipWidget(),
|
||||
confirm: MyElevatedButton(
|
||||
title: 'Yes'.tr,
|
||||
onPressed: () async {
|
||||
Get.to(() => RateDriverFromPassenger(), arguments: {
|
||||
'driverId': driverList[0].toString(),
|
||||
'rideId': driverList[1].toString(),
|
||||
'price': driverList[3].toString()
|
||||
});
|
||||
},
|
||||
kolor: AppColor.greenColor,
|
||||
),
|
||||
cancel: MyElevatedButton(
|
||||
title: 'No,I want'.tr,
|
||||
onPressed: () {
|
||||
Get.to(() => RateDriverFromPassenger(), arguments: {
|
||||
'driverId': driverList[0].toString(),
|
||||
'rideId': driverList[1].toString(),
|
||||
'price': driverList[3].toString()
|
||||
});
|
||||
},
|
||||
kolor: AppColor.redColor,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class DriverTipWidget extends StatelessWidget {
|
||||
const DriverTipWidget({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<RideLifecycleController>(builder: (controller) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// Text(
|
||||
// '${'Your fee is '.tr}${Get.find<RideLifecycleController>().totalPassenger.toStringAsFixed(2)}'),
|
||||
Text(
|
||||
'Do you want to pay Tips for this Driver'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
box.write(BoxName.tipPercentage, '0.05');
|
||||
|
||||
Toast.show(
|
||||
context,
|
||||
'${'Tip is '.tr}${(double.parse(controller.totalPassenger.toString())) * (double.parse(box.read(BoxName.tipPercentage.toString())))}',
|
||||
AppColor.cyanBlue);
|
||||
controller.update();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(border: Border.all()),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: Center(
|
||||
child: Text('5%'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
box.write(BoxName.tipPercentage, '0.10');
|
||||
Toast.show(
|
||||
context,
|
||||
'${'Tip is'.tr} ${(double.parse(controller.totalPassenger.toString())) * (double.parse(box.read(BoxName.tipPercentage.toString())))}',
|
||||
AppColor.cyanBlue);
|
||||
controller.update();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(border: Border.all()),
|
||||
child: const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(5),
|
||||
child: Text('10%'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
box.write(BoxName.tipPercentage, '0.15');
|
||||
Toast.show(
|
||||
context,
|
||||
'${'Tip is'.tr} ${(double.parse(controller.totalPassenger.toString())) * (double.parse(box.read(BoxName.tipPercentage.toString())))}',
|
||||
AppColor.cyanBlue);
|
||||
controller.update();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(border: Border.all()),
|
||||
child: const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(5),
|
||||
child: Text('15%'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
box.write(BoxName.tipPercentage, '0.20');
|
||||
Toast.show(
|
||||
context,
|
||||
'${'Tip is'.tr} ${(double.parse(controller.totalPassenger.toString())) * (double.parse(box.read(BoxName.tipPercentage.toString())))}',
|
||||
AppColor.cyanBlue);
|
||||
controller.update();
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(border: Border.all()),
|
||||
child: const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(5),
|
||||
child: Text('20%'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
MyElevatedButton(
|
||||
kolor: AppColor.redColor,
|
||||
title: 'No i want'.tr,
|
||||
onPressed: () {
|
||||
box.write(BoxName.tipPercentage, '0');
|
||||
controller.update();
|
||||
}),
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Text(
|
||||
'${(double.parse(controller.totalPassenger.toString())) * (double.parse(box.read(BoxName.tipPercentage.toString())))} ${box.read(BoxName.countryCode) == 'Egypt' ? 'LE'.tr : 'JOD'.tr}',
|
||||
style: AppStyle.title,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:siro_rider/print.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class LiveActivityScreen extends StatefulWidget {
|
||||
@override
|
||||
_LiveActivityScreenState createState() => _LiveActivityScreenState();
|
||||
}
|
||||
|
||||
class _LiveActivityScreenState extends State<LiveActivityScreen> {
|
||||
static const platform = MethodChannel('live_activity_channel');
|
||||
|
||||
Future<void> _startLiveActivity() async {
|
||||
try {
|
||||
await platform.invokeMethod('startLiveActivity');
|
||||
} on PlatformException catch (e) {
|
||||
Log.print("Failed to start Live Activity: '${e.message}'.");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateLiveActivity(double progress) async {
|
||||
try {
|
||||
await platform.invokeMethod('updateLiveActivity', {"progress": progress});
|
||||
} on PlatformException catch (e) {
|
||||
Log.print("Failed to update Live Activity: '${e.message}'.");
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _endLiveActivity() async {
|
||||
try {
|
||||
await platform.invokeMethod('endLiveActivity');
|
||||
} on PlatformException catch (e) {
|
||||
Log.print("Failed to end Live Activity: '${e.message}'.");
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text("Live Activity Test")),
|
||||
body: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: _startLiveActivity,
|
||||
child: Text("Start Live Activity"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => _updateLiveActivity(0.5),
|
||||
child: Text("Update Progress to 50%"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _endLiveActivity,
|
||||
child: Text("End Live Activity"),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
import 'package:siro_rider/print.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:timezone/data/latest.dart' as tz;
|
||||
import 'package:timezone/timezone.dart' as tz;
|
||||
|
||||
import '../../main.dart';
|
||||
|
||||
class NotificationController extends GetxController {
|
||||
final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
FlutterLocalNotificationsPlugin get plugin =>
|
||||
_flutterLocalNotificationsPlugin;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
initNotifications();
|
||||
}
|
||||
|
||||
// Initializes the local notifications plugin
|
||||
Future<void> initNotifications() async {
|
||||
const AndroidInitializationSettings android =
|
||||
AndroidInitializationSettings('@mipmap/launcher_icon');
|
||||
DarwinInitializationSettings ios = DarwinInitializationSettings(
|
||||
requestAlertPermission: true,
|
||||
requestBadgePermission: true,
|
||||
requestSoundPermission: true,
|
||||
// onDidReceiveLocalNotification:
|
||||
// (int id, String? title, String? body, String? payload) async {},
|
||||
);
|
||||
InitializationSettings initializationSettings =
|
||||
InitializationSettings(android: android, iOS: ios);
|
||||
await _flutterLocalNotificationsPlugin.initialize(
|
||||
settings: initializationSettings);
|
||||
|
||||
tz.initializeTimeZones();
|
||||
Log.print('Notifications initialized');
|
||||
}
|
||||
|
||||
// Displays a notification with the given title and message
|
||||
void showNotification(String title, String message, String tone) async {
|
||||
final AndroidNotificationDetails android = AndroidNotificationDetails(
|
||||
'high_importance_channel',
|
||||
'High Importance Notifications',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
showWhen: false,
|
||||
sound: RawResourceAndroidNotificationSound(tone),
|
||||
);
|
||||
|
||||
const DarwinNotificationDetails ios = DarwinNotificationDetails(
|
||||
sound: 'default',
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
);
|
||||
|
||||
final NotificationDetails details =
|
||||
NotificationDetails(android: android, iOS: ios);
|
||||
await _flutterLocalNotificationsPlugin.show(
|
||||
id: 0, title: title, body: message, notificationDetails: details);
|
||||
Log.print('Notification shown: $title - $message');
|
||||
}
|
||||
|
||||
// 🆕 Static method for background isolates where GetX might not be initialized
|
||||
static Future<void> showBackgroundNotification(String title, String message, String tone) async {
|
||||
final FlutterLocalNotificationsPlugin plugin = FlutterLocalNotificationsPlugin();
|
||||
const AndroidInitializationSettings android = AndroidInitializationSettings('@mipmap/launcher_icon');
|
||||
const DarwinInitializationSettings ios = DarwinInitializationSettings();
|
||||
await plugin.initialize(settings: const InitializationSettings(android: android, iOS: ios));
|
||||
|
||||
final AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
|
||||
'high_importance_channel',
|
||||
'High Importance Notifications',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
showWhen: false,
|
||||
sound: RawResourceAndroidNotificationSound(tone),
|
||||
);
|
||||
const DarwinNotificationDetails iosDetails = DarwinNotificationDetails(sound: 'default');
|
||||
|
||||
await plugin.show(id: 0, title: title, body: message, notificationDetails: NotificationDetails(android: androidDetails, iOS: iosDetails));
|
||||
Log.print('Background notification shown: $title - $message');
|
||||
}
|
||||
// /Users/hamzaaleghwairyeen/development/App/ride 2/lib/controller/firebase/local_notification.dart
|
||||
|
||||
// Assume _flutterLocalNotificationsPlugin is initialized somewhere in your code
|
||||
|
||||
// void scheduleNotificationsForSevenDays(
|
||||
// String title, String message, String tone) async {
|
||||
// final AndroidNotificationDetails android = AndroidNotificationDetails(
|
||||
// 'high_importance_channel',
|
||||
// 'High Importance Notifications',
|
||||
// importance: Importance.max,
|
||||
// priority: Priority.high,
|
||||
// sound: RawResourceAndroidNotificationSound(tone),
|
||||
// );
|
||||
|
||||
// const DarwinNotificationDetails ios = DarwinNotificationDetails(
|
||||
// sound: 'default',
|
||||
// presentAlert: true,
|
||||
// presentBadge: true,
|
||||
// presentSound: true,
|
||||
// );
|
||||
|
||||
// final NotificationDetails details =
|
||||
// NotificationDetails(android: android, iOS: ios);
|
||||
|
||||
// // Check for the exact alarm permission on Android 12 and above
|
||||
// if (Platform.isAndroid) {
|
||||
// if (await Permission.scheduleExactAlarm.isDenied) {
|
||||
// if (await Permission.scheduleExactAlarm.request().isGranted) {
|
||||
// Log.print('SCHEDULE_EXACT_ALARM permission granted');
|
||||
// } else {
|
||||
// Log.print('SCHEDULE_EXACT_ALARM permission denied');
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Schedule notifications for the next 7 days
|
||||
// for (int day = 0; day < 7; day++) {
|
||||
// // Schedule for 8:00 AM
|
||||
// await _scheduleNotificationForTime(
|
||||
// day, 8, 0, title, message, details, day * 1000 + 1);
|
||||
|
||||
// // Schedule for 3:00 PM
|
||||
// await _scheduleNotificationForTime(
|
||||
// day, 15, 0, title, message, details, day * 1000 + 2); // Unique ID
|
||||
|
||||
// // Schedule for 8:00 PM
|
||||
// await _scheduleNotificationForTime(
|
||||
// day, 20, 0, title, message, details, day * 1000 + 3); // Unique ID
|
||||
// }
|
||||
|
||||
// Log.print('Notifications scheduled successfully for the next 7 days');
|
||||
// }
|
||||
void scheduleNotificationsForSevenDays(
|
||||
String title, String message, String tone) async {
|
||||
final AndroidNotificationDetails android = AndroidNotificationDetails(
|
||||
'high_importance_channel',
|
||||
'High Importance Notifications',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
sound: RawResourceAndroidNotificationSound(tone),
|
||||
);
|
||||
|
||||
const DarwinNotificationDetails ios = DarwinNotificationDetails(
|
||||
sound: 'default',
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
);
|
||||
|
||||
final NotificationDetails details =
|
||||
NotificationDetails(android: android, iOS: ios);
|
||||
|
||||
// Check for the exact alarm permission on Android 12 and above
|
||||
if (Platform.isAndroid) {
|
||||
if (await Permission.scheduleExactAlarm.isDenied) {
|
||||
if (await Permission.scheduleExactAlarm.request().isGranted) {
|
||||
Log.print('SCHEDULE_EXACT_ALARM permission granted');
|
||||
} else {
|
||||
Log.print('SCHEDULE_EXACT_ALARM permission denied');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule notifications for the next 7 days
|
||||
for (int day = 0; day < 7; day++) {
|
||||
// List of notification times
|
||||
final notificationTimes = [
|
||||
{'hour': 8, 'minute': 0, 'id': day * 1000 + 1}, // 8:00 AM
|
||||
{'hour': 15, 'minute': 0, 'id': day * 1000 + 2}, // 3:00 PM
|
||||
{'hour': 20, 'minute': 0, 'id': day * 1000 + 3}, // 8:00 PM
|
||||
];
|
||||
|
||||
for (var time in notificationTimes) {
|
||||
final notificationId = time['id'] as int;
|
||||
|
||||
// Check if this notification ID is already stored
|
||||
bool isScheduled = box.read('notification_$notificationId') ?? false;
|
||||
|
||||
if (!isScheduled) {
|
||||
// Schedule the notification if not already scheduled
|
||||
await _scheduleNotificationForTime(
|
||||
day,
|
||||
time['hour'] as int,
|
||||
time['minute'] as int,
|
||||
title,
|
||||
message,
|
||||
details,
|
||||
notificationId,
|
||||
);
|
||||
|
||||
// Mark this notification ID as scheduled in GetStorage
|
||||
box.write('notification_$notificationId', true);
|
||||
} else {
|
||||
Log.print('Notification with ID $notificationId is already scheduled.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.print('Notifications scheduled successfully for the next 7 days');
|
||||
}
|
||||
|
||||
void scheduleNotificationsForTimeSelected(
|
||||
String title, String message, String tone, DateTime timeSelected) async {
|
||||
final AndroidNotificationDetails android = AndroidNotificationDetails(
|
||||
'high_importance_channel',
|
||||
'High Importance Notifications',
|
||||
importance: Importance.max,
|
||||
priority: Priority.high,
|
||||
sound: RawResourceAndroidNotificationSound(tone),
|
||||
);
|
||||
|
||||
const DarwinNotificationDetails ios = DarwinNotificationDetails(
|
||||
sound: 'default',
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
);
|
||||
|
||||
final NotificationDetails details =
|
||||
NotificationDetails(android: android, iOS: ios);
|
||||
|
||||
// Check for the exact alarm permission on Android 12 and above
|
||||
if (Platform.isAndroid) {
|
||||
if (await Permission.scheduleExactAlarm.isDenied) {
|
||||
if (await Permission.scheduleExactAlarm.request().isGranted) {
|
||||
Log.print('SCHEDULE_EXACT_ALARM permission granted');
|
||||
} else {
|
||||
Log.print('SCHEDULE_EXACT_ALARM permission denied');
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule notifications for 10 and 30 minutes before the timeSelected
|
||||
await _scheduleNotificationForTimeVIP(
|
||||
timeSelected.subtract(const Duration(minutes: 10)), // 10 minutes before
|
||||
title,
|
||||
message,
|
||||
details,
|
||||
1, // Unique ID for 10-minute before notification
|
||||
);
|
||||
|
||||
await _scheduleNotificationForTimeVIP(
|
||||
timeSelected.subtract(const Duration(minutes: 30)), // 30 minutes before
|
||||
title,
|
||||
message,
|
||||
details,
|
||||
2, // Unique ID for 30-minute before notification
|
||||
);
|
||||
|
||||
Log.print('Notifications scheduled successfully for the time selected');
|
||||
}
|
||||
|
||||
Future<void> _scheduleNotificationForTimeVIP(
|
||||
DateTime scheduledDate,
|
||||
String title,
|
||||
String message,
|
||||
NotificationDetails details,
|
||||
int notificationId,
|
||||
) async {
|
||||
// Initialize and set Cairo timezone
|
||||
tz.initializeTimeZones();
|
||||
var cairoLocation = tz.getLocation('Africa/Cairo');
|
||||
|
||||
final now = tz.TZDateTime.now(cairoLocation);
|
||||
|
||||
// Convert to Cairo time
|
||||
tz.TZDateTime scheduledTZDateTime =
|
||||
tz.TZDateTime.from(scheduledDate, cairoLocation);
|
||||
|
||||
// Check if 10 minutes before the scheduled time is in the past
|
||||
if (scheduledTZDateTime
|
||||
.subtract(const Duration(minutes: 10))
|
||||
.isBefore(now)) {
|
||||
// If the 10 minutes before the scheduled time is in the past, don't schedule
|
||||
Log.print(
|
||||
'Scheduled time minus 10 minutes is in the past. Skipping notification.');
|
||||
return; // Skip this notification
|
||||
}
|
||||
|
||||
Log.print('Current time (Cairo): $now');
|
||||
Log.print('Scheduling notification for: $scheduledTZDateTime');
|
||||
|
||||
await _flutterLocalNotificationsPlugin.zonedSchedule(
|
||||
id: notificationId, // Unique ID for each notification
|
||||
title: title,
|
||||
body: message,
|
||||
scheduledDate: scheduledTZDateTime,
|
||||
notificationDetails: details,
|
||||
androidScheduleMode: AndroidScheduleMode.exact,
|
||||
// uiLocalNotificationDateInterpretation:
|
||||
// UILocalNotificationDateInterpretation.absoluteTime,
|
||||
matchDateTimeComponents:
|
||||
null, // Don't repeat automatically; we handle manually
|
||||
);
|
||||
|
||||
Log.print('Notification scheduled successfully for: $scheduledTZDateTime');
|
||||
}
|
||||
|
||||
Future<void> _scheduleNotificationForTime(
|
||||
int dayOffset,
|
||||
int hour,
|
||||
int minute,
|
||||
String title,
|
||||
String message,
|
||||
NotificationDetails details,
|
||||
int notificationId,
|
||||
) async {
|
||||
// Initialize and set Cairo timezone
|
||||
tz.initializeTimeZones();
|
||||
var cairoLocation = tz.getLocation('Africa/Cairo');
|
||||
|
||||
final now = tz.TZDateTime.now(cairoLocation);
|
||||
tz.TZDateTime scheduledDate = tz.TZDateTime(
|
||||
cairoLocation,
|
||||
now.year,
|
||||
now.month,
|
||||
now.day + dayOffset, // Add offset to schedule for the next days
|
||||
hour,
|
||||
minute,
|
||||
);
|
||||
|
||||
// If the scheduled time is in the past, move it to the next day
|
||||
if (scheduledDate.isBefore(now)) {
|
||||
scheduledDate = scheduledDate.add(const Duration(days: 1));
|
||||
}
|
||||
|
||||
Log.print('Current time (Cairo): $now');
|
||||
Log.print('Scheduling notification for: $scheduledDate');
|
||||
|
||||
await _flutterLocalNotificationsPlugin.zonedSchedule(
|
||||
id: notificationId, // Unique ID for each notification
|
||||
title: title,
|
||||
body: message,
|
||||
scheduledDate: scheduledDate,
|
||||
notificationDetails: details,
|
||||
androidScheduleMode: AndroidScheduleMode.exact,
|
||||
// uiLocalNotificationDateInterpretation:
|
||||
// UILocalNotificationDateInterpretation.absoluteTime,
|
||||
matchDateTimeComponents:
|
||||
null, // Don't repeat automatically; we handle 7 days manually
|
||||
);
|
||||
|
||||
Log.print('Notification scheduled successfully for: $scheduledDate');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:get/get.dart';
|
||||
import '../../constant/links.dart';
|
||||
|
||||
import '../../print.dart'; // للترجمة .tr
|
||||
|
||||
class NotificationService {
|
||||
static String get _serverUrl => '${AppLink.server}/ride/firebase/send_fcm.php';
|
||||
|
||||
static Future<void> sendNotification({
|
||||
required String target,
|
||||
required String title,
|
||||
required String body,
|
||||
required String category, // إلزامي للتصنيف
|
||||
String? tone,
|
||||
List<String>? driverList,
|
||||
bool isTopic = false,
|
||||
}) async {
|
||||
try {
|
||||
// 1. تجهيز البيانات المخصصة (Data Payload)
|
||||
Map<String, dynamic> customData = {};
|
||||
|
||||
customData['category'] = category;
|
||||
|
||||
// إذا كان هناك قائمة سائقين/ركاب، نضعها هنا
|
||||
if (driverList != null && driverList.isNotEmpty) {
|
||||
// نرسلها كـ JSON String لأن FCM v1 يدعم String Values فقط في الـ data
|
||||
customData['driverList'] = jsonEncode(driverList);
|
||||
}
|
||||
|
||||
// 2. تجهيز الطلب الرئيسي للسيرفر
|
||||
final Map<String, dynamic> requestPayload = {
|
||||
'target': target,
|
||||
'title': title,
|
||||
'body': body,
|
||||
'isTopic': isTopic,
|
||||
'data':
|
||||
customData, // 🔥🔥 التغيير الجوهري: وضعنا البيانات داخل "data" 🔥🔥
|
||||
};
|
||||
|
||||
if (tone != null) {
|
||||
requestPayload['tone'] = tone;
|
||||
}
|
||||
|
||||
final response = await http.post(
|
||||
Uri.parse(_serverUrl),
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
},
|
||||
body: jsonEncode(requestPayload),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
Log.print('✅ Notification sent successfully.');
|
||||
// Log.print('Response: ${response.body}');
|
||||
} else {
|
||||
Log.print(
|
||||
'❌ Failed to send notification. Code: ${response.statusCode}');
|
||||
Log.print('Error Body: ${response.body}');
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('❌ Error sending notification: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user