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
+19 -95
View File
@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:user_repository/user_repository.dart';
@@ -86,18 +85,18 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
} else {
switch (error) {
case UpdatePassworErros.credentialsWrong:
emit(const AuthStateFailure(message: "Contraseña incorrecta"));
emit(const AuthStateFailure(message: "Contrasena incorrecta"));
break;
case UpdatePassworErros.userNotFound:
emit(const AuthStateFailure(message: "Usuario no encontrado"));
break;
case UpdatePassworErros.unknown:
emit(const AuthStateFailure(message: "Error inesperado. 🥸"));
emit(const AuthStateFailure(message: "Error inesperado."));
break;
}
}
} catch (e) {
emit(const AuthStateFailure(message: "Error inesperado. 🥸"));
emit(const AuthStateFailure(message: "Error inesperado."));
}
}
@@ -110,81 +109,33 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
in phoneVerificationService.verifyPhoneNumber(event.phoneNumber)) {
switch (event.type) {
case PhoneAuthEventType.verificationCompleted:
AuthCredential credential = event.data;
print('Verificación completada. Credencial: $credential');
final credential = event.data;
print('Verificacion completada. Credencial: $credential');
break;
case PhoneAuthEventType.verificationFailed:
FirebaseAuthException exception = event.data;
print('Verificación fallida. Excepción: $exception');
emit(AuthStateFailure(message: "Error inesperado. 🥸 $exception"));
return; // Detener la ejecución aquí
final exception = event.data;
print('Verificacion fallida. Excepcion: $exception');
emit(AuthStateFailure(message: "Error inesperado. $exception"));
return;
case PhoneAuthEventType.codeAutoRetrievalTimeout:
String verificationId = event.data;
print(
'Tiempo de espera agotado para recuperar el código. ID: $verificationId');
final verificationId = event.data as String;
print('Tiempo de espera agotado. ID: $verificationId');
emit(const AuthStateFailure(
message: "Tiempo de espera agotado. 🥸",
message: "Tiempo de espera agotado.",
));
break;
case PhoneAuthEventType.codeSent:
Map<String, dynamic> eventData = event.data;
String verificationId = eventData['verificationId'];
int? resendToken = eventData['resendToken'];
print(
'Código enviado. ID: $verificationId, resendToken: $resendToken');
final eventData = event.data as Map<String, dynamic>;
final verificationId = eventData['verificationId'] as String;
final resendToken = eventData['resendToken'] as int?;
print('Codigo enviado. ID: $verificationId, resendToken: $resendToken');
_verificationId = verificationId;
emit(const AuthStateVerifyOAuth(false));
break;
}
}
} catch (e) {
emit(const AuthStateFailure(message: "Error inesperado. 🥸"));
}
}
void _linkWithPhoneNumber3(
LinkWithPhoneNumber event, Emitter<AuthState> emit) async {
emit(AuthStateProcess());
try {
StreamSubscription<PhoneAuthEvent> subscription = phoneVerificationService
.verifyPhoneNumber(event.phoneNumber)
.listen((event) async {
switch (event.type) {
case PhoneAuthEventType.verificationCompleted:
AuthCredential credential = event.data;
print('Verificación completada. Credencial: $credential');
break;
case PhoneAuthEventType.verificationFailed:
FirebaseAuthException exception = event.data;
print('Verificación fallida. Excepción: $exception');
break;
case PhoneAuthEventType.codeAutoRetrievalTimeout:
String verificationId = event.data;
print(
'Tiempo de espera agotado para recuperar el código. ID: $verificationId');
break;
case PhoneAuthEventType.codeSent:
Map<String, dynamic> eventData = event.data;
String verificationId = eventData['verificationId'];
int? resendToken = eventData['resendToken'];
print(
'Código enviado. ID: $verificationId, resendToken: $resendToken');
_verificationId = verificationId;
emit(const AuthStateVerifyOAuth(false));
break;
}
if (event.type == PhoneAuthEventType.verificationFailed) {
// Detener la suscripción si la verificación falla
// subscription.cancel();
emit(const AuthStateFailure(message: "Error inesperado. 🥸"));
}
});
// await _userRepository
} catch (e) {
emit(const AuthStateFailure(message: "Error inesperado. 🥸"));
emit(const AuthStateFailure(message: "Error inesperado."));
}
}
@@ -193,7 +144,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
emit(AuthStateProcess());
try {
final bool isVerified = await _userRepository.linkWithOTP(
event.phoneNumber, _verificationId!, event.code);
event.phoneNumber, _verificationId ?? '', event.code);
if (isVerified) {
emit(AuthStateSuccess());
@@ -201,34 +152,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
emit(const AuthStateVerifyOAuth(true));
}
} catch (e) {
if (e is FirebaseAuthException) {
switch (e.code) {
case "invalid-verification-code":
emit(const AuthStateFailure(
message: "Código de verificación incorrecto. 🥸",
));
break;
case "provider-already-linked":
emit(const AuthStateFailure(
message: "Cuenta ya vinculada. 🥸",
));
break;
case "credential-already-in-use":
emit(const AuthStateFailure(
message:
"Este numero ya se encuentra registrado con otra cuenta. 🥸",
));
break;
default:
emit(AuthStateFailure(message: "Error inesperado. 🥸 $e"));
break;
}
} else {
emit(AuthStateFailure(message: "Error inesperado. 🥸 $e"));
}
emit(AuthStateFailure(message: "Error inesperado. $e"));
}
}
}
+2 -2
View File
@@ -8,10 +8,10 @@ part 'chat_event.dart';
part 'chat_state.dart';
class ChatBloc extends Bloc<ChatEvent, ChatState> {
final FirebaseChatRepository _chatRepository;
final ApiChatRepository _chatRepository;
ChatBloc({
required FirebaseChatRepository chatRepository,
required ApiChatRepository chatRepository,
}) : _chatRepository = chatRepository,
super(ChatInitial()) {
on<LoadChatEvent>(_onLoadChatEvent);
@@ -1,25 +1,16 @@
import 'dart:developer';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:prosappco/firebase_options.dart';
import 'package:prosappco/local_notifications/local_notifications.dart';
import 'package:user_repository/user_repository.dart';
part 'notification_event.dart';
part 'notification_state.dart';
Future<void> firebaseMessagingBackgroundHandler(RemoteMessage message) async {
await Firebase.initializeApp();
}
// Stub: Firebase messaging removed. Background handler is no longer needed.
Future<void> firebaseMessagingBackgroundHandler(dynamic message) async {}
class NotificationBloc extends Bloc<NotificationEvent, NotificationState> {
FirebaseMessaging messaging = FirebaseMessaging.instance;
final Future<void> Function() requestLocalNotificationPermission;
final void Function({
required int id,
@@ -33,83 +24,18 @@ class NotificationBloc extends Bloc<NotificationEvent, NotificationState> {
required this.showLocalNotification})
: super(const NotificationState()) {
on<NotificationStatusChanged>(_notificationStatusChanged);
_initialStatusCheck();
_onForegroundMessage();
}
static Future<void> initializeFCM() async {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
}
void _notificationStatusChanged(
NotificationStatusChanged event, Emitter<NotificationState> emit) {
emit(state.copyWith(status: event.status));
_getFCMToken();
}
void _initialStatusCheck() async {
final settings = await messaging.getNotificationSettings();
add(NotificationStatusChanged(settings.authorizationStatus));
_getFCMToken();
}
Future<void> _saveFCMTokenToFirestore(String token) async {
try {
CollectionReference users = FirebaseFirestore.instance.collection('users');
await users.doc(FirebaseAuth.instance.currentUser!.uid).update({
'token': token,
});
} catch (e) {
print('tokenFCM Error saving FCM token to Firestore: $e');
}
}
void _getFCMToken() async {
if (state.status != AuthorizationStatus.authorized) return;
final fcmToken = await messaging.getToken();
log('tokenFCM ${fcmToken.toString()}');
if (fcmToken == null) return;
_saveFCMTokenToFirestore(fcmToken);
}
void _handleRemoteMessage(RemoteMessage message) {
if (message.notification == null) return;
showLocalNotification(
id: 1,
title: message.notification!.title,
body: message.notification!.body,
);
}
void _onForegroundMessage() {
FirebaseMessaging.onMessage.listen(_handleRemoteMessage);
}
void requestPermission() async {
NotificationSettings settings = await messaging.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
);
// Solicitar permiso a las local notificaciones
await requestLocalNotificationPermission();
add(NotificationStatusChanged(settings.authorizationStatus));
try {
await requestLocalNotificationPermission();
} catch (e) {
log('NotificationBloc.requestPermission error: $e');
}
}
}
@@ -1,5 +1,13 @@
part of 'notification_bloc.dart';
// Stub for firebase_messaging AuthorizationStatus
enum AuthorizationStatus {
authorized,
denied,
notDetermined,
provisional,
}
class NotificationState extends Equatable {
final AuthorizationStatus status;
@@ -10,11 +10,11 @@ part 'professional_event.dart';
part 'professional_state.dart';
class ProfessionalBloc extends Bloc<ProfessionalEvent, ProfessionalState> {
final FirebaseProfessionalRepository _professionalRepository;
final ApiProfessionalRepository _professionalRepository;
final UserRepository _userRepository;
ProfessionalBloc(
{required FirebaseProfessionalRepository professionalRepository,
{required ApiProfessionalRepository professionalRepository,
required UserRepository userRepository})
: _professionalRepository = professionalRepository,
_userRepository = userRepository,
@@ -9,11 +9,11 @@ part 'professional_list_state.dart';
class ProfessionalListBloc
extends Bloc<ProfessionalListEvent, ProfessionalListState> {
final UserRepository _userRepository;
final FirebaseProfessionalRepository _firebaseProfessionalRepository;
final ApiProfessionalRepository _firebaseProfessionalRepository;
ProfessionalListBloc({
required UserRepository userRepository,
required FirebaseProfessionalRepository firebaseProfessonalRepository,
required ApiProfessionalRepository firebaseProfessonalRepository,
}) : _userRepository = userRepository,
_firebaseProfessionalRepository = firebaseProfessonalRepository,
super(ProfessionalListInitial()) {
@@ -7,10 +7,10 @@ part 'professional_profile_state.dart';
class ProfessionalProfileBloc
extends Bloc<ProfessionalProfileEvent, ProfessionalProfileState> {
final FirebaseProfessionalRepository _professionalRepository;
final ApiProfessionalRepository _professionalRepository;
ProfessionalProfileBloc(
{required FirebaseProfessionalRepository professionalRepository})
{required ApiProfessionalRepository professionalRepository})
: _professionalRepository = professionalRepository,
super((UpdateProfessionalInfoInitial())) {
on<UpdateProfessionalBannerInfo>(_onUpdateProfessionalBannerInfo);
+2 -2
View File
@@ -9,11 +9,11 @@ part 'score_event.dart';
part 'score_state.dart';
class ScoreBloc extends Bloc<ScoreEvent, ScoreState> {
final FirebaseScoreRepository _scoreRepository;
final ApiScoreRepository _scoreRepository;
final UserRepository _userRepository;
ScoreBloc({
required FirebaseScoreRepository scoreRepository,
required ApiScoreRepository scoreRepository,
required UserRepository userRepository,
}) : _scoreRepository = scoreRepository,
_userRepository = userRepository,
+4 -5
View File
@@ -1,6 +1,5 @@
import 'dart:developer';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
@@ -13,14 +12,14 @@ part 'service_event.dart';
part 'service_state.dart';
class ServiceBloc extends Bloc<ServiceEvent, ServiceState> {
final FirebaseServiceRepository _serviceRepository;
final ApiServiceRepository _serviceRepository;
final UserRepository _userRepository;
final FirebaseProfessionalRepository _professionalRepository;
final ApiProfessionalRepository _professionalRepository;
ServiceBloc({
required FirebaseServiceRepository serviceRepository,
required ApiServiceRepository serviceRepository,
required UserRepository userRepository,
required FirebaseProfessionalRepository professionRepository,
required ApiProfessionalRepository professionRepository,
}) : _serviceRepository = serviceRepository,
_userRepository = userRepository,
_professionalRepository = professionRepository,
+1 -1
View File
@@ -117,7 +117,7 @@ class CreateService extends ServiceEvent {
final double latitude;
final double longitude;
final String day;
final Timestamp createdAt;
final String createdAt;
final String description;
final TimeOfDay range1Hour1;
final TimeOfDay range1Hour2;