قرار المالك 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>
222 lines
7.4 KiB
Dart
222 lines
7.4 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io' show Platform;
|
|
import 'package:flutter/foundation.dart' show debugPrint;
|
|
import 'package:flutter/services.dart';
|
|
|
|
/// Model for trip data passed to the overlay
|
|
class TripData {
|
|
final String tripId;
|
|
final String passengerName;
|
|
final String pickupAddress;
|
|
final String dropoffAddress;
|
|
final double distanceKm;
|
|
final double estimatedFare;
|
|
final int estimatedMinutes;
|
|
final double pickupLat;
|
|
final double pickupLng;
|
|
final String? passengerAvatarUrl;
|
|
|
|
TripData({
|
|
required this.tripId,
|
|
required this.passengerName,
|
|
required this.pickupAddress,
|
|
required this.dropoffAddress,
|
|
required this.distanceKm,
|
|
required this.estimatedFare,
|
|
required this.estimatedMinutes,
|
|
required this.pickupLat,
|
|
required this.pickupLng,
|
|
this.passengerAvatarUrl,
|
|
});
|
|
|
|
Map<String, dynamic> toMap() => {
|
|
'tripId': tripId,
|
|
'passengerName': passengerName,
|
|
'pickupAddress': pickupAddress,
|
|
'dropoffAddress': dropoffAddress,
|
|
'distanceKm': distanceKm,
|
|
'estimatedFare': estimatedFare,
|
|
'estimatedMinutes': estimatedMinutes,
|
|
'pickupLat': pickupLat,
|
|
'pickupLng': pickupLng,
|
|
'passengerAvatarUrl': passengerAvatarUrl ?? '',
|
|
};
|
|
|
|
factory TripData.fromMap(Map<String, dynamic> map) => TripData(
|
|
tripId: map['tripId'] ?? '',
|
|
passengerName: map['passengerName'] ?? '',
|
|
pickupAddress: map['pickupAddress'] ?? '',
|
|
dropoffAddress: map['dropoffAddress'] ?? '',
|
|
distanceKm: (map['distanceKm'] ?? 0.0).toDouble(),
|
|
estimatedFare: (map['estimatedFare'] ?? 0.0).toDouble(),
|
|
estimatedMinutes: map['estimatedMinutes'] ?? 0,
|
|
pickupLat: (map['pickupLat'] ?? 0.0).toDouble(),
|
|
pickupLng: (map['pickupLng'] ?? 0.0).toDouble(),
|
|
passengerAvatarUrl: map['passengerAvatarUrl'],
|
|
);
|
|
|
|
factory TripData.fromJson(String json) =>
|
|
TripData.fromMap(jsonDecode(json) as Map<String, dynamic>);
|
|
|
|
String toJson() => jsonEncode(toMap());
|
|
}
|
|
|
|
/// Result returned when the driver accepts a trip
|
|
class TripAcceptedResult {
|
|
final String tripId;
|
|
final DateTime acceptedAt;
|
|
|
|
TripAcceptedResult({required this.tripId, required this.acceptedAt});
|
|
}
|
|
|
|
/// Main plugin class — single entry point for Flutter side
|
|
class TripOverlayPlugin {
|
|
static const MethodChannel _channel = MethodChannel('trip_overlay_plugin');
|
|
|
|
// Stream controller for trip accepted events coming FROM Android overlay
|
|
static final StreamController<TripAcceptedResult> _tripAcceptedController =
|
|
StreamController<TripAcceptedResult>.broadcast();
|
|
|
|
// Stream controller for trip rejected/expired events
|
|
static final StreamController<String> _tripRejectedController =
|
|
StreamController<String>.broadcast();
|
|
|
|
static bool _isInitialized = false;
|
|
|
|
/// Stream that fires when the driver taps "Accept" in the overlay
|
|
static Stream<TripAcceptedResult> get onTripAccepted =>
|
|
_tripAcceptedController.stream;
|
|
|
|
/// Stream that fires when the driver rejects or overlay times out
|
|
static Stream<String> get onTripRejected => _tripRejectedController.stream;
|
|
|
|
/// Initialize the plugin — call this once in main() or initState()
|
|
static Future<void> initialize() async {
|
|
if (_isInitialized) return;
|
|
_channel.setMethodCallHandler(_handleMethodCall);
|
|
_isInitialized = true;
|
|
}
|
|
|
|
/// Handle incoming calls FROM Android → Flutter
|
|
static Future<dynamic> _handleMethodCall(MethodCall call) async {
|
|
switch (call.method) {
|
|
case 'onTripAccepted':
|
|
final tripId = call.arguments['tripId'] as String;
|
|
_tripAcceptedController.add(
|
|
TripAcceptedResult(tripId: tripId, acceptedAt: DateTime.now()),
|
|
);
|
|
break;
|
|
|
|
case 'onTripRejected':
|
|
final tripId = call.arguments['tripId'] as String;
|
|
_tripRejectedController.add(tripId);
|
|
break;
|
|
|
|
default:
|
|
throw PlatformException(
|
|
code: 'UNKNOWN_METHOD',
|
|
message: 'Method ${call.method} not implemented',
|
|
);
|
|
}
|
|
}
|
|
|
|
/// هذه الإضافة **أندرويد فقط** — لا يوجد تنفيذ iOS ولا مجلد ios/ ولا مدخل
|
|
/// في pubspec.plugin.platforms. iOS لا يسمح بنوافذ فوق التطبيقات أصلاً.
|
|
///
|
|
/// بلا هذا الحرس كان أي نداء يرمي MissingPluginException على iOS، وأخطر
|
|
/// موضع: backgroundMessageHandler ينادي showOverlay **قبل** حفظ
|
|
/// pending_driver_list، فيسقط الاستثناء ويُلغي الحفظ ⇒ يفتح الكابتن الإشعار
|
|
/// فلا يجد طلباً وتبدأ الشاشة من الصفر.
|
|
static bool get isSupported => Platform.isAndroid;
|
|
|
|
/// Check if SYSTEM_ALERT_WINDOW permission is granted
|
|
static Future<bool> isPermissionGranted() async {
|
|
if (!isSupported) return false;
|
|
try {
|
|
final result = await _channel.invokeMethod<bool>('isPermissionGranted');
|
|
return result ?? false;
|
|
} on PlatformException catch (e) {
|
|
debugPrint('[TripOverlay] isPermissionGranted failed: ${e.message}');
|
|
return false;
|
|
} on MissingPluginException {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Open system settings to grant SYSTEM_ALERT_WINDOW permission
|
|
static Future<void> requestPermission() async {
|
|
if (!isSupported) return;
|
|
try {
|
|
await _channel.invokeMethod('requestPermission');
|
|
} on PlatformException catch (e) {
|
|
debugPrint('[TripOverlay] requestPermission failed: ${e.message}');
|
|
} on MissingPluginException {
|
|
// لا شيء — غير مدعوم على هذه المنصة
|
|
}
|
|
}
|
|
|
|
/// Show the trip overlay with the given [tripData]
|
|
/// [autoCloseSeconds] — how long before auto-dismiss (default 30s)
|
|
///
|
|
/// يرجع false ولا يرمي أبداً: المُنادي يجب أن يكمل مساره (حفظ الطلب المعلّق،
|
|
/// الإشعار المحلي) حتى لو تعذّرت النافذة.
|
|
static Future<bool> showOverlay(
|
|
TripData tripData, {
|
|
int autoCloseSeconds = 30,
|
|
}) async {
|
|
if (!isSupported) return false;
|
|
|
|
final granted = await isPermissionGranted();
|
|
if (!granted) {
|
|
await requestPermission();
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
final result = await _channel.invokeMethod<bool>('showOverlay', {
|
|
'tripData': tripData.toJson(),
|
|
'autoCloseSeconds': autoCloseSeconds,
|
|
});
|
|
return result ?? false;
|
|
} on PlatformException catch (e) {
|
|
debugPrint('[TripOverlay] showOverlay failed: ${e.message}');
|
|
return false;
|
|
} on MissingPluginException {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Programmatically close the overlay (e.g. if trip was cancelled)
|
|
static Future<void> hideOverlay() async {
|
|
if (!isSupported) return;
|
|
try {
|
|
await _channel.invokeMethod('hideOverlay');
|
|
} on PlatformException catch (e) {
|
|
debugPrint('[TripOverlay] hideOverlay failed: ${e.message}');
|
|
} on MissingPluginException {
|
|
// لا شيء
|
|
}
|
|
}
|
|
|
|
/// Check if the overlay is currently visible
|
|
static Future<bool> isOverlayActive() async {
|
|
if (!isSupported) return false;
|
|
try {
|
|
final result = await _channel.invokeMethod<bool>('isOverlayActive');
|
|
return result ?? false;
|
|
} on PlatformException catch (e) {
|
|
debugPrint('[TripOverlay] isOverlayActive failed: ${e.message}');
|
|
return false;
|
|
} on MissingPluginException {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Dispose streams — call in app's dispose()
|
|
static void dispose() {
|
|
_tripAcceptedController.close();
|
|
_tripRejectedController.close();
|
|
}
|
|
}
|