feat: migrate prosappco from Firebase to NestJS REST API (Fase 2)

- 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>
This commit is contained in:
Lizandro Guarnizo
2026-06-17 16:53:11 -05:00
co-authored by Claude Sonnet 4.6
parent 726cf12fd2
commit 733384091c
65 changed files with 1404 additions and 389 deletions
@@ -1,5 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:service_repository/service_repository.dart';
import 'package:flutter/material.dart';
@@ -14,7 +13,7 @@ class ServiceEntity extends Equatable {
final double latitude;
final double longitude;
final String day;
final Timestamp createdAt;
final String createdAt;
final String description;
final TimeOfDay range1Hour1;
final TimeOfDay range1Hour2;
@@ -54,7 +53,7 @@ class ServiceEntity extends Equatable {
latitude: doc['latitude'] as double,
longitude: doc['longitude'] as double,
day: doc['day'] as String,
createdAt: doc['created_at'] as Timestamp,
createdAt: doc['created_at']?.toString() ?? DateTime.now().toIso8601String(),
description: doc['description'] as String,
range1Hour1: parseTimeOfDay(doc['range1_hour1'] as String),
range1Hour2: parseTimeOfDay(doc['range1_hour2'] as String),
@@ -0,0 +1,188 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:service_repository/service_repository.dart';
const _base = 'https://backend.prosapp.co/api/v1';
/// API-backed replacement for FirebaseServiceRepository.
/// Mirrors the same public API so existing blocs work without changes.
class ApiServiceRepository {
String? _token;
Future<String?> _getToken() async {
if (_token != null) return _token;
final prefs = await SharedPreferences.getInstance();
return _token = prefs.getString('token');
}
Future<Map<String, String>> _headers() async {
final t = await _getToken();
return {
'Content-Type': 'application/json',
if (t != null) 'Authorization': 'Bearer $t',
};
}
Future<dynamic> _get(String path, {Map<String, String>? query}) async {
final uri = Uri.parse('$_base$path').replace(queryParameters: query);
final res = await http.get(uri, headers: await _headers());
return jsonDecode(res.body);
}
Future<dynamic> _post(String path, Map<String, dynamic> body) async {
final res = await http.post(
Uri.parse('$_base$path'),
headers: await _headers(),
body: jsonEncode(body),
);
return jsonDecode(res.body);
}
Future<dynamic> _patch(String path, Map<String, dynamic> body) async {
final res = await http.patch(
Uri.parse('$_base$path'),
headers: await _headers(),
body: jsonEncode(body),
);
return jsonDecode(res.body);
}
ServiceEntity _fromApi(Map<String, dynamic> json) {
String range1Hour1 = json['range1_hour1']?.toString() ?? '0:0';
String range1Hour2 = json['range1_hour2']?.toString() ?? '0:0';
TimeOfDay parseTime(String s) {
final parts = s.split(':');
return TimeOfDay(
hour: int.tryParse(parts[0]) ?? 0,
minute: int.tryParse(parts.length > 1 ? parts[1] : '0') ?? 0,
);
}
return ServiceEntity(
id: json['id']?.toString(),
professionalId: json['professional_id']?.toString() ?? '',
professionalScored: json['professional_scored'] as bool? ?? false,
userId: json['user_id']?.toString() ?? '',
userScored: json['user_scored'] as bool? ?? false,
address: json['address']?.toString() ?? '',
aditionalAddress: json['aditional_address']?.toString() ?? '',
latitude: double.tryParse(json['latitude']?.toString() ?? '0') ?? 0.0,
longitude: double.tryParse(json['longitude']?.toString() ?? '0') ?? 0.0,
day: json['day']?.toString() ?? '',
createdAt: json['created_at']?.toString() ?? DateTime.now().toIso8601String(),
description: json['description']?.toString() ?? '',
range1Hour1: parseTime(range1Hour1),
range1Hour2: parseTime(range1Hour2),
rate: json['rate']?.toString() ?? '',
status: intToEnumService((json['status'] as num?)?.toInt() ?? 0),
location: intToEnum((json['location'] as num?)?.toInt() ?? 0),
);
}
Future<String> createService(ServiceEntity entity) async {
final data = await _post('/services', entity.toDocument());
return data['id']?.toString() ?? '';
}
Future<void> updateServiceStatus(String serviceId, ServiceStatus newStatus) async {
await _patch('/services/$serviceId', {'status': enumToIntService(newStatus)});
}
Stream<ServiceEntity> getService(String serviceId) {
final controller = StreamController<ServiceEntity>();
_get('/services', query: {'id': serviceId}).then((data) {
if (data is List && data.isNotEmpty) {
controller.add(_fromApi(data.first as Map<String, dynamic>));
} else if (data is Map) {
controller.add(_fromApi(data as Map<String, dynamic>));
}
controller.close();
}).catchError((e) {
controller.addError(e);
controller.close();
});
return controller.stream;
}
Stream<List<ServiceEntity>> getServicesForUser(String userId) {
return _streamList('/services', query: {'userId': userId}, statusFilter: [0, 1, 3]);
}
Stream<List<ServiceEntity>> getServicesForProfessional(String professionalId) {
return _streamList('/services', query: {'professionalId': professionalId}, statusFilter: [1, 3]);
}
Future<List<ServiceEntity>> getServicesForProfessionalforCalendar(String professionalId) async {
try {
final data = await _get('/services', query: {'professionalId': professionalId});
if (data is! List) return [];
return data
.map((e) => _fromApi(e as Map<String, dynamic>))
.where((s) => [0, 1, 2, 3, 6].contains(s.status.index))
.toList();
} catch (_) {
return [];
}
}
Stream<List<ServiceEntity>> getServicesHistoryForUser(String userId) {
return _streamList('/services', query: {'userId': userId}, statusFilter: [2, 4, 5]);
}
Stream<List<ServiceEntity>> getServicesHistoryForProfessional(String professionalId) {
return _streamList('/services', query: {'professionalId': professionalId}, statusFilter: [2, 4, 5]);
}
Stream<List<ServiceEntity>> getPendingServicesForProfessional(String professionalId) {
return _streamList('/services', query: {'professionalId': professionalId}, statusFilter: [0]);
}
Future<int> countPendingServicesForProfessional(String professionalId) async {
try {
final data = await _get('/services', query: {'professionalId': professionalId});
if (data is! List) return 0;
return data
.map((e) => _fromApi(e as Map<String, dynamic>))
.where((s) => s.status == ServiceStatus.pending)
.length;
} catch (_) {
return 0;
}
}
Future<void> setProfessionalScored(String serviceId) async {
await _patch('/services/$serviceId', {'professional_scored': true});
}
Future<void> setUserScored(String serviceId) async {
await _patch('/services/$serviceId', {'user_scored': true});
}
Stream<List<ServiceEntity>> _streamList(
String path, {
Map<String, String>? query,
List<int> statusFilter = const [],
}) {
final controller = StreamController<List<ServiceEntity>>();
_get(path, query: query).then((data) {
if (data is! List) {
controller.add([]);
} else {
var list = data.map((e) => _fromApi(e as Map<String, dynamic>)).toList();
if (statusFilter.isNotEmpty) {
list = list.where((s) => statusFilter.contains(s.status.index)).toList();
}
controller.add(list);
}
controller.close();
}).catchError((e) {
controller.add([]);
controller.close();
});
return controller.stream;
}
}