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
+7 -6
View File
@@ -13,16 +13,17 @@ class Username extends FormzInput<String, String> {
}
}
class Email extends FormzInput<String, String> {
const Email.pure() : super.pure('');
const Email.dirty([super.value = '']) : super.dirty();
class Phone extends FormzInput<String, String> {
const Phone.pure() : super.pure('');
const Phone.dirty([super.value = '']) : super.dirty();
static final _regex = RegExp(r'^[\w.-]+@[\w.-]+\.\w+$');
static final _regex = RegExp(r'^\d{7,14}$');
@override
String? validator(String value) {
if (value.isEmpty) return '请输入邮箱';
if (!_regex.hasMatch(value)) return '邮箱格式不正确';
final normalized = value.replaceAll(RegExp(r'\s+'), '');
if (normalized.isEmpty) return '请输入手机号';
if (!_regex.hasMatch(normalized)) return '手机号格式不正确';
return null;
}
}
+4 -26
View File
@@ -4,11 +4,8 @@ import '../../features/auth/presentation/bloc/auth_bloc.dart';
import '../../features/auth/presentation/bloc/auth_state.dart';
import 'app_routes.dart';
import 'go_router_refresh_stream.dart';
import '../../features/auth/ui/screens/login_screen.dart';
import '../../features/auth/ui/screens/auth_boot_screen.dart';
import '../../features/auth/ui/screens/register_screen.dart';
import '../../features/auth/ui/screens/register_verification_screen.dart';
import '../../features/auth/ui/screens/reset_password_screen.dart';
import '../../features/auth/ui/screens/login_screen.dart';
import '../../features/home/ui/screens/home_screen.dart';
import '../../features/messages/ui/screens/message_invite_list_screen.dart';
import '../../features/messages/ui/screens/message_invite_detail_screen.dart';
@@ -28,7 +25,6 @@ import '../../features/settings/ui/screens/settings_screen.dart';
import '../../features/settings/ui/screens/features_screen.dart';
import '../../features/settings/ui/screens/memory_screen.dart';
import '../../features/settings/ui/screens/account_screen.dart';
import '../../features/settings/ui/screens/change_password_screen.dart';
import '../../features/settings/ui/screens/edit_profile_screen.dart';
final _protectedRoutes = [
@@ -43,7 +39,6 @@ final _protectedRoutes = [
AppRoutes.settingsFeatures,
AppRoutes.settingsMemory,
AppRoutes.settingsAccount,
AppRoutes.settingsChangePassword,
AppRoutes.settingsEditProfile,
AppRoutes.messageInviteList,
];
@@ -61,8 +56,7 @@ GoRouter createAppRouter(AuthBloc authBloc) {
final isBootRoute = state.matchedLocation == AppRoutes.authBoot;
final isAuthRoute =
state.matchedLocation == AppRoutes.authLogin ||
state.matchedLocation.startsWith('/login') ||
state.matchedLocation.startsWith('/register');
state.matchedLocation.startsWith('/login');
final isProtected = _protectedRoutes.any(
(route) => state.matchedLocation.startsWith(route),
);
@@ -86,10 +80,6 @@ GoRouter createAppRouter(AuthBloc authBloc) {
path: AppRoutes.authBoot,
builder: (context, state) => const AuthBootScreen(),
),
GoRoute(
path: AppRoutes.authLogin,
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: AppRoutes.calendarEventCreate,
builder: (context, state) => CalendarEventCreateScreen(
@@ -112,16 +102,8 @@ GoRouter createAppRouter(AuthBloc authBloc) {
CalendarEventShareScreen(eventId: state.pathParameters['id']!),
),
GoRoute(
path: AppRoutes.authRegister,
builder: (context, state) => const RegisterScreen(),
),
GoRoute(
path: AppRoutes.authRegisterVerification,
builder: (context, state) => const RegisterVerificationScreen(),
),
GoRoute(
path: AppRoutes.authResetPassword,
builder: (context, state) => const ResetPasswordScreen(),
path: AppRoutes.authLogin,
builder: (context, state) => const LoginScreen(),
),
GoRoute(
path: AppRoutes.homeMain,
@@ -196,10 +178,6 @@ GoRouter createAppRouter(AuthBloc authBloc) {
path: AppRoutes.settingsAccount,
builder: (context, state) => const AccountScreen(),
),
GoRoute(
path: AppRoutes.settingsChangePassword,
builder: (context, state) => const ChangePasswordScreen(),
),
GoRoute(
path: AppRoutes.settingsEditProfile,
builder: (context, state) => const EditProfileScreen(),
-4
View File
@@ -3,9 +3,6 @@ class AppRoutes {
static const authBoot = '/boot';
static const authLogin = '/';
static const authRegister = '/register';
static const authRegisterVerification = '/register/verification';
static const authResetPassword = '/reset-password';
static const homeMain = '/home';
@@ -31,6 +28,5 @@ class AppRoutes {
static const settingsFeatures = '/settings/features';
static const settingsMemory = '/settings/memory';
static const settingsAccount = '/settings/account';
static const settingsChangePassword = '/change-password';
static const settingsEditProfile = '/edit-profile';
}