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
@@ -3,3 +3,4 @@ library professional_repository;
export 'src/models/models.dart';
export 'src/entities/entities.dart';
export 'src/repositories/firebase_professional_repository.dart';
export 'src/repositories/api_professional_repository.dart';
@@ -0,0 +1,217 @@
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:professional_repository/professional_repository.dart';
const _base = 'https://backend.prosapp.co/api/v1';
/// API-backed replacement for FirebaseProfessionalRepository.
/// Mirrors the same public API so existing blocs work without changes.
class ApiProfessionalRepository {
ProfessionalEntity? _proInfo;
final StreamController<ProfessionalEntity?> _proInfoBroadcast =
StreamController<ProfessionalEntity?>.broadcast();
bool isProModeActive = false;
final StreamController<bool> _isProModeActiveBroadcast =
StreamController<bool>.broadcast();
String? _token;
ApiProfessionalRepository() {
_isProModeActiveBroadcast.add(isProModeActive);
}
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) async {
final res = await http.get(Uri.parse('$_base$path'), headers: await _headers());
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);
}
ProfessionalEntity? lastProInfo() => _proInfo;
Stream<ProfessionalEntity?> streamProInfo() => _proInfoBroadcast.stream;
Stream<bool> sreamIsProModeActive() => _isProModeActiveBroadcast.stream;
switchProMode() {
isProModeActive = !isProModeActive;
_isProModeActiveBroadcast.add(isProModeActive);
}
ProfessionalEntity _fromApi(Map<String, dynamic> json) {
return ProfessionalEntity(
id: json['user_id']?.toString() ?? json['id']?.toString() ?? '',
identification: json['identification']?.toString() ?? '',
address: json['address']?.toString() ?? '',
aditionalAddress: json['aditional_address']?.toString() ?? '',
profession: json['profession']?.toString() ?? '',
ratePreferences: json['rate_preferences'] as bool? ?? false,
rate: json['rate']?.toString() ?? '',
locationPreferences: intToEnum((json['location_preferences'] as num?)?.toInt() ?? 0),
bannerPicture: json['banner_picture']?.toString() ?? '',
identificationPicture: json['identification_picture']?.toString() ?? '',
certificatePicture: json['certificate_picture']?.toString() ?? '',
latitude: double.tryParse(json['latitude']?.toString() ?? '0') ?? 0.0,
longitude: double.tryParse(json['longitude']?.toString() ?? '0') ?? 0.0,
specializations: json['specializations'] != null
? List<String>.from(json['specializations'])
: [],
specializationsPictures: json['specializations_pictures'] != null
? List<String>.from(json['specializations_pictures'])
: [],
schedules: json['schedules'] != null
? Schedules.fromDocument(json['schedules'] as Map<String, dynamic>)
: Schedules.empty,
paymentMethods: json['payment_methods'] != null
? PaymentMethodEntity.fromDocument(
json['payment_methods'] as Map<String, dynamic>)
: PaymentMethodEntity.empty,
);
}
Future<void> updateFromFirebase({required String userId}) async {
try {
final proInfo = await getProInfo(userId);
_proInfo = proInfo;
_proInfoBroadcast.add(proInfo);
} catch (e) {
_proInfo = null;
_proInfoBroadcast.add(null);
}
}
Future<ProfessionalEntity?> getProInfo(String myUserId) async {
try {
final data = await _get('/professionals/$myUserId');
if (data == null) return null;
return _fromApi(data as Map<String, dynamic>);
} catch (_) {
return null;
}
}
Future<void> updateProfessionalInfo(
String address,
String aditionalAddress,
bool ratePreferences,
String rate,
LocationPreferences locationPreferences,
double latitude,
double longitude,
Schedules schedules,
PaymentMethodEntity paymentMethods,
) async {
await _patch('/professionals/me', {
'address': address,
'aditional_address': aditionalAddress,
'rate_preferences': ratePreferences,
'rate': rate,
'location_preferences': enumToInt(locationPreferences),
'latitude': latitude,
'longitude': longitude,
'schedules': schedules.toJson(),
'payment_methods': paymentMethods.toDocument(),
});
if (_proInfo != null) {
await updateFromFirebase(userId: _proInfo!.id);
}
}
Future<void> saveProfessionalInfo(ProfessionalEntity entity) async {
await _patch('/professionals/me', entity.toDocument());
}
Future<void> uploadBannerPicture(String file) async {
final token = await _getToken();
final req = http.MultipartRequest('POST', Uri.parse('$_base/storage/upload'));
if (token != null) req.headers['Authorization'] = 'Bearer $token';
req.files.add(await http.MultipartFile.fromPath('file', file));
final streamed = await req.send();
final res = await http.Response.fromStream(streamed);
final body = jsonDecode(res.body) as Map<String, dynamic>;
final url = body['url'] as String;
await _patch('/professionals/me', {'banner_picture': url});
}
Future<String> uploadPdfCedula(String file, String userId) async {
final token = await _getToken();
final req = http.MultipartRequest('POST', Uri.parse('$_base/storage/upload'));
if (token != null) req.headers['Authorization'] = 'Bearer $token';
req.files.add(await http.MultipartFile.fromPath('file', file));
final streamed = await req.send();
final res = await http.Response.fromStream(streamed);
final body = jsonDecode(res.body) as Map<String, dynamic>;
return body['url'] as String;
}
Future<String> uploadPdfCertificado(String file, String userId) async {
final token = await _getToken();
final req = http.MultipartRequest('POST', Uri.parse('$_base/storage/upload'));
if (token != null) req.headers['Authorization'] = 'Bearer $token';
req.files.add(await http.MultipartFile.fromPath('file', file));
final streamed = await req.send();
final res = await http.Response.fromStream(streamed);
final body = jsonDecode(res.body) as Map<String, dynamic>;
return body['url'] as String;
}
Future<List<String>> uploadPdfsEspecializaciones(
List<String> files, String userId) async {
final List<String> urls = [];
for (final file in files) {
final token = await _getToken();
final req = http.MultipartRequest('POST', Uri.parse('$_base/storage/upload'));
if (token != null) req.headers['Authorization'] = 'Bearer $token';
req.files.add(await http.MultipartFile.fromPath('file', file));
final streamed = await req.send();
final res = await http.Response.fromStream(streamed);
final body = jsonDecode(res.body) as Map<String, dynamic>;
urls.add(body['url'] as String);
}
return urls;
}
Future<List<ProfessionalEntity>> getProfessionalInfo() async {
try {
final data = await _get('/professionals') as List;
return data.map((e) => _fromApi(e as Map<String, dynamic>)).toList();
} catch (_) {
return [];
}
}
Future<List<ProfessionalEntity>> getProfessionalsFromIds(
Iterable<String> ids) async {
final result = <ProfessionalEntity>[];
for (final id in ids) {
final p = await getProInfo(id);
if (p != null) result.add(p);
}
return result;
}
}