import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import '../env.dart'; import '../utils/connection_error.dart'; /// Logged-in user from the API. class ApiUser { final String id; final String? email; final String? displayName; const ApiUser({required this.id, this.email, this.displayName}); factory ApiUser.fromJson(Map json) { final metadata = json['user_metadata'] as Map?; return ApiUser( id: json['id'] as String? ?? '', email: json['email'] as String?, displayName: json['display_name'] as String? ?? metadata?['display_name'] as String?, ); } } /// Auth service that uses the backend REST API (login, token storage). class ApiAuthService { ApiAuthService._(); static final ApiAuthService _instance = ApiAuthService._(); static ApiAuthService get instance => _instance; static const String _tokenKey = 'api_auth_token'; static const String _userKey = 'api_auth_user'; final ValueNotifier currentUser = ValueNotifier(null); String? _token; Future get _prefs async => await SharedPreferences.getInstance(); /// Call after app start to restore session from stored token. Future init() async { final prefs = await _prefs; _token = prefs.getString(_tokenKey); final userJson = prefs.getString(_userKey); if (_token != null && userJson != null) { try { final map = jsonDecode(userJson) as Map; currentUser.value = ApiUser.fromJson(map); } catch (_) { await logout(); } } if (_token != null && currentUser.value == null) { final ok = await _fetchSession(); if (!ok) await logout(); } } Future _fetchSession() async { if (_token == null) return false; try { final uri = Uri.parse('$apiBaseUrl/api/auth/session'); final res = await http.get( uri, headers: {'Authorization': 'Bearer $_token'}, ); if (res.statusCode != 200) return false; final data = jsonDecode(res.body) as Map; final userJson = data['user'] as Map?; if (userJson == null) return false; currentUser.value = ApiUser.fromJson(userJson); final prefs = await _prefs; await prefs.setString(_userKey, jsonEncode(userJson)); return true; } catch (_) { return false; } } /// Returns the current Bearer token for API requests, or null if not logged in. String? get token => _token; /// Login with email or username and password. /// Returns null on success, or an error message string. Future login(String emailOrUsername, String password) async { final trimmed = emailOrUsername.trim(); if (trimmed.isEmpty) return 'Enter your email or username.'; if (password.isEmpty) return 'Enter your password.'; try { final uri = Uri.parse('$apiBaseUrl/api/auth/login'); final res = await http.post( uri, headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'email_or_username': trimmed, 'password': password, }), ); if (res.statusCode == 401) { final data = jsonDecode(res.body) as Map?; return data?['error'] as String? ?? data?['message'] as String? ?? 'Invalid email or password.'; } if (res.statusCode != 200) { final data = jsonDecode(res.body) as Map?; return data?['error'] as String? ?? data?['message'] as String? ?? 'Login failed. Please try again.'; } final data = jsonDecode(res.body) as Map; _token = data['access_token'] as String?; final userJson = data['user'] as Map?; if (_token == null || userJson == null) { return 'Invalid response from server.'; } currentUser.value = ApiUser.fromJson(userJson); final prefs = await _prefs; await prefs.setString(_tokenKey, _token!); await prefs.setString(_userKey, jsonEncode(userJson)); return null; } catch (e) { return connectionErrorMessage(e); } } /// Register with email, password, and optional display name. /// Returns null on success, or an error message string. Future register(String email, String password, String displayName) async { final trimmedEmail = email.trim(); if (trimmedEmail.isEmpty) return 'Enter your email.'; if (password.isEmpty) return 'Enter your password.'; try { final uri = Uri.parse('$apiBaseUrl/api/auth/register'); final res = await http.post( uri, headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'email': trimmedEmail, 'password': password, 'displayName': displayName.trim().isEmpty ? null : displayName.trim(), }), ); if (res.statusCode == 400 || res.statusCode == 422) { final data = jsonDecode(res.body) as Map?; return data?['message'] as String? ?? 'Registration failed.'; } if (res.statusCode != 200) { return 'Registration failed. Please try again.'; } final data = jsonDecode(res.body) as Map; _token = data['access_token'] as String?; final userJson = data['user'] as Map?; if (_token == null || userJson == null) { return 'Invalid response from server.'; } currentUser.value = ApiUser.fromJson(userJson); final prefs = await _prefs; await prefs.setString(_tokenKey, _token!); await prefs.setString(_userKey, jsonEncode(userJson)); return null; } catch (e) { return connectionErrorMessage(e); } } Future logout() async { _token = null; currentUser.value = null; final prefs = await _prefs; await prefs.remove(_tokenKey); await prefs.remove(_userKey); } }