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:
qzl
2026-03-19 18:42:05 +08:00
parent 636b37ee5a
commit 0661016827
29 changed files with 615 additions and 2030 deletions
@@ -6,14 +6,11 @@ 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';
import 'package:social_app/core/storage/token_storage.dart';
import 'package:social_app/core/api/api_client.dart';
class MockAuthApi extends Mock implements AuthApi {}
class MockTokenStorage extends Mock implements TokenStorage {}
class MockApiClient extends Mock implements ApiClient {}
void main() {
late AuthRepositoryImpl repository;
late MockAuthApi mockApi;
@@ -23,43 +20,29 @@ void main() {
mockApi = MockAuthApi();
mockStorage = MockTokenStorage();
repository = AuthRepositoryImpl(api: mockApi, tokenStorage: mockStorage);
registerFallbackValue(
const SignupStartRequest(username: '', email: '', password: ''),
);
registerFallbackValue(const LoginRequest(email: '', password: ''));
registerFallbackValue(const SignupVerifyRequest(email: '', token: ''));
registerFallbackValue(const SignupResendRequest(email: ''));
registerFallbackValue(const OtpSendRequest(phone: ''));
registerFallbackValue(const LoginRequest(phone: '', token: ''));
registerFallbackValue(const LogoutRequest(refreshToken: ''));
registerFallbackValue(const RefreshRequest(refreshToken: ''));
});
group('AuthRepositoryImpl', () {
test('createVerification calls api and returns response', () async {
when(() => mockApi.createVerification(any())).thenAnswer(
(_) async =>
const VerificationCreateResponse(email: 'test@example.com'),
);
test('sendOtp calls api', () async {
when(() => mockApi.sendOtp(any())).thenAnswer((_) async {});
final result = await repository.createVerification(
const SignupStartRequest(
username: 'testuser',
email: 'test@example.com',
password: 'password123',
),
);
await repository.sendOtp('+8613812345678');
expect(result.email, 'test@example.com');
verify(() => mockApi.createVerification(any())).called(1);
verify(() => mockApi.sendOtp(any())).called(1);
});
test('createSession calls api and saves tokens', () async {
when(() => mockApi.createSession(any())).thenAnswer(
test('createPhoneSession calls api and saves tokens', () async {
when(() => mockApi.createPhoneSession(any())).thenAnswer(
(_) async => AuthResponse(
accessToken: 'access_token',
refreshToken: 'refresh_token',
expiresIn: 3600,
tokenType: 'bearer',
user: const AuthUser(id: '123', email: 'test@example.com'),
user: const AuthUser(id: '123', phone: '+8613812345678'),
),
);
when(
@@ -69,8 +52,9 @@ void main() {
),
).thenAnswer((_) async {});
final result = await repository.createSession(
const LoginRequest(email: 'test@example.com', password: 'password123'),
final result = await repository.createPhoneSession(
phone: '+8613812345678',
token: '123456',
);
expect(result.accessToken, 'access_token');
@@ -117,7 +101,7 @@ void main() {
refreshToken: 'new_refresh',
expiresIn: 3600,
tokenType: 'bearer',
user: const AuthUser(id: '123', email: 'test@example.com'),
user: const AuthUser(id: '123', phone: '+8613812345678'),
),
);
when(
@@ -4,33 +4,32 @@ import 'package:social_app/features/auth/data/models/login_request.dart';
import 'package:social_app/features/auth/data/models/auth_response.dart';
void main() {
group('SignupStartRequest', () {
test('serializes to JSON', () {
final request = SignupStartRequest(
username: 'testuser',
email: 'test@example.com',
password: 'password123',
);
group('OtpSendRequest', () {
test('serializes e164 phone to JSON', () {
final request = OtpSendRequest(phone: '+14155552671');
final json = request.toJson();
expect(json['username'], 'testuser');
expect(json['email'], 'test@example.com');
expect(json['password'], 'password123');
expect(json['phone'], '+14155552671');
});
test('normalizes 00 prefix to plus', () {
final request = OtpSendRequest(phone: '0014155552671');
final json = request.toJson();
expect(json['phone'], '+14155552671');
});
});
group('LoginRequest', () {
test('serializes to JSON', () {
final request = LoginRequest(
email: 'test@example.com',
password: 'password123',
);
test('serializes e164 to JSON', () {
final request = LoginRequest(phone: '+14155552671', token: '123456');
final json = request.toJson();
expect(json['email'], 'test@example.com');
expect(json['password'], 'password123');
expect(json['phone'], '+14155552671');
expect(json['token'], '123456');
});
});
@@ -41,7 +40,7 @@ void main() {
'refresh_token': 'test_refresh',
'expires_in': 3600,
'token_type': 'bearer',
'user': {'id': '123', 'email': 'test@example.com'},
'user': {'id': '123', 'phone': '+8613812345678'},
};
final response = AuthResponse.fromJson(json);
@@ -50,7 +49,7 @@ void main() {
expect(response.refreshToken, 'test_refresh');
expect(response.expiresIn, 3600);
expect(response.user.id, '123');
expect(response.user.email, 'test@example.com');
expect(response.user.phone, '+8613812345678');
});
});
}