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
@@ -0,0 +1,121 @@
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:chat_repository/chat_repository.dart';
const _base = 'https://backend.prosapp.co/api/v1';
/// API-backed replacement for FirebaseChatRepository.
/// Mirrors the same public API so existing blocs work without changes.
class ApiChatRepository {
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',
};
}
ChatEntity _chatFromApi(Map<String, dynamic> json) {
final rawMessages = json['messages'] as List? ?? [];
final messages = rawMessages
.map((m) => MessageEntity.fromDocument(m as Map<String, dynamic>))
.toList();
return ChatEntity(
id: json['id']?.toString(),
userId: json['user_id']?.toString() ?? json['userId']?.toString() ?? '',
professionalId: json['professional_id']?.toString() ??
json['professionalId']?.toString() ??
'',
messages: messages,
);
}
MessageEntity _msgFromApi(Map<String, dynamic> json) {
return MessageEntity(
ownerId: json['owner_id']?.toString() ?? json['sender_id']?.toString() ?? '',
content: json['content']?.toString() ?? '',
createdAt: json['created_at'] != null
? DateTime.tryParse(json['created_at'].toString()) ?? DateTime.now()
: DateTime.now(),
);
}
/// Get or create a chat session. Maps to POST /chat/start/:professionalUserId.
/// [chatId] here is used as the professional's userId for the REST call.
Future<ChatEntity> createNewChat(
String chatId, String userId, String professionalId) async {
final res = await http.post(
Uri.parse('$_base/chat/start/$professionalId'),
headers: await _headers(),
);
final data = jsonDecode(res.body) as Map<String, dynamic>;
return _chatFromApi(data);
}
/// Streams a single chat by its ID. Fetches once and emits.
Stream<ChatEntity?> getChatById(String chatId) {
final controller = StreamController<ChatEntity?>();
_fetchChat(chatId).then((chat) {
controller.add(chat);
controller.close();
}).catchError((e) {
controller.add(null);
controller.close();
});
return controller.stream;
}
Future<ChatEntity?> _fetchChat(String chatId) async {
try {
// Try to get messages for this chat — if the chat exists it'll succeed
final res = await http.get(
Uri.parse('$_base/chat/$chatId/messages'),
headers: await _headers(),
);
if (res.statusCode == 404) return null;
final messages = jsonDecode(res.body) as List? ?? [];
return ChatEntity(
id: chatId,
userId: '',
professionalId: '',
messages: messages
.map((m) => _msgFromApi(m as Map<String, dynamic>))
.toList(),
);
} catch (_) {
return null;
}
}
Future<void> sendMessage(String chatId, MessageEntity message) async {
await http.post(
Uri.parse('$_base/chat/$chatId/message'),
headers: await _headers(),
body: jsonEncode({'content': message.content}),
);
}
/// Get all chats for the current user.
Future<List<ChatEntity>> getMyChats() async {
try {
final res = await http.get(
Uri.parse('$_base/chat/my'),
headers: await _headers(),
);
final data = jsonDecode(res.body) as List? ?? [];
return data.map((e) => _chatFromApi(e as Map<String, dynamic>)).toList();
} catch (_) {
return [];
}
}
}