From 38beca4b4fe340dd333fa14cc480b150d52eded6 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:23:39 -0500 Subject: [PATCH] feat: implement SMS OTP auth via backend API - signInWithPhoneNumber: calls POST /auth/send-otp, stores pending phone - verifyOTP: calls POST /auth/phone with phone+code, saves token - linkWithOTP: calls POST /auth/verify-phone (authenticated) with phone+code - AuthBloc._linkWithPhoneNumber: replaced Firebase PhoneVerificationService with repository call Co-Authored-By: Claude Sonnet 4.6 --- lib/blocs/auth_bloc/auth_bloc.dart | 40 ++----------- .../src/repositories/api_user_repository.dart | 60 +++++++++++++++---- 2 files changed, 54 insertions(+), 46 deletions(-) diff --git a/lib/blocs/auth_bloc/auth_bloc.dart b/lib/blocs/auth_bloc/auth_bloc.dart index 3f7bb07..8950a84 100644 --- a/lib/blocs/auth_bloc/auth_bloc.dart +++ b/lib/blocs/auth_bloc/auth_bloc.dart @@ -9,9 +9,6 @@ part 'auth_state.dart'; class AuthBloc extends Bloc { final UserRepository _userRepository; - final PhoneVerificationService phoneVerificationService = - PhoneVerificationService(); - String? _verificationId; AuthBloc({required UserRepository userRepository}) : _userRepository = userRepository, @@ -103,39 +100,11 @@ class AuthBloc extends Bloc { void _linkWithPhoneNumber( LinkWithPhoneNumber event, Emitter emit) async { emit(AuthStateProcess()); - try { - await for (PhoneAuthEvent event - in phoneVerificationService.verifyPhoneNumber(event.phoneNumber)) { - switch (event.type) { - case PhoneAuthEventType.verificationCompleted: - final credential = event.data; - print('Verificacion completada. Credencial: $credential'); - break; - case PhoneAuthEventType.verificationFailed: - final exception = event.data; - print('Verificacion fallida. Excepcion: $exception'); - emit(AuthStateFailure(message: "Error inesperado. $exception")); - return; - case PhoneAuthEventType.codeAutoRetrievalTimeout: - final verificationId = event.data as String; - print('Tiempo de espera agotado. ID: $verificationId'); - emit(const AuthStateFailure( - message: "Tiempo de espera agotado.", - )); - break; - case PhoneAuthEventType.codeSent: - final eventData = event.data as Map; - 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; - } - } + await _userRepository.signInWithPhoneNumber(event.phoneNumber); + emit(const AuthStateVerifyOAuth(false)); } catch (e) { - emit(const AuthStateFailure(message: "Error inesperado.")); + emit(AuthStateFailure(message: "Error al enviar código. $e")); } } @@ -144,8 +113,7 @@ class AuthBloc extends Bloc { emit(AuthStateProcess()); try { final bool isVerified = await _userRepository.linkWithOTP( - event.phoneNumber, _verificationId ?? '', event.code); - + event.phoneNumber, '', event.code); if (isVerified) { emit(AuthStateSuccess()); } else { diff --git a/packages/user_repository/lib/src/repositories/api_user_repository.dart b/packages/user_repository/lib/src/repositories/api_user_repository.dart index a9a877f..021ee6b 100644 --- a/packages/user_repository/lib/src/repositories/api_user_repository.dart +++ b/packages/user_repository/lib/src/repositories/api_user_repository.dart @@ -16,6 +16,7 @@ class ApiUserRepository implements UserRepository { final _controller = StreamController.broadcast(); MyUser? _current; String? _token; + String? _pendingOtpPhone; Future _getToken() async { if (_token != null) return _token; @@ -31,20 +32,27 @@ class ApiUserRepository implements UserRepository { }; } + static const _timeout = Duration(seconds: 20); + Future _get(String path) async { - final res = await http.get(Uri.parse('$_base$path'), headers: await _headers()); + final res = await http.get(Uri.parse('$_base$path'), headers: await _headers()).timeout(_timeout); return jsonDecode(res.body); } Future _post(String path, Map body, {bool auth = false}) async { final h = await _headers(); if (!auth) h.remove('Authorization'); - final res = await http.post(Uri.parse('$_base$path'), headers: h, body: jsonEncode(body)); - return jsonDecode(res.body); + final res = await http.post(Uri.parse('$_base$path'), headers: h, body: jsonEncode(body)).timeout(_timeout); + final data = jsonDecode(res.body); + if (res.statusCode >= 400) { + final msg = (data is Map ? data['message'] : null) ?? 'Error del servidor'; + throw Exception(msg); + } + return data; } Future _patch(String path, Map body) async { - final res = await http.patch(Uri.parse('$_base$path'), headers: await _headers(), body: jsonEncode(body)); + final res = await http.patch(Uri.parse('$_base$path'), headers: await _headers(), body: jsonEncode(body)).timeout(_timeout); return jsonDecode(res.body); } @@ -87,14 +95,19 @@ class ApiUserRepository implements UserRepository { Stream isAuthenticated() async* { final t = await _getToken(); if (t != null) { - // Try to restore the current user from the API try { final data = await _get('/auth/me'); if (data != null) { _current = _fromApi(data as Map); - currentUserId = _current?.id; + _emit(_current); + } else { + _emit(null); } - } catch (_) {} + } catch (_) { + _emit(null); + } + } else { + _emit(null); } yield t != null; } @@ -245,11 +258,31 @@ class ApiUserRepository implements UserRepository { @override Future signInWithPhoneNumber(String phoneNumber) async { - // The API's phone auth is direct — no OTP flow via this method + _pendingOtpPhone = phoneNumber; + await _post('/auth/send-otp', {'phone': phoneNumber}); } @override - Future verifyOTP(String code) async => false; + Future verifyOTP(String code) async { + if (_pendingOtpPhone == null) return false; + try { + final data = await _post('/auth/phone', { + 'phone': _pendingOtpPhone!, + 'code': code, + }); + _token = data['access_token'] as String?; + if (_token != null) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('token', _token!); + } + _current = _fromApi(data['user'] as Map); + _emit(_current); + _pendingOtpPhone = null; + return true; + } catch (_) { + return false; + } + } @override Future addPhoneAuthCredential( @@ -263,7 +296,14 @@ class ApiUserRepository implements UserRepository { } @override - Future linkWithOTP(String phoneNumber, String verificationId, String code) async => false; + Future linkWithOTP(String phoneNumber, String verificationId, String code) async { + try { + await _post('/auth/verify-phone', {'phone': phoneNumber, 'code': code}, auth: true); + return true; + } catch (_) { + return false; + } + } @override Future resetPassword(String email) async {