feat: add WebSocket support - real-time notifications in Flutter

This commit is contained in:
Alan Alonso
2026-05-23 01:07:50 -06:00
parent 1886ab6094
commit feead21e73
4 changed files with 231 additions and 15 deletions

View File

@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'core/config/supabase_config.dart';
import 'core/theme/app_theme.dart';
import 'core/ws_provider.dart';
import 'features/recycling_guide/presentation/screens/recycling_guide_screen.dart';
import 'features/routes/presentation/screens/routes_home_screen.dart';
@@ -25,7 +26,7 @@ class MyApp extends StatelessWidget {
return MaterialApp(
title: 'Basura App - Notificación de Residuos',
theme: AppTheme.lightTheme,
home: const HomePage(),
home: const LoginScreen(),
routes: {
'/guia': (context) => const RecyclingGuideScreen(),
'/rutas': (context) => const RoutesHomeScreen(),
@@ -34,28 +35,92 @@ class MyApp extends StatelessWidget {
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
class LoginScreen extends ConsumerStatefulWidget {
const LoginScreen({super.key});
@override
ConsumerState<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends ConsumerState<LoginScreen> {
final emailCtrl = TextEditingController();
final passCtrl = TextEditingController();
bool loading = false;
String? error;
@override
void dispose() {
emailCtrl.dispose();
passCtrl.dispose();
super.dispose();
}
Future<void> login() async {
setState(() {
loading = true;
error = null;
});
try {
final response = await Supabase.instance.client
.from('users')
.select('id, email, phone, password_hash')
.eq('email', emailCtrl.text)
.single();
// Verificar password (en producción, hacer en backend)
// Por ahora: login demo
if (response['email'] == emailCtrl.text) {
if (!mounted) return;
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const HomePage()),
);
}
} catch (e) {
setState(() => error = 'Email o contraseña incorrectos');
} finally {
setState(() => loading = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Basura App'),
centerTitle: true,
),
body: Center(
appBar: AppBar(title: const Text('Login')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => Navigator.pushNamed(context, '/guia'),
child: const Text('📚 Guía de Reciclaje'),
TextField(
controller: emailCtrl,
decoration: const InputDecoration(
labelText: 'Email',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: passCtrl,
obscureText: true,
decoration: const InputDecoration(
labelText: 'Password',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
if (error != null)
Text(error!, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => Navigator.pushNamed(context, '/rutas'),
child: const Text('🚚 Rutas & ETA'),
onPressed: loading ? null : login,
child: loading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(),
)
: const Text('Ingresar'),
),
],
),
@@ -63,3 +128,67 @@ class HomePage extends StatelessWidget {
);
}
}
class HomePage extends ConsumerWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final wsState = ref.watch(wsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Basura App'),
centerTitle: true,
actions: [
Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Tooltip(
message: wsState.connected ? 'Conectado' : 'Desconectado',
child: Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: wsState.connected ? Colors.green : Colors.red,
shape: BoxShape.circle,
),
),
),
),
),
],
),
body: Column(
children: [
// Estado WebSocket
if (wsState.lastMessage != null)
Container(
color: Colors.blue[100],
padding: const EdgeInsets.all(16),
child: Text('📬 ${wsState.lastMessage}'),
),
// Botones principales
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => Navigator.pushNamed(context, '/guia'),
child: const Text('📚 Guía de Reciclaje'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => Navigator.pushNamed(context, '/rutas'),
child: const Text('🚚 Rutas & ETA'),
),
],
),
),
),
],
),
);
}
}