import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import '../config/api_config.dart'; import '../models/app_user.dart'; class AuthException implements Exception { const AuthException(this.message, {this.errors = const {}}); final String message; final Map> errors; @override String toString() => message; } class AuthService { static const String _loggedInKey = 'auth_logged_in'; static const String _tokenKey = 'auth_access_token'; static const String _nameKey = 'auth_user_name'; static const String _emailKey = 'auth_user_email'; Future get savedName async { final preferences = await SharedPreferences.getInstance(); return _normalizeName(preferences.getString(_nameKey)); } Future register({ required String name, required String email, required String password, required String passwordConfirmation, }) async { if (password != passwordConfirmation) { throw const AuthException('Konfirmasi password tidak sesuai.'); } try { final response = await _post('/register', body: { 'name': name.trim(), 'email': email.trim(), 'password': password, 'password_confirmation': passwordConfirmation, }); final payload = _decodeResponse(response); _throwIfUnsuccessful(response, payload); final responseUser = _userFromPayload(payload); final user = responseUser.copyWith( name: _normalizeName(responseUser.name), email: responseUser.email.isEmpty ? email.trim() : responseUser.email, ); await _saveUser(user, loggedIn: false); return user; } on AuthException { rethrow; } catch (_) { throw const AuthException( 'Tidak bisa terhubung ke server. Periksa koneksi dan alamat API.', ); } } Future login({ required String email, required String password, }) async { try { final response = await _post('/login', body: { 'email': email.trim(), 'password': password, }); final payload = _decodeResponse(response); _throwIfUnsuccessful(response, payload); final user = _userFromPayload(payload); final token = _stringAt(payload, ['data', 'token']); if (token.isEmpty) { throw const AuthException('Token login tidak ditemukan dari server.'); } await _saveUser(user, token: token, loggedIn: true); return user; } on AuthException { rethrow; } catch (_) { throw const AuthException( 'Tidak bisa terhubung ke server. Periksa koneksi dan alamat API.', ); } } Future profile() async { final preferences = await SharedPreferences.getInstance(); final loggedIn = preferences.getBool(_loggedInKey) ?? false; if (!loggedIn) return null; final token = preferences.getString(_tokenKey); final email = preferences.getString(_emailKey); final fallbackUser = email == null ? null : AppUser( id: 0, name: _normalizeName(preferences.getString(_nameKey)), email: email, ); if (token == null || token.isEmpty) return fallbackUser; try { final response = await http .get( ApiConfig.apiUri('/profile'), headers: _headers(token: token), ) .timeout(const Duration(seconds: 10)); final payload = _decodeResponse(response); _throwIfUnsuccessful(response, payload); final user = _userFromPayload(payload); await _saveUser(user, token: token, loggedIn: true); return user; } on AuthException { rethrow; } catch (_) { return fallbackUser; } } Future logout() async { final preferences = await SharedPreferences.getInstance(); final token = preferences.getString(_tokenKey); if (token != null && token.isNotEmpty) { try { await _post('/logout', token: token); } catch (_) { // Session lokal tetap dibersihkan meski server logout gagal. } } await preferences.setBool(_loggedInKey, false); await preferences.remove(_tokenKey); } Future clearSession() async { final preferences = await SharedPreferences.getInstance(); await preferences.setBool(_loggedInKey, false); await preferences.remove(_tokenKey); } Future checkApiHealth() async => true; Future checkLaravelApiHealth() async { try { final response = await http .get(ApiConfig.apiUri('/health')) .timeout(const Duration(seconds: 3)); return response.statusCode >= 200 && response.statusCode < 500; } catch (_) { return false; } } Future _saveUser( AppUser user, { String? token, required bool loggedIn, }) async { final preferences = await SharedPreferences.getInstance(); await preferences.setBool(_loggedInKey, loggedIn); await preferences.setString(_nameKey, _normalizeName(user.name)); await preferences.setString(_emailKey, user.email); if (token != null && token.isNotEmpty) { await preferences.setString(_tokenKey, token); } } Future _post( String path, { Map? body, String? token, }) async { final url = ApiConfig.apiUri(path); final isLoginRequest = path == '/login' || path == 'login'; if (isLoginRequest) { debugPrint('LOGIN REQUEST METHOD: POST'); debugPrint('LOGIN REQUEST URL: $url'); } try { final response = await http .post( url, headers: _headers(token: token), body: jsonEncode(body ?? const {}), ) .timeout(const Duration(seconds: 15)); if (isLoginRequest) { debugPrint('LOGIN RESPONSE STATUS: ${response.statusCode}'); debugPrint('LOGIN RESPONSE BODY: ${response.body}'); } return response; } catch (error) { if (isLoginRequest) { debugPrint('LOGIN REQUEST ERROR: $error'); } rethrow; } } Map _headers({String? token}) { return { 'Accept': 'application/json', 'Content-Type': 'application/json', if (token != null && token.isNotEmpty) 'Authorization': 'Bearer $token', }; } Map _decodeResponse(http.Response response) { if (response.body.trim().isEmpty) return {}; final decoded = jsonDecode(response.body); if (decoded is Map) return decoded; throw const AuthException('Format response server tidak valid.'); } void _throwIfUnsuccessful( http.Response response, Map payload, ) { if (response.statusCode >= 200 && response.statusCode < 300) return; throw AuthException( _messageFromPayload(payload), errors: _errorsFromPayload(payload), ); } AppUser _userFromPayload(Map payload) { final data = payload['data']; final userJson = data is Map ? data['user'] : null; if (userJson is! Map) { throw const AuthException('Data user tidak ditemukan dari server.'); } final user = AppUser.fromJson(userJson); return user.copyWith(name: _normalizeName(user.name)); } String _messageFromPayload(Map payload) { final errors = _errorsFromPayload(payload); if (errors.isNotEmpty) { return _localizeAuthMessage(errors.values.first.first); } final message = payload['message']?.toString().trim(); if (message != null && message.isNotEmpty && message != 'Success') { return _localizeAuthMessage(message); } return 'Permintaan gagal diproses. Silakan coba lagi.'; } Map> _errorsFromPayload(Map payload) { final errors = payload['errors']; if (errors is! Map) return const {}; return errors.map((key, value) { final messages = value is List ? value .map((message) => _localizeAuthMessage(message.toString())) .toList() : [_localizeAuthMessage(value.toString())]; return MapEntry(key.toString(), messages); }); } String _localizeAuthMessage(String message) { final text = message.trim(); final normalized = text.toLowerCase(); if (normalized.contains('invalid credentials') || normalized.contains('incorrect password') || normalized.contains('wrong password') || normalized.contains('password is incorrect') || normalized.contains('these credentials do not match') || normalized.contains('the provided credentials are incorrect')) { return 'Email atau password yang Anda masukkan salah.'; } if (normalized.contains('user not found') || normalized.contains('email not found') || normalized.contains('no user found') || normalized.contains('account not found')) { return 'Akun dengan email tersebut tidak ditemukan.'; } if (normalized.contains('unauthenticated') || normalized.contains('unauthorized')) { return 'Sesi Anda tidak valid. Silakan login kembali.'; } if (normalized.contains('too many login attempts') || normalized.contains('too many attempts')) { return 'Terlalu banyak percobaan login. Silakan coba lagi nanti.'; } if (normalized.contains('email field is required') || normalized.contains('email is required')) { return 'Email wajib diisi.'; } if (normalized.contains('password field is required') || normalized.contains('password is required')) { return 'Password wajib diisi.'; } return text; } String _stringAt(Map payload, List path) { Object? current = payload; for (final key in path) { if (current is! Map) return ''; current = current[key]; } return current?.toString().trim() ?? ''; } String _normalizeName(String? value) { final name = value?.trim() ?? ''; return name.isEmpty ? 'Wisatawan' : name; } }