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> {
|
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||||
final UserRepository _userRepository;
|
final UserRepository _userRepository;
|
||||||
final PhoneVerificationService phoneVerificationService =
|
|
||||||
PhoneVerificationService();
|
|
||||||
String? _verificationId;
|
|
||||||
|
|
||||||
AuthBloc({required UserRepository userRepository})
|
AuthBloc({required UserRepository userRepository})
|
||||||
: _userRepository = userRepository,
|
: _userRepository = userRepository,
|
||||||
@@ -103,39 +100,11 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|||||||
void _linkWithPhoneNumber(
|
void _linkWithPhoneNumber(
|
||||||
LinkWithPhoneNumber event, Emitter<AuthState> emit) async {
|
LinkWithPhoneNumber event, Emitter<AuthState> emit) async {
|
||||||
emit(AuthStateProcess());
|
emit(AuthStateProcess());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await for (PhoneAuthEvent event
|
await _userRepository.signInWithPhoneNumber(event.phoneNumber);
|
||||||
in phoneVerificationService.verifyPhoneNumber(event.phoneNumber)) {
|
emit(const AuthStateVerifyOAuth(false));
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} 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());
|
emit(AuthStateProcess());
|
||||||
try {
|
try {
|
||||||
final bool isVerified = await _userRepository.linkWithOTP(
|
final bool isVerified = await _userRepository.linkWithOTP(
|
||||||
event.phoneNumber, _verificationId ?? '', event.code);
|
event.phoneNumber, '', event.code);
|
||||||
|
|
||||||
if (isVerified) {
|
if (isVerified) {
|
||||||
emit(AuthStateSuccess());
|
emit(AuthStateSuccess());
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class ApiUserRepository implements UserRepository {
|
|||||||
final _controller = StreamController<MyUser?>.broadcast();
|
final _controller = StreamController<MyUser?>.broadcast();
|
||||||
MyUser? _current;
|
MyUser? _current;
|
||||||
String? _token;
|
String? _token;
|
||||||
|
String? _pendingOtpPhone;
|
||||||
|
|
||||||
Future<String?> _getToken() async {
|
Future<String?> _getToken() async {
|
||||||
if (_token != null) return _token;
|
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 {
|
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);
|
return jsonDecode(res.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> _post(String path, Map<String, dynamic> body, {bool auth = false}) async {
|
Future<dynamic> _post(String path, Map<String, dynamic> body, {bool auth = false}) async {
|
||||||
final h = await _headers();
|
final h = await _headers();
|
||||||
if (!auth) h.remove('Authorization');
|
if (!auth) h.remove('Authorization');
|
||||||
final res = await http.post(Uri.parse('$_base$path'), headers: h, body: jsonEncode(body));
|
final res = await http.post(Uri.parse('$_base$path'), headers: h, body: jsonEncode(body)).timeout(_timeout);
|
||||||
return jsonDecode(res.body);
|
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 {
|
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);
|
return jsonDecode(res.body);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,14 +95,19 @@ class ApiUserRepository implements UserRepository {
|
|||||||
Stream<bool> isAuthenticated() async* {
|
Stream<bool> isAuthenticated() async* {
|
||||||
final t = await _getToken();
|
final t = await _getToken();
|
||||||
if (t != null) {
|
if (t != null) {
|
||||||
// Try to restore the current user from the API
|
|
||||||
try {
|
try {
|
||||||
final data = await _get('/auth/me');
|
final data = await _get('/auth/me');
|
||||||
if (data != null) {
|
if (data != null) {
|
||||||
_current = _fromApi(data as Map<String, dynamic>);
|
_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;
|
yield t != null;
|
||||||
}
|
}
|
||||||
@@ -245,11 +258,31 @@ class ApiUserRepository implements UserRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> signInWithPhoneNumber(String phoneNumber) async {
|
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
|
@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
|
@override
|
||||||
Future<void> addPhoneAuthCredential(
|
Future<void> addPhoneAuthCredential(
|
||||||
@@ -263,7 +296,14 @@ class ApiUserRepository implements UserRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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
|
@override
|
||||||
Future<void> resetPassword(String email) async {
|
Future<void> resetPassword(String email) async {
|
||||||
|
|||||||
Reference in New Issue
Block a user