// ignore_for_file: use_build_context_synchronously import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../app/injection_container.dart'; import '../../core/network/api_client.dart'; // --------------------------------------------------------------------------- // RegisterScreen // --------------------------------------------------------------------------- // // Step 1: Pilih role (Guardian / User) dengan UI card besar. // • Guardian: icon perisai, "I will guide someone" // • User: icon mata, "I need navigation assistance" // Step 2: Form email, password, displayName. // Submit → POST /auth/register → simpan pending email → dialog sukses → /login. // Jika User: dialog menampilkan uniqueUserId untuk dibagikan ke Guardian. // --------------------------------------------------------------------------- class RegisterScreen extends StatefulWidget { const RegisterScreen({super.key}); @override State createState() => _RegisterScreenState(); } class _RegisterScreenState extends State { final _name = TextEditingController(); final _email = TextEditingController(); final _password = TextEditingController(); bool _showPassword = false; String _role = 'USER'; bool _loading = false; int _step = 0; // 0 = role selection, 1 = form @override void dispose() { _name.dispose(); _email.dispose(); _password.dispose(); super.dispose(); } Future _register() async { if (_name.text.trim().isEmpty || _email.text.trim().isEmpty || _password.text.isEmpty) { _snack(context, 'Isi semua field terlebih dahulu.'); return; } setState(() => _loading = true); try { final res = await sl().dio.post('/auth/register', data: { 'displayName': _name.text.trim(), 'email': _email.text.trim(), 'password': _password.text, 'role': _role, }); final data = Map.from(res.data['data'] as Map); if (!mounted) return; final prefs = await SharedPreferences.getInstance(); await prefs.setString('pending_login_email', _email.text.trim()); await _showRegisterSuccess(context, data); if (mounted) context.go('/login'); } on DioException catch (e) { _snack(context, _friendlyDioMessage(e, fallback: 'Registrasi gagal')); } catch (e) { _snack(context, 'Registrasi gagal: $e'); } finally { if (mounted) setState(() => _loading = false); } } @override Widget build(BuildContext context) { return _AuthFrame( title: _step == 0 ? 'Create Account' : 'Fill Your Details', subtitle: _step == 0 ? 'Who are you in the WalkGuide system?' : _role == 'USER' ? 'User akan mendapat Unique ID untuk pairing.' : 'Guardian dapat monitor dan konfigurasi User.', child: _step == 0 ? _buildRoleStep(context) : _buildFormStep(context), ); } Widget _buildRoleStep(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _RoleCard( selected: _role == 'USER', icon: Icons.accessibility_new, title: 'User', subtitle: 'I need navigation assistance', onTap: () => setState(() => _role = 'USER'), ), const SizedBox(height: 12), _RoleCard( selected: _role == 'GUARDIAN', icon: Icons.shield_outlined, title: 'Guardian', subtitle: 'I will guide someone', onTap: () => setState(() => _role = 'GUARDIAN'), ), const SizedBox(height: 20), FilledButton.icon( onPressed: () => setState(() => _step = 1), icon: const Icon(Icons.arrow_forward), label: Text('Continue as $_role'), ), TextButton( onPressed: () => context.go('/login'), child: const Text('Sudah punya akun')), ], ); } Widget _buildFormStep(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Role indicator chip Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: _role == 'USER' ? const Color(0xFFEFF6FF) : const Color(0xFFF0FDF4), borderRadius: BorderRadius.circular(999), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( _role == 'USER' ? Icons.accessibility_new : Icons.shield_outlined, size: 16, color: _role == 'USER' ? const Color(0xFF1A56DB) : const Color(0xFF16A34A), ), const SizedBox(width: 6), Text( 'Daftar sebagai $_role', style: TextStyle( fontWeight: FontWeight.w700, color: _role == 'USER' ? const Color(0xFF1A56DB) : const Color(0xFF16A34A), ), ), const SizedBox(width: 6), GestureDetector( onTap: () => setState(() => _step = 0), child: const Icon(Icons.edit_outlined, size: 14), ), ], ), ), const SizedBox(height: 16), TextField( controller: _name, textInputAction: TextInputAction.next, decoration: const InputDecoration( labelText: 'Display name', prefixIcon: Icon(Icons.person_outline), )), const SizedBox(height: 12), 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: (_) => _register(), 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 : _register, icon: _loading ? const SizedBox( width: 18, height: 18, child: CircularProgressIndicator(strokeWidth: 2)) : const Icon(Icons.person_add_alt_1), label: const Text('Register'), ), TextButton( onPressed: () => context.go('/login'), child: const Text('Sudah punya akun')), ], ); } } // --------------------------------------------------------------------------- // _RoleCard // --------------------------------------------------------------------------- class _RoleCard extends StatelessWidget { final bool selected; final IconData icon; final String title; final String subtitle; final VoidCallback onTap; const _RoleCard({ required this.selected, required this.icon, required this.title, required this.subtitle, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 180), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: selected ? const Color(0xFFEFF6FF) : Colors.white, borderRadius: BorderRadius.circular(14), border: Border.all( color: selected ? const Color(0xFF1A56DB) : const Color(0xFFE2E8F0), width: selected ? 2 : 1, ), ), child: Row( children: [ Container( width: 48, height: 48, decoration: BoxDecoration( color: selected ? const Color(0xFF1A56DB) : const Color(0xFFF1F5F9), borderRadius: BorderRadius.circular(12), ), child: Icon(icon, color: selected ? Colors.white : const Color(0xFF64748B)), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle( fontWeight: FontWeight.w800, fontSize: 16)), Text(subtitle, style: const TextStyle( color: Color(0xFF64748B), fontSize: 13)), ], ), ), if (selected) const Icon(Icons.check_circle_rounded, color: Color(0xFF1A56DB)), ], ), ), ); } } // --------------------------------------------------------------------------- // 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( body: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 460), child: Card( elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(18), side: const BorderSide(color: Color(0xFFE2E8F0))), child: Padding( padding: const EdgeInsets.all(24), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Icon(Icons.navigation_rounded, color: Color(0xFF1A56DB), size: 42), const SizedBox(height: 14), Text(title, textAlign: TextAlign.center, style: Theme.of(context) .textTheme .headlineSmall ?.copyWith(fontWeight: FontWeight.w800)), const SizedBox(height: 4), Text(subtitle, textAlign: TextAlign.center, style: const TextStyle(color: Color(0xFF64748B))), const SizedBox(height: 22), child, ], ), ), ), ), ), ), ); } } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- Future _showRegisterSuccess( BuildContext context, Map data) async { final uniqueId = data['uniqueUserId']?.toString(); final message = uniqueId == null || uniqueId.isEmpty ? 'Registrasi berhasil. Silakan login.' : 'Registrasi berhasil!\n\nUnique User ID kamu:\n$uniqueId\n\nBagikan ID ini ke Guardian untuk pairing. Silakan login.'; _snack( context, uniqueId == null ? 'Registrasi berhasil.' : 'Registrasi berhasil. ID: $uniqueId'); await showDialog( context: context, builder: (dialogContext) => AlertDialog( title: const Text('Register Successful'), content: SelectableText(message), actions: [ FilledButton( onPressed: () => Navigator.of(dialogContext).pop(), child: const Text('Login sekarang'), ), ], ), ); } void _snack(BuildContext context, String message) { if (context.mounted) { ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(message))); } } String _friendlyDioMessage(DioException e, {required String fallback}) { final data = e.response?.data; if (data is Map && data['message'] != null) return data['message'].toString(); if (e.response?.statusCode == 409) { return 'Email sudah terdaftar. Gunakan email lain atau login.'; } if (e.type == DioExceptionType.connectionTimeout || e.type == DioExceptionType.receiveTimeout) { return 'Server terlalu lama merespons. Pastikan backend masih running.'; } if (e.type == DioExceptionType.connectionError) { return 'Tidak bisa ke server. Di HP pakai IP server, bukan localhost.'; } return fallback; }