// ignore_for_file: use_build_context_synchronously import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../app/app_cubit.dart'; import '../../app/router.dart'; import '../../app/injection_container.dart'; import '../../core/constants/app_constants.dart'; import '../../core/errors/friendly_error.dart'; import '../../core/network/api_client.dart'; import '../../core/services/fcm_service.dart'; import '../../core/services/incoming_call_polling_service.dart'; import '../../core/services/offline_queue_service.dart'; import '../../core/services/tts_service.dart'; import '../../core/services/websocket_service.dart'; import '../../core/storage/secure_storage.dart'; // --------------------------------------------------------------------------- // LoginScreen // --------------------------------------------------------------------------- class LoginScreen extends StatefulWidget { const LoginScreen({super.key}); @override State createState() => _LoginScreenState(); } class _LoginScreenState extends State { final _email = TextEditingController(); final _password = TextEditingController(); bool _loading = false; bool _showPassword = false; @override void initState() { super.initState(); _loadPendingLoginEmail(); } @override void dispose() { _email.dispose(); _password.dispose(); super.dispose(); } Future _loadPendingLoginEmail() async { final prefs = await SharedPreferences.getInstance(); final pendingEmail = prefs.getString('pending_login_email'); if (!mounted) return; setState(() { if (pendingEmail != null && pendingEmail.isNotEmpty) { _email.text = pendingEmail; } }); await prefs.remove('pending_login_email'); } Future _login() async { if (_email.text.trim().isEmpty || _password.text.isEmpty) { _snack(context, 'Isi email dan password dulu.'); return; } setState(() => _loading = true); await runFriendlyAction( () async { final res = await sl().dio.post('/auth/login', data: { 'email': _email.text.trim(), 'password': _password.text, }); await _saveAuthAndRoute( context, Map.from(res.data['data'] as Map)); }, onError: (message) => _snack(context, message), fallback: 'Login gagal. Periksa email dan password kamu.', connectionHint: 'Tidak bisa ke server. Pakai URL backend publik/aktif.', ); if (mounted) setState(() => _loading = false); } @override Widget build(BuildContext context) { return _AuthFrame( title: 'Sign in', subtitle: 'Masuk ke navigasi asistif realtime WalkGuide.', child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ TextField( controller: _email, keyboardType: TextInputType.emailAddress, textInputAction: TextInputAction.next, decoration: const InputDecoration( labelText: 'Email', prefixIcon: Icon(Icons.email_outlined), )), const SizedBox(height: 12), TextField( controller: _password, obscureText: !_showPassword, textInputAction: TextInputAction.done, onSubmitted: (_) => _login(), decoration: InputDecoration( labelText: 'Password', prefixIcon: const Icon(Icons.lock_outline), suffixIcon: IconButton( icon: Icon( _showPassword ? Icons.visibility_off : Icons.visibility), onPressed: () => setState(() => _showPassword = !_showPassword), ), )), const SizedBox(height: 18), FilledButton.icon( onPressed: _loading ? null : _login, icon: _loading ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.login), label: const Text('Login'), ), TextButton( onPressed: () => context.go('/register'), child: const Text('Buat akun baru')), ], ), ); } } // --------------------------------------------------------------------------- // Shared private widgets // --------------------------------------------------------------------------- class _AuthFrame extends StatelessWidget { final String title; final String subtitle; final Widget child; const _AuthFrame( {required this.title, required this.subtitle, required this.child}); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFEAF4FF), body: LayoutBuilder( builder: (context, constraints) { final compact = constraints.maxWidth < 480 || constraints.maxHeight < 720; return Stack( children: [ const Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFD9EEFF), Color(0xFFF7FAFC)], ), ), ), ), Positioned( top: compact ? -70 : -90, right: compact ? -70 : -60, child: Container( width: compact ? 180 : 260, height: compact ? 180 : 260, decoration: BoxDecoration( color: const Color(0xFF2563EB).withValues(alpha: 0.12), shape: BoxShape.circle, ), ), ), Center( child: SingleChildScrollView( keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, padding: EdgeInsets.fromLTRB( compact ? 14 : 24, compact ? 12 : 24, compact ? 14 : 24, 20 + MediaQuery.of(context).viewInsets.bottom, ), child: ConstrainedBox( constraints: BoxConstraints(maxWidth: compact ? 380 : 430), child: TweenAnimationBuilder( tween: Tween(begin: 18, end: 0), duration: const Duration(milliseconds: 520), curve: Curves.easeOutCubic, builder: (_, offset, child) => Opacity( opacity: (1 - offset / 18).clamp(0.0, 1.0), child: Transform.translate( offset: Offset(0, offset), child: child, ), ), child: RepaintBoundary( child: Container( decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.96), borderRadius: BorderRadius.circular(compact ? 22 : 30), border: Border.all( color: Colors.white.withValues(alpha: 0.8)), boxShadow: [ BoxShadow( color: const Color(0xFF1E3A8A) .withValues(alpha: 0.14), blurRadius: compact ? 24 : 40, offset: const Offset(0, 18), ), ], ), child: Padding( padding: EdgeInsets.fromLTRB( compact ? 18 : 24, compact ? 18 : 26, compact ? 18 : 24, compact ? 18 : 24, ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Container( width: compact ? 44 : 56, height: compact ? 44 : 56, decoration: BoxDecoration( gradient: const LinearGradient( colors: [ Color(0xFF2563EB), Color(0xFF0891B2) ], ), borderRadius: BorderRadius.circular(16), ), child: Icon(Icons.navigation_rounded, color: Colors.white, size: compact ? 26 : 30), ), const SizedBox(width: 12), const Expanded( child: Text( 'WalkGuide', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w900, color: Color(0xFF0F172A), ), ), ), ], ), SizedBox(height: compact ? 14 : 16), if (!compact) Container( padding: const EdgeInsets.symmetric( horizontal: 10, vertical: 6), decoration: BoxDecoration( color: const Color(0xFFEFF6FF), borderRadius: BorderRadius.circular(999), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.shield_outlined, size: 14, color: Color(0xFF1D4ED8)), SizedBox(width: 6), Text( 'Secure Assistive Navigation', style: TextStyle( color: Color(0xFF1D4ED8), fontSize: 11, fontWeight: FontWeight.w800, ), ), ], ), ), if (!compact) const SizedBox(height: 18), Text( title, maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of(context) .textTheme .headlineMedium ?.copyWith( fontSize: compact ? 26 : null, fontWeight: FontWeight.w900, color: const Color(0xFF0F172A), ), ), const SizedBox(height: 6), Text( subtitle, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Color(0xFF64748B), height: 1.35, ), ), SizedBox(height: compact ? 18 : 26), child, ], ), ), ), ), ), ), ), ), ], ); }, ), ); } } // --------------------------------------------------------------------------- // Helpers (shared for login flow) // --------------------------------------------------------------------------- Future _saveAuthAndRoute( BuildContext context, Map data) async { await sl().saveTokens( accessToken: data['accessToken'], refreshToken: data['refreshToken'], role: data['role'], userId: data['userId'].toString(), displayName: data['displayName'], uniqueUserId: data['uniqueUserId'], ); final serverUrl = await AppConstants.getServerUrl(); if (serverUrl != null) { context .read() .setSession(role: data['role'], serverUrl: serverUrl); _startPostLoginServices(serverUrl); } sl().speak('Selamat datang ${data['displayName'] ?? ''}'); if (context.mounted) { context.go(data['role'] == 'ROLE_GUARDIAN' ? '/guardian/dashboard' : '/user/walkguide'); } } void _startPostLoginServices(String serverUrl) { Future.microtask(() async { sl().start(); await sl().init().timeout(const Duration(seconds: 4)); final ws = sl(); await ws.connect(serverUrl).timeout(const Duration(seconds: 2)); ws.subscribeCall((data) { final type = data['type']?.toString(); if (type == 'INCOMING_CALL') { appRouter.go('/incoming-call', extra: data); } }); await sl() .syncPending(sl()) .timeout(const Duration(seconds: 3)); }).catchError((Object e) { debugPrint('Post-login services skipped: $e'); }); } void _snack(BuildContext context, String message) { if (context.mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(message))); } }