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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
7446f22e23
commit
38beca4b4f
@@ -9,9 +9,6 @@ part 'auth_state.dart';
|
||||
|
||||
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
final UserRepository _userRepository;
|
||||
final PhoneVerificationService phoneVerificationService =
|
||||
PhoneVerificationService();
|
||||
String? _verificationId;
|
||||
|
||||
AuthBloc({required UserRepository userRepository})
|
||||
: _userRepository = userRepository,
|
||||
@@ -103,39 +100,11 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
void _linkWithPhoneNumber(
|
||||
LinkWithPhoneNumber event, Emitter<AuthState> 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<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;
|
||||
}
|
||||
}
|
||||
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<AuthEvent, AuthState> {
|
||||
emit(AuthStateProcess());
|
||||
try {
|
||||
final bool isVerified = await _userRepository.linkWithOTP(
|
||||
event.phoneNumber, _verificationId ?? '', event.code);
|
||||
|
||||
event.phoneNumber, '', event.code);
|
||||
if (isVerified) {
|
||||
emit(AuthStateSuccess());
|
||||
} else {
|
||||
|
||||
@@ -16,6 +16,7 @@ class ApiUserRepository implements UserRepository {
|
||||
final _controller = StreamController<MyUser?>.broadcast();
|
||||
MyUser? _current;
|
||||
String? _token;
|
||||
String? _pendingOtpPhone;
|
||||
|
||||
Future<String?> _getToken() async {
|
||||
if (_token != null) return _token;
|
||||
@@ -31,20 +32,27 @@ class ApiUserRepository implements UserRepository {
|
||||
};
|
||||
}
|
||||
|
||||
static const _timeout = Duration(seconds: 20);
|
||||
|
||||
Future<dynamic> _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<dynamic> _post(String path, Map<String, dynamic> 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<dynamic> _patch(String path, Map<String, dynamic> 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<bool> 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<String, dynamic>);
|
||||
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<void> 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<bool> verifyOTP(String code) async => false;
|
||||
Future<bool> 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<String, dynamic>);
|
||||
_emit(_current);
|
||||
_pendingOtpPhone = null;
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addPhoneAuthCredential(
|
||||
@@ -263,7 +296,14 @@ class ApiUserRepository implements UserRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> linkWithOTP(String phoneNumber, String verificationId, String code) async => false;
|
||||
Future<bool> 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<void> resetPassword(String email) async {
|
||||
|
||||
Reference in New Issue
Block a user