import 'dart:io'; import 'package:miler/helpers/http_overrides.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:get/get.dart'; import 'package:miler/helpers/app_bootstrap.dart'; import 'package:miler/views/offline/offline_banner.dart'; import 'package:miler/views/helpers/constants/Colorconstants.dart'; import 'package:miler/controllers/profile_controller.dart'; import 'package:miler/controllers/riderlog.dart'; import 'package:miler/controllers/pickup.dart'; import 'package:miler/controllers/logcontroller.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:miler/providers/notifications/notificationservce.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:new_version_plus/new_version_plus.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:miler/background/backgroundservice.dart'; import 'package:miler/helpers/shift_end_alarm.dart'; import 'package:miler/views/helpers/constants/app_theme.dart'; import 'package:miler/views/helpers/widgets/page_transitions.dart'; import 'views/offline/offline_page.dart'; // Background message handler @pragma('vm:entry-point') Future firebaseMessagingBackgroundHandler(RemoteMessage message) async { await Firebase.initializeApp(); await NotificationServce.display(message); } String currentVersion = ''; String storeVersion = ''; bool updateRequired = false; Future getAppVersion() async { SharedPreferences prefs = await SharedPreferences.getInstance(); PackageInfo packageInfo = await PackageInfo.fromPlatform(); String version = packageInfo.version; currentVersion = version; prefs.setString('CurrentVersion', currentVersion); debugPrint('Current version from main: $currentVersion'); } Future checkForUpdate(BuildContext context) async { // Version check is handled in splash screen } Future recheckVersion() async { final newVersion = NewVersionPlus( iOSId: '284882215', androidId: "com.doormile.partner", ); try { final status = await newVersion.getVersionStatus(); if (status != null) { debugPrint( "Recheck - Current: ${status.localVersion}, Store: ${status.storeVersion}", ); return status.canUpdate; } } catch (e) { debugPrint("Error rechecking version: $e"); } return false; } Future main() async { WidgetsFlutterBinding.ensureInitialized(); HttpOverrides.global = MyHttpOverrides(); try { await Firebase.initializeApp(); FirebaseMessaging.onBackgroundMessage(firebaseMessagingBackgroundHandler); } catch (e) { debugPrint('[FIREBASE] Initialization failed: $e'); // Firebase may not be configured for this platform (e.g. iOS simulator without GoogleService-Info.plist) } // Loaded at boot, not on first visit to Account: the Home app bar draws the // rider's photo, so the controller has to be populated before the first // frame or the avatar flashes its letter fallback on every launch. await Get.put(ProfileController(), permanent: true).loadFromPrefs(); Get.put(RiderLogController(), permanent: true); Get.put(PickupController(), permanent: true); final logController = Get.put(LogController(), permanent: true); await logController.startLogging(); _applySystemChrome(); runApp(const _RootApp()); } /// Paints the status and navigation bars to match the page behind them. void _applySystemChrome() { SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( statusBarColor: ColorConstants.surface, statusBarIconBrightness: Brightness.dark, statusBarBrightness: Brightness.light, systemNavigationBarColor: ColorConstants.surface, systemNavigationBarIconBrightness: Brightness.dark, ), ); } class _RootApp extends StatefulWidget { const _RootApp(); @override State<_RootApp> createState() => _RootAppState(); } class _RootAppState extends State<_RootApp> with WidgetsBindingObserver { List _connectionStatus = [ConnectivityResult.none]; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); Connectivity().onConnectivityChanged.listen((result) { if (mounted) { setState(() { _connectionStatus = result; }); } }); Connectivity().checkConnectivity().then((result) { if (mounted) { setState(() { _connectionStatus = result; }); } }); // ✅ Check if shift ended while app was killed OR if shift end time has already passed _checkShiftEndOnStartup(); // Decides banner-vs-blocker only. See [_hasSession]. _readSessionPresence(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } /// Check if shift ended while app was killed OR if shift end time has already passed /// This will trigger break log immediately if rider is on duty and shift end time passed Future _checkShiftEndOnStartup() async { try { final prefs = await SharedPreferences.getInstance(); final int onduty = prefs.getInt('onduty') ?? 0; // Only check if rider was on duty if (onduty != 1) { debugPrint('[APP_STARTUP] Rider not on duty, skipping shift end check'); return; } // Check if shift has ended final String endTimeStr = prefs.getString('endtime') ?? ''; if (endTimeStr.isEmpty) { debugPrint('[APP_STARTUP] No endtime found, skipping shift end check'); return; } debugPrint( '[APP_STARTUP] Checking shift end - Current time: ${DateTime.now()}, End time: $endTimeStr', ); // Use the same logic as BackgroundCollectionLog.checkShiftEnd() final now = DateTime.now(); final endParts = endTimeStr.split(':'); if (endParts.length < 2) return; final int endHour = int.tryParse(endParts[0]) ?? 0; final int endMinute = int.tryParse(endParts[1]) ?? 0; final int endSecond = endParts.length > 2 ? (int.tryParse(endParts[2]) ?? 0) : 0; final DateTime endToday = DateTime( now.year, now.month, now.day, endHour, endMinute, endSecond, ); bool isShiftOver = false; final String startTimeStr = prefs.getString('starttime') ?? ''; if (startTimeStr.isNotEmpty) { final startParts = startTimeStr.split(':'); if (startParts.length >= 2) { final int startHour = int.tryParse(startParts[0]) ?? 0; final int startMinute = int.tryParse(startParts[1]) ?? 0; final double startVal = startHour + (startMinute / 60.0); final double endVal = endHour + (endMinute / 60.0); if (startVal > endVal) { // Overnight shift final DateTime startToday = DateTime( now.year, now.month, now.day, startHour, startMinute, ); if (now.isAfter(endToday) && now.isBefore(startToday)) { isShiftOver = true; } } else { // Normal day shift if (now.isAfter(endToday)) { isShiftOver = true; } } } else { if (now.isAfter(endToday)) { isShiftOver = true; } } } else { if (now.isAfter(endToday)) { isShiftOver = true; } } if (isShiftOver) { debugPrint( '[APP_STARTUP] ⏰ Shift end time ($endTimeStr) has already passed - creating break log IMMEDIATELY...', ); // Call the background service to create break log await BackgroundCollectionLog.checkShiftEnd(); debugPrint('[APP_STARTUP] ✅ Break log created (if needed)'); } else { debugPrint( '[APP_STARTUP] Shift end time ($endTimeStr) has not passed yet - scheduling alarm', ); // Also reschedule the alarm in case it was cancelled try { final String startTime = prefs.getString('starttime') ?? ''; await ShiftEndAlarm.scheduleAlarm( endTime: endTimeStr, startTime: startTime, ); } catch (e) { debugPrint('[APP_STARTUP] Error rescheduling alarm: $e'); } } } catch (e) { debugPrint('[APP_STARTUP] Error checking shift end: $e'); } } @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); if (state == AppLifecycleState.resumed) { // When app comes back to foreground, ensure rider logs resume try { if (Get.isRegistered()) { final ctl = Get.find(); // Fire an immediate rider log (best-effort) ctl.createLoginNowV2(); // Ensure the periodic background/foreground loop is running ctl.startAutoCreateLoginLoop(); } } catch (_) { // Ignore lifecycle errors; app should not crash because of logging } } } bool get _isOffline { return _connectionStatus.isEmpty || _connectionStatus.every((status) => status == ConnectivityResult.none); } /// Whether there is a signed-in rider whose work is already on the device. /// /// Read once, purely to choose between two *presentations* of being offline — /// it changes no auth behaviour and no routing. [AppBootstrap] still decides /// where the app actually goes. bool _hasSession = false; Future _readSessionPresence() async { try { final prefs = await SharedPreferences.getInstance(); final loggedOut = prefs.getBool('logged_out') ?? false; final userId = prefs.getInt('userid') ?? prefs.getInt('userId') ?? 0; if (mounted) setState(() => _hasSession = !loggedOut && userId != 0); } catch (_) { // Unknown → treat as no session, which is the more cautious of the two // presentations. It only affects which offline UI is shown. } } @override Widget build(BuildContext context) { // ⭐ RESPONSIVE WRAPPER ADDED return ScreenUtilInit( designSize: const Size(390, 844), // YOUR BASE SIZE minTextAdapt: true, splitScreenMode: true, builder: (_, __) { return GetMaterialApp( debugShowCheckedModeBanner: false, title: 'Miler partner', // Smooth, consistent page motion across the whole app (Uber-style // horizontal slide with a gentle ease) instead of abrupt cuts. // Screens navigated to by hand use `openScreen`/`openSheet` from // views/helpers/widgets/page_transitions.dart; this covers the // whole-app swaps (`Get.offAll`) that don't go through them. defaultTransition: Transition.cupertino, transitionDuration: kMilerPushIn, theme: AppTheme.light, home: const AppBootstrap(), // ── Offline is a strip, not a wall ── // // This used to stack an opaque [OfflinePage] over the whole app the // moment connectivity dropped — so a rider at a doorstep in a // basement lost the address, the customer, the parcel count and the // OTP field while holding the parcel. Everything he needs at the door // was already fetched; the network is required to *report* the stop, // not to work it. // // So a signed-in rider keeps his screen and gets a line of text above // it. The full-screen page survives only for a cold start with no // session, where there genuinely is nothing to show him. builder: (context, child) { final baseChild = child ?? const SizedBox.shrink(); if (_isOffline && !_hasSession) { return Stack(children: [baseChild, const OfflinePage()]); } return OfflineScope(offline: _isOffline, child: baseChild); }, ); }, ); } }