feat(auth): transition from email to phone-based OTP authentication
- Replace Email+Password login with Phone+OTP flow - Remove RegisterCubit and registration screens (email verification) - Remove ResetPasswordCubit and reset password screens - Add phone normalization and international dial code support - Update LoginCubit with sendCode/resend cooldown logic - Add new widgets: phone prefix selector, confirm sheet - Update all auth API endpoints: /otp/send, /phone-session - Update form inputs: Email -> Phone with E.164 validation - Update tests for new auth flow
This commit is contained in:
@@ -9,34 +9,13 @@ class AuthApi {
|
||||
|
||||
AuthApi(this._client);
|
||||
|
||||
Future<VerificationCreateResponse> createVerification(
|
||||
SignupStartRequest request,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'$_prefix/verifications',
|
||||
data: request.toJson(),
|
||||
);
|
||||
return VerificationCreateResponse.fromJson(response.data);
|
||||
Future<void> sendOtp(OtpSendRequest request) async {
|
||||
await _client.post('$_prefix/otp/send', data: request.toJson());
|
||||
}
|
||||
|
||||
Future<AuthResponse> verifyVerification(SignupVerifyRequest request) async {
|
||||
Future<AuthResponse> createPhoneSession(LoginRequest request) async {
|
||||
final response = await _client.post(
|
||||
'$_prefix/verify',
|
||||
data: {'type': 'signup', ...request.toJson()},
|
||||
);
|
||||
return AuthResponse.fromJson(response.data);
|
||||
}
|
||||
|
||||
Future<void> resendVerification(SignupResendRequest request) async {
|
||||
await _client.post(
|
||||
'$_prefix/resend',
|
||||
data: {'type': 'signup', ...request.toJson()},
|
||||
);
|
||||
}
|
||||
|
||||
Future<AuthResponse> createSession(LoginRequest request) async {
|
||||
final response = await _client.post(
|
||||
'$_prefix/sessions',
|
||||
'$_prefix/phone-session',
|
||||
data: request.toJson(),
|
||||
);
|
||||
return AuthResponse.fromJson(response.data);
|
||||
@@ -53,27 +32,4 @@ class AuthApi {
|
||||
Future<void> deleteSession(LogoutRequest request) async {
|
||||
await _client.delete('$_prefix/sessions', data: request.toJson());
|
||||
}
|
||||
|
||||
Future<void> requestPasswordReset(String email) async {
|
||||
await _client.post(
|
||||
'$_prefix/resend',
|
||||
data: {'type': 'recovery', 'email': email},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> confirmPasswordReset({
|
||||
required String email,
|
||||
required String token,
|
||||
required String newPassword,
|
||||
}) async {
|
||||
await _client.post(
|
||||
'$_prefix/verify',
|
||||
data: {
|
||||
'type': 'recovery',
|
||||
'email': email,
|
||||
'token': token,
|
||||
'new_password': newPassword,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
import 'package:social_app/features/auth/data/models/signup_request.dart';
|
||||
import 'package:social_app/features/auth/data/models/login_request.dart';
|
||||
import 'package:social_app/features/auth/data/models/auth_response.dart';
|
||||
|
||||
abstract class AuthRepository {
|
||||
Future<VerificationCreateResponse> createVerification(
|
||||
SignupStartRequest request,
|
||||
);
|
||||
Future<AuthResponse> verifyVerification(SignupVerifyRequest request);
|
||||
Future<void> resendVerification(SignupResendRequest request);
|
||||
Future<AuthResponse> createSession(LoginRequest request);
|
||||
Future<void> sendOtp(String phone);
|
||||
Future<AuthResponse> createPhoneSession({
|
||||
required String phone,
|
||||
required String token,
|
||||
});
|
||||
Future<AuthResponse> refreshSession(String refreshToken);
|
||||
Future<void> deleteSession();
|
||||
Future<void> clearSessionLocalOnly();
|
||||
Future<String?> getAccessToken();
|
||||
Future<String?> getRefreshToken();
|
||||
Future<bool> isAuthenticated();
|
||||
Future<void> requestPasswordReset(String email);
|
||||
Future<void> confirmPasswordReset({
|
||||
required String email,
|
||||
required String token,
|
||||
required String newPassword,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -19,30 +19,18 @@ class AuthRepositoryImpl implements AuthRepository {
|
||||
_onLogout = onLogout;
|
||||
|
||||
@override
|
||||
Future<VerificationCreateResponse> createVerification(
|
||||
SignupStartRequest request,
|
||||
) {
|
||||
return _api.createVerification(request);
|
||||
Future<void> sendOtp(String phone) {
|
||||
return _api.sendOtp(OtpSendRequest(phone: phone));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AuthResponse> verifyVerification(SignupVerifyRequest request) async {
|
||||
final response = await _api.verifyVerification(request);
|
||||
await _tokenStorage.saveTokens(
|
||||
access: response.accessToken,
|
||||
refresh: response.refreshToken,
|
||||
Future<AuthResponse> createPhoneSession({
|
||||
required String phone,
|
||||
required String token,
|
||||
}) async {
|
||||
final response = await _api.createPhoneSession(
|
||||
LoginRequest(phone: phone, token: token),
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> resendVerification(SignupResendRequest request) {
|
||||
return _api.resendVerification(request);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AuthResponse> createSession(LoginRequest request) async {
|
||||
final response = await _api.createSession(request);
|
||||
await _tokenStorage.saveTokens(
|
||||
access: response.accessToken,
|
||||
refresh: response.refreshToken,
|
||||
@@ -94,22 +82,4 @@ class AuthRepositoryImpl implements AuthRepository {
|
||||
final token = await _tokenStorage.getAccessToken();
|
||||
return token != null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestPasswordReset(String email) {
|
||||
return _api.requestPasswordReset(email);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> confirmPasswordReset({
|
||||
required String email,
|
||||
required String token,
|
||||
required String newPassword,
|
||||
}) {
|
||||
return _api.confirmPasswordReset(
|
||||
email: email,
|
||||
token: token,
|
||||
newPassword: newPassword,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
class AuthUser {
|
||||
final String id;
|
||||
final String email;
|
||||
final String phone;
|
||||
|
||||
const AuthUser({required this.id, required this.email});
|
||||
const AuthUser({required this.id, required this.phone});
|
||||
|
||||
factory AuthUser.fromJson(Map<String, dynamic> json) {
|
||||
return AuthUser(id: json['id'] as String, email: json['email'] as String);
|
||||
return AuthUser(id: json['id'] as String, phone: json['phone'] as String);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,13 +34,3 @@ class AuthResponse {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class VerificationCreateResponse {
|
||||
final String email;
|
||||
|
||||
const VerificationCreateResponse({required this.email});
|
||||
|
||||
factory VerificationCreateResponse.fromJson(Map<String, dynamic> json) {
|
||||
return VerificationCreateResponse(email: json['email'] as String);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
class LoginRequest {
|
||||
final String email;
|
||||
final String password;
|
||||
final String phone;
|
||||
final String token;
|
||||
|
||||
const LoginRequest({required this.email, required this.password});
|
||||
const LoginRequest({required this.phone, required this.token});
|
||||
|
||||
Map<String, dynamic> toJson() => {'email': email, 'password': password};
|
||||
Map<String, dynamic> toJson() => {
|
||||
'phone': _normalizePhone(phone),
|
||||
'token': token,
|
||||
};
|
||||
}
|
||||
|
||||
String _normalizePhone(String input) {
|
||||
var normalized = input.trim();
|
||||
normalized = normalized.replaceAll(RegExp(r'[\s\-\(\)]'), '');
|
||||
if (normalized.startsWith('00') && normalized.length > 2) {
|
||||
return '+${normalized.substring(2)}';
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
class RefreshRequest {
|
||||
|
||||
@@ -1,37 +1,28 @@
|
||||
class SignupStartRequest {
|
||||
final String username;
|
||||
final String email;
|
||||
final String password;
|
||||
final String? inviteCode;
|
||||
class OtpSendRequest {
|
||||
final String phone;
|
||||
|
||||
const SignupStartRequest({
|
||||
required this.username,
|
||||
required this.email,
|
||||
required this.password,
|
||||
this.inviteCode,
|
||||
});
|
||||
const OtpSendRequest({required this.phone});
|
||||
|
||||
Map<String, dynamic> toJson() => {'phone': _normalizePhone(phone)};
|
||||
}
|
||||
|
||||
class PhoneSessionRequest {
|
||||
final String phone;
|
||||
final String token;
|
||||
|
||||
const PhoneSessionRequest({required this.phone, required this.token});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'username': username,
|
||||
'email': email,
|
||||
'password': password,
|
||||
if (inviteCode != null) 'invite_code': inviteCode,
|
||||
'phone': _normalizePhone(phone),
|
||||
'token': token,
|
||||
};
|
||||
}
|
||||
|
||||
class SignupVerifyRequest {
|
||||
final String email;
|
||||
final String token;
|
||||
|
||||
const SignupVerifyRequest({required this.email, required this.token});
|
||||
|
||||
Map<String, dynamic> toJson() => {'email': email, 'token': token};
|
||||
}
|
||||
|
||||
class SignupResendRequest {
|
||||
final String email;
|
||||
|
||||
const SignupResendRequest({required this.email});
|
||||
|
||||
Map<String, dynamic> toJson() => {'email': email};
|
||||
String _normalizePhone(String input) {
|
||||
var normalized = input.trim();
|
||||
normalized = normalized.replaceAll(RegExp(r'[\s\-\(\)]'), '');
|
||||
if (normalized.startsWith('00') && normalized.length > 2) {
|
||||
return '+${normalized.substring(2)}';
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user