import 'package:cached_network_image/cached_network_image.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:go_router/go_router.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import '../firebase_options.dart'; final bootstrapProvider = FutureProvider((ref) async { await dotenv.load(fileName: 'assets/.env'); // Inicializar Firebase (si hay DefaultFirebaseOptions, úsalas; sino, intenta initializeApp() y espera que haya google-services/Info.plist) final FirebaseOptions? options = DefaultFirebaseOptions.currentPlatform; if (options != null) { await Firebase.initializeApp(options: options); } else { await Firebase.initializeApp(); } // Registrar handler para mensajes en background FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); }); // Handler top-level requerido por FCM Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { // Asegurar Firebase inicializado en background isolate try { await Firebase.initializeApp(); } catch (_) { // ignore if already initialized } // Aquí puedes procesar y guardar la notificación si hace falta debugPrint( 'FCM background message received: ${message.messageId} | data: ${message.data}', ); } final apiClientProvider = Provider((ref) { final baseUrl = dotenv.env['API_BASE_URL'] ?? 'http://10.0.2.2:8000'; return Dio( BaseOptions( baseUrl: baseUrl, connectTimeout: const Duration(seconds: 15), receiveTimeout: const Duration(seconds: 15), headers: const {'Content-Type': 'application/json'}, ), ); }); final secureStorageProvider = Provider((ref) { return const FlutterSecureStorage(); }); final routerProvider = Provider((ref) { return GoRouter( initialLocation: '/home', routes: [ GoRoute( path: '/home', name: 'home', builder: (context, state) => const HomePage(), ), GoRoute( path: '/status', name: 'status', builder: (context, state) => const StatusPage(), ), ], ); }); class RecolectaApp extends ConsumerWidget { const RecolectaApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final bootstrap = ref.watch(bootstrapProvider); return bootstrap.when( loading: () => const MaterialApp( debugShowCheckedModeBanner: false, home: BootstrapLoadingPage(), ), error: (error, stackTrace) => MaterialApp( debugShowCheckedModeBanner: false, home: BootstrapErrorPage(error: error), ), data: (_) => MaterialApp.router( debugShowCheckedModeBanner: false, title: 'Recolecta', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF1F6F78)), scaffoldBackgroundColor: const Color(0xFFF4F7F6), useMaterial3: true, ), routerConfig: ref.watch(routerProvider), ), ); } } class BootstrapLoadingPage extends StatelessWidget { const BootstrapLoadingPage({super.key}); @override Widget build(BuildContext context) { return const Scaffold(body: Center(child: CircularProgressIndicator())); } } class BootstrapErrorPage extends StatelessWidget { const BootstrapErrorPage({super.key, required this.error}); final Object error; @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Padding( padding: const EdgeInsets.all(24), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.error_outline, size: 48), const SizedBox(height: 16), const Text( 'No se pudo cargar la configuración inicial.', textAlign: TextAlign.center, ), const SizedBox(height: 12), Text( error.toString(), textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall, ), ], ), ), ), ); } } class HomePage extends ConsumerWidget { const HomePage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final dio = ref.read(apiClientProvider); final storage = ref.read(secureStorageProvider); final baseUrl = dio.options.baseUrl; return Scaffold( appBar: AppBar( title: const Text('Recolecta'), actions: [ IconButton( onPressed: () => context.goNamed('status'), icon: const Icon(Icons.route), tooltip: 'Estado', ), ], ), body: Padding( padding: const EdgeInsets.all(24), child: ListView( children: [ const Text( 'Bootstrap listo', style: TextStyle(fontSize: 28, fontWeight: FontWeight.w700), ), const SizedBox(height: 8), Text( 'La app ya carga .env, Riverpod y GoRouter para la base del MVP.', style: Theme.of(context).textTheme.bodyLarge, ), const SizedBox(height: 24), _InfoCard(title: 'API base URL', value: baseUrl, icon: Icons.cloud), const SizedBox(height: 16), _InfoCard( title: 'Secure Storage', value: storage.runtimeType.toString(), icon: Icons.lock, ), const SizedBox(height: 16), _ImageCard( title: 'Widget listo para caché de imágenes', imageUrl: 'https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=800&q=80', ), ], ), ), ); } } class StatusPage extends StatelessWidget { const StatusPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Estado')), body: const Padding( padding: EdgeInsets.all(24), child: Text( 'Aquí después se conectarán ETA, notificaciones, ruta asignada y métricas de privacidad.', ), ), ); } } class _InfoCard extends StatelessWidget { const _InfoCard({ required this.title, required this.value, required this.icon, }); final String title; final String value; final IconData icon; @override Widget build(BuildContext context) { return Card( elevation: 0, child: ListTile( leading: Icon(icon), title: Text(title), subtitle: Text(value), ), ); } } class _ImageCard extends StatelessWidget { const _ImageCard({required this.title, required this.imageUrl}); final String title; final String imageUrl; @override Widget build(BuildContext context) { return Card( clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CachedNetworkImage( imageUrl: imageUrl, height: 180, width: double.infinity, fit: BoxFit.cover, placeholder: (context, url) => const SizedBox( height: 180, child: Center(child: CircularProgressIndicator()), ), errorWidget: (context, url, error) => const SizedBox( height: 180, child: Center(child: Icon(Icons.image_not_supported)), ), ), Padding(padding: const EdgeInsets.all(16), child: Text(title)), ], ), ); } }