/
opt
/
canhelp
/
apps
/
mobile
/
lib
/
/opt/canhelp/apps/mobile/lib
mkdir
upload
Name
Size
Mode
Actions
models/
-
0755
rm
providers/
-
0755
rm
routing/
-
0755
rm
screens/
-
0755
rm
services/
-
0755
rm
theme/
-
0755
rm
widgets/
-
0755
rm
main.dart
23712
0644
edit
dl
rm
Edit:
/opt/canhelp/apps/mobile/lib/main.dart
(23712B)
import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:app_links/app_links.dart'; import 'providers/admin_preview_provider.dart'; import 'providers/admin_provider.dart'; import 'providers/app_navigator.dart'; import 'providers/auth_provider.dart'; import 'providers/categories_provider.dart'; import 'providers/chat_provider.dart'; import 'providers/favorites_provider.dart'; import 'providers/locale_provider.dart'; import 'providers/locations_provider.dart'; import 'providers/availability_provider.dart'; import 'providers/notifications_provider.dart'; import 'providers/specialists_provider.dart'; import 'providers/tasks_provider.dart'; import 'screens/auth/login_screen.dart'; import 'screens/auth/register_screen.dart'; import 'screens/auth/forgot_password_screen.dart'; import 'screens/auth/reset_password_screen.dart'; import 'screens/admin/admin_screen.dart'; import 'screens/admin/admin_users_screen.dart'; import 'screens/admin/admin_user_detail_screen.dart'; import 'screens/admin/admin_tasks_screen.dart'; import 'screens/admin/admin_reports_screen.dart'; import 'screens/admin/admin_offers_moderation_screen.dart'; import 'screens/admin/admin_support_screen.dart'; import 'screens/chat/chat_room_screen.dart'; import 'screens/chat/chat_screen.dart'; import 'screens/dashboard/dashboard_screen.dart'; import 'screens/favorites/favorites_screen.dart'; import 'screens/home/home_screen.dart'; import 'screens/schedule/schedule_screen.dart'; import 'screens/home/main_shell.dart'; import 'screens/notifications/notifications_screen.dart'; import 'screens/map/map_screen.dart'; import 'screens/search/categories_screen.dart'; import 'screens/search/search_screen.dart'; import 'screens/profile/profile_screen.dart'; import 'screens/profile/edit_profile_screen.dart'; import 'screens/profile/profile_cards_screen.dart'; import 'screens/profile/profile_notifications_screen.dart'; import 'screens/profile/profile_balance_history_screen.dart'; import 'screens/profile/profile_plan_history_screen.dart'; import 'screens/profile/profile_reviews_screen.dart'; import 'screens/profile/profile_referral_screen.dart'; import 'screens/profile/profile_status_screen.dart'; import 'screens/profile/profile_portfolio_screen.dart'; import 'screens/profile/profile_topup_screen.dart'; import 'screens/profile/profile_site_screen.dart'; import 'screens/profile/profile_blocked_users_screen.dart'; import 'screens/profile/support_ticket_screen.dart'; import 'screens/profile/payment_result_screen.dart'; import 'screens/home/become_specialist_screen.dart'; import 'screens/profile/specialist_setup_screen.dart'; import 'screens/pricing/pricing_screen.dart'; import 'screens/specialists/specialist_profile_screen.dart'; import 'screens/tasks/create_task_screen.dart'; import 'screens/tasks/make_offer_screen.dart'; import 'screens/tasks/task_detail_screen.dart'; import 'routing/auth_redirect.dart'; import 'services/api_client.dart'; import 'services/push_service.dart'; import 'services/storage_service.dart'; import 'theme/app_theme.dart'; void main() { WidgetsFlutterBinding.ensureInitialized(); runApp(const CanHelpApp()); } class CanHelpApp extends StatefulWidget { const CanHelpApp({super.key}); @override State<CanHelpApp> createState() => _CanHelpAppState(); } class _CanHelpAppState extends State<CanHelpApp> { // ── Services ────────────────────────────────────────────── late final ApiClient _api; late final StorageService _storage; late final PushService _push; late final AppLinks _appLinks; StreamSubscription<Uri>? _deepLinkSub; // ── Providers (created once) ────────────────────────────── late final AuthProvider _auth; late final LocaleProvider _locale; late final TasksProvider _tasks; late final CategoriesProvider _categories; late final LocationsProvider _locations; late final SpecialistsProvider _specialists; late final NotificationsProvider _notifications; late final FavoritesProvider _favorites; late final ChatProvider _chat; late final AvailabilityProvider _availability; late final AppNavigator _appNavigator; late final AdminProvider _admin; late final AdminPreviewProvider _adminPreview; // ── Router (created once, refreshes on auth change) ─────── late final GoRouter _router; final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>(); static const _protectedPaths = defaultProtectedPaths; @override void initState() { super.initState(); _api = ApiClient(); _storage = StorageService(); _auth = AuthProvider(api: _api, storage: _storage); _locale = LocaleProvider(api: _api, storage: _storage); _tasks = TasksProvider(api: _api); _categories = CategoriesProvider(api: _api); _locations = LocationsProvider(api: _api); _specialists = SpecialistsProvider(api: _api); _notifications = NotificationsProvider(api: _api); _favorites = FavoritesProvider(api: _api); _chat = ChatProvider(api: _api); _availability = AvailabilityProvider(api: _api); _appNavigator = AppNavigator(); _admin = AdminProvider(api: _api); _adminPreview = AdminPreviewProvider(api: _api); _push = PushService(api: _api, auth: _auth, storage: _storage); _router = GoRouter( refreshListenable: _auth, redirect: (context, state) { return resolveAuthRedirect( isAuthLoading: _auth.isLoading, isAuthenticated: _auth.isAuthenticated, matchedLocation: state.matchedLocation, protectedPaths: _protectedPaths, ); }, routes: [ // ── Shell routes (persistent bottom nav) ────────────── StatefulShellRoute.indexedStack( builder: (context, state, navigationShell) => MainShell(navigationShell: navigationShell), branches: [ StatefulShellBranch( routes: [ GoRoute( path: '/', pageBuilder: (_, __) => const NoTransitionPage(child: HomeScreen()), ), ], ), StatefulShellBranch( routes: [ GoRoute( path: '/search', pageBuilder: (context, state) => NoTransitionPage( child: SearchScreen( initialQuery: state.uri.queryParameters['q'], initialCategory: state.uri.queryParameters['category'], initialMode: state.uri.queryParameters['mode'], ), ), ), ], ), StatefulShellBranch( routes: [ GoRoute( path: '/chat', pageBuilder: (_, __) => const NoTransitionPage(child: ChatScreen()), ), ], ), StatefulShellBranch( routes: [ GoRoute( path: '/profile', pageBuilder: (_, __) => const NoTransitionPage(child: ProfileScreen()), ), ], ), ], ), // ── Notifications (standalone, accessed from home bell) ── GoRoute( path: '/notifications', builder: (_, __) => const NotificationsScreen(), ), // ── Auth routes ───────────────────────────────────────── GoRoute( path: '/login', pageBuilder: (_, __) => const NoTransitionPage(child: LoginScreen()), ), GoRoute( path: '/register', pageBuilder: (context, state) => CustomTransitionPage<void>( key: state.pageKey, child: const RegisterScreen(), transitionDuration: const Duration(milliseconds: 280), reverseTransitionDuration: const Duration(milliseconds: 220), transitionsBuilder: (context, animation, secondaryAnimation, child) { final curved = CurvedAnimation( parent: animation, curve: Curves.easeOutCubic, reverseCurve: Curves.easeInCubic, ); return FadeTransition(opacity: curved, child: child); }, ), ), GoRoute( path: '/forgot-password', builder: (context, state) => const ForgotPasswordScreen(), ), GoRoute( path: '/reset-password', builder: (context, state) => ResetPasswordScreen( token: state.uri.queryParameters['token'] ?? '', ), ), // ── Task list / Search (backwards-compatible route) ───── GoRoute( path: '/tasks', builder: (context, state) => SearchScreen( initialCategory: state.uri.queryParameters['category'], initialQuery: state.uri.queryParameters['q'], ), ), // ── Map ───────────────────────────────────────────────── GoRoute(path: '/map', builder: (context, state) => const MapScreen()), // ── Categories list ─────────────────────────────────────── GoRoute( path: '/categories', builder: (context, state) => const CategoriesScreen(), ), // ── Task detail routes ─────────────────────────────────── GoRoute( path: '/tasks/new', builder: (context, state) => CreateTaskScreen( categorySlug: state.uri.queryParameters['category'], ), ), GoRoute( path: '/tasks/:id/offer', builder: (context, state) => MakeOfferScreen(taskId: state.pathParameters['id']!), ), GoRoute( path: '/tasks/:id', builder: (context, state) => TaskDetailScreen(taskId: state.pathParameters['id']!), ), // ── Specialist profile ─────────────────────────────────── GoRoute( path: '/specialists/:id', builder: (context, state) => SpecialistProfileScreen(userId: state.pathParameters['id']!), ), // ── Chat room ──────────────────────────────────────────── GoRoute( path: '/chat/:roomId', builder: (context, state) => ChatRoomScreen(roomId: state.pathParameters['roomId']!), ), // ── Other full-screen routes ───────────────────────────── // ── Favorites ──────────────────────────────────────────────── GoRoute( path: '/favorites', builder: (context, state) => const FavoritesScreen(), ), GoRoute( path: '/dashboard', builder: (context, state) => const DashboardScreen(), ), GoRoute( path: '/schedule', builder: (context, state) => const ScheduleScreen(), ), GoRoute( path: '/pricing', builder: (context, state) => const PricingScreen(), ), GoRoute( path: '/support', builder: (context, state) => const SupportTicketScreen(), ), GoRoute( path: '/payment', builder: (context, state) => PaymentResultScreen( status: state.uri.queryParameters['status'], ref: state.uri.queryParameters['ref'], ), ), // ── Profile sub-routes ─────────────────────────────────── GoRoute( path: '/profile/edit', builder: (context, state) => const EditProfileScreen(), ), GoRoute( path: '/profile/cards', builder: (context, state) => const ProfileCardsScreen(), ), GoRoute( path: '/profile/notifications', builder: (context, state) => const ProfileNotificationsScreen(), ), GoRoute( path: '/profile/blocked-users', builder: (context, state) => const ProfileBlockedUsersScreen(), ), GoRoute( path: '/profile/balance-history', builder: (context, state) => const ProfileBalanceHistoryScreen(), ), GoRoute( path: '/profile/plan-history', builder: (context, state) => const ProfilePlanHistoryScreen(), ), GoRoute( path: '/profile/reviews', builder: (context, state) => const ProfileReviewsScreen(), ), GoRoute( path: '/profile/referral', builder: (context, state) => const ProfileReferralScreen(), ), GoRoute( path: '/profile/status', builder: (context, state) => const ProfileStatusScreen(), ), GoRoute( path: '/profile/portfolio', builder: (context, state) => const ProfilePortfolioScreen(), ), GoRoute( path: '/profile/topup', builder: (context, state) => const ProfileTopupScreen(), ), GoRoute( path: '/profile/site', builder: (context, state) => const ProfileSiteScreen(), ), GoRoute( path: '/become-specialist', builder: (context, state) => const BecomeSpecialistScreen(), ), GoRoute( path: '/specialist-setup', builder: (context, state) => const SpecialistSetupScreen(), ), // ── Admin routes ───────────────────────────────────────────── GoRoute( path: '/admin', builder: (context, state) => const AdminScreen(), ), GoRoute( path: '/admin/users', builder: (context, state) => const AdminUsersScreen(), ), GoRoute( path: '/admin/users/:id', builder: (context, state) => AdminUserDetailScreen(userId: state.pathParameters['id']!), ), GoRoute( path: '/admin/tasks', builder: (context, state) => const AdminTasksScreen(), ), GoRoute( path: '/admin/reports', builder: (context, state) => const AdminReportsScreen(), ), GoRoute( path: '/admin/offers-moderation', builder: (context, state) => const AdminOffersModerationScreen(), ), GoRoute( path: '/admin/support', builder: (context, state) => const AdminSupportScreen(), ), ], ); _push.init( onForegroundMessage: _handleForegroundPush, onMessageTap: _handlePushTap, ); _initDeepLinks(); } @override void dispose() { _deepLinkSub?.cancel(); _push.dispose(); _auth.dispose(); _locale.dispose(); _tasks.dispose(); _categories.dispose(); _locations.dispose(); _specialists.dispose(); _notifications.dispose(); _favorites.dispose(); _chat.dispose(); _availability.dispose(); _appNavigator.dispose(); _admin.dispose(); _adminPreview.dispose(); _router.dispose(); super.dispose(); } void _initDeepLinks() { _appLinks = AppLinks(); // Handle deep link that launched the app cold _appLinks .getInitialLink() .then((uri) { if (uri != null) _handleDeepLink(uri); }) .catchError((_) {}); // Handle deep links while app is running _deepLinkSub = _appLinks.uriLinkStream.listen( _handleDeepLink, onError: (_) {}, ); } void _handlePushTap(Map<String, String> data) { final type = (data['type'] ?? '').toLowerCase(); final referenceId = data['referenceId']; WidgetsBinding.instance.addPostFrameCallback((_) { String? targetRoute; if (type == 'message' || type == 'new_message') { targetRoute = (referenceId != null && referenceId.isNotEmpty) ? '/chat/$referenceId' : '/chat'; } else if (type == 'support_ticket_reply') { targetRoute = '/support'; } else if (type == 'referral_reward') { targetRoute = '/profile/referral'; } else if (type == 'specialist_card_submitted' || type == 'specialist_card_approved' || type == 'specialist_card_rejected') { targetRoute = '/profile/cards'; } else if (type == 'review' || type == 'new_review') { targetRoute = '/profile/reviews'; } else if ((type == 'new_offer' || type == 'offer_accepted' || type == 'offer_declined' || type == 'task_updated' || type == 'task_completed' || type == 'task_cancelled' || type == 'task_invite' || type == 'task_deadline_reminder' || type == 'task_archived') && referenceId != null && referenceId.isNotEmpty) { targetRoute = '/tasks/$referenceId'; } else { targetRoute = '/notifications'; } _router.go('/'); WidgetsBinding.instance.addPostFrameCallback((_) { if (targetRoute != null && targetRoute.isNotEmpty) { _router.push(targetRoute); } }); }); } void _handleForegroundPush(Map<String, String> data) { if (_auth.isAuthenticated) { _notifications.load(); } final messenger = _scaffoldMessengerKey.currentState; if (messenger == null) return; final type = (data['type'] ?? '').toLowerCase(); final visual = _pushVisualForType(type); final title = (data['title'] ?? '').trim().isNotEmpty ? data['title']!.trim() : _locale.t('nav.notifications', 'Notification'); final body = (data['body'] ?? '').trim(); messenger ..hideCurrentSnackBar() ..showSnackBar( SnackBar( content: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( margin: const EdgeInsets.only(top: 1), child: Icon(visual.icon, color: visual.color, size: 20), ), const SizedBox(width: 10), Expanded( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontWeight: FontWeight.w700, color: AppTheme.textPrimary, ), ), if (body.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 2), child: Text( body, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle(color: AppTheme.textSecondary), ), ), ], ), ), ], ), behavior: SnackBarBehavior.floating, backgroundColor: AppTheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppTheme.buttonRadius), side: BorderSide(color: visual.color.withValues(alpha: 0.35)), ), duration: const Duration(seconds: 5), action: SnackBarAction( label: _locale.t('common.open', 'Open'), textColor: visual.color, onPressed: () => _handlePushTap(data), ), ), ); } _PushVisual _pushVisualForType(String type) { switch (type) { case 'message': case 'new_message': return const _PushVisual( icon: Icons.chat_bubble_outline, color: AppTheme.primary, ); case 'new_offer': case 'offer_accepted': case 'offer_declined': case 'task_invite': return const _PushVisual( icon: Icons.local_offer_outlined, color: AppTheme.warning, ); case 'support_ticket_reply': return const _PushVisual( icon: Icons.support_agent_outlined, color: AppTheme.emerald, ); case 'referral_reward': return const _PushVisual( icon: Icons.redeem_outlined, color: AppTheme.amber, ); default: return const _PushVisual( icon: Icons.notifications_outlined, color: AppTheme.textSecondary, ); } } void _handleDeepLink(Uri uri) { if (uri.scheme != 'canhelp') return; final path = uri.path.isEmpty ? '/${uri.host}' : uri.path; final query = uri.query.isNotEmpty ? '?${uri.query}' : ''; _router.go('$path$query'); } @override Widget build(BuildContext context) { return AnnotatedRegion<SystemUiOverlayStyle>( value: const SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.dark, statusBarBrightness: Brightness.light, systemNavigationBarColor: Colors.white, systemNavigationBarIconBrightness: Brightness.dark, ), child: MultiProvider( providers: [ ChangeNotifierProvider.value(value: _auth), ChangeNotifierProvider.value(value: _locale), ChangeNotifierProvider.value(value: _tasks), ChangeNotifierProvider.value(value: _categories), ChangeNotifierProvider.value(value: _locations), ChangeNotifierProvider.value(value: _specialists), ChangeNotifierProvider.value(value: _notifications), ChangeNotifierProvider.value(value: _favorites), ChangeNotifierProvider.value(value: _chat), ChangeNotifierProvider.value(value: _availability), ChangeNotifierProvider.value(value: _appNavigator), ChangeNotifierProvider.value(value: _admin), ChangeNotifierProvider.value(value: _adminPreview), ], child: MaterialApp.router( title: 'CanHelp', debugShowCheckedModeBanner: false, scaffoldMessengerKey: _scaffoldMessengerKey, theme: AppTheme.light, darkTheme: AppTheme.dark, themeMode: ThemeMode.light, routerConfig: _router, ), ), ); } } class _PushVisual { final IconData icon; final Color color; const _PushVisual({required this.icon, required this.color}); }
Save
cmd:
run