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:
co-authored by
Claude Sonnet 4.6
parent
726cf12fd2
commit
733384091c
@@ -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';
|
||||
|
||||
+217
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user