- Replace all Firebase* repositories with Api* repositories using HTTP + SharedPreferences JWT - Remove Firebase.initializeApp() and firebase_messaging background handler from main.dart - Update DI (app_di.dart) to inject Api* repositories instead of Firebase* ones - Replace all Timestamp/cloud_firestore usage with ISO 8601 String dates - Stub PhoneVerificationService (Firebase phone OTP → backend OTP when implemented) - Add ApiService singleton with JWT management in lib/services/ - Legacy firebase_*_repository.dart files preserved for Fase 4 cleanup Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
93 lines
3.0 KiB
Dart
93 lines
3.0 KiB
Dart
import 'dart:convert';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
const String _baseUrl = 'https://backend.prosapp.co/api/v1';
|
|
|
|
class ApiException implements Exception {
|
|
final int statusCode;
|
|
final String message;
|
|
ApiException(this.statusCode, this.message);
|
|
@override
|
|
String toString() => 'ApiException($statusCode): $message';
|
|
}
|
|
|
|
class ApiService {
|
|
static ApiService? _instance;
|
|
static ApiService get instance => _instance ??= ApiService._();
|
|
ApiService._();
|
|
|
|
String? _token;
|
|
|
|
Future<String?> getToken() async {
|
|
if (_token != null) return _token;
|
|
final prefs = await SharedPreferences.getInstance();
|
|
_token = prefs.getString('token');
|
|
return _token;
|
|
}
|
|
|
|
Future<void> saveToken(String token) async {
|
|
_token = token;
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.setString('token', token);
|
|
}
|
|
|
|
Future<void> clearToken() async {
|
|
_token = null;
|
|
final prefs = await SharedPreferences.getInstance();
|
|
await prefs.remove('token');
|
|
}
|
|
|
|
Future<Map<String, String>> _headers({bool auth = true}) async {
|
|
final headers = <String, String>{'Content-Type': 'application/json'};
|
|
if (auth) {
|
|
final token = await getToken();
|
|
if (token != null) headers['Authorization'] = 'Bearer $token';
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
dynamic _parse(http.Response res) {
|
|
final body = jsonDecode(res.body);
|
|
if (res.statusCode >= 200 && res.statusCode < 300) return body;
|
|
final msg = body is Map ? (body['message'] ?? res.reasonPhrase) : res.reasonPhrase;
|
|
throw ApiException(res.statusCode, msg.toString());
|
|
}
|
|
|
|
Future<dynamic> get(String path, {bool auth = true, Map<String, String>? query}) async {
|
|
final uri = Uri.parse('$_baseUrl$path').replace(queryParameters: query);
|
|
final res = await http.get(uri, headers: await _headers(auth: auth));
|
|
return _parse(res);
|
|
}
|
|
|
|
Future<dynamic> post(String path, Map<String, dynamic> body, {bool auth = false}) async {
|
|
final res = await http.post(
|
|
Uri.parse('$_baseUrl$path'),
|
|
headers: await _headers(auth: auth),
|
|
body: jsonEncode(body),
|
|
);
|
|
return _parse(res);
|
|
}
|
|
|
|
Future<dynamic> patch(String path, Map<String, dynamic> body, {bool auth = true}) async {
|
|
final res = await http.patch(
|
|
Uri.parse('$_baseUrl$path'),
|
|
headers: await _headers(auth: auth),
|
|
body: jsonEncode(body),
|
|
);
|
|
return _parse(res);
|
|
}
|
|
|
|
Future<String> uploadFile(String filePath) async {
|
|
final token = await getToken();
|
|
final req = http.MultipartRequest('POST', Uri.parse('$_baseUrl/storage/upload'));
|
|
if (token != null) req.headers['Authorization'] = 'Bearer $token';
|
|
req.files.add(await http.MultipartFile.fromPath('file', filePath));
|
|
final streamed = await req.send();
|
|
final res = await http.Response.fromStream(streamed);
|
|
final body = jsonDecode(res.body);
|
|
if (res.statusCode >= 200 && res.statusCode < 300) return body['url'] as String;
|
|
throw ApiException(res.statusCode, body['message']?.toString() ?? 'Upload failed');
|
|
}
|
|
}
|