fix(apps): consolidate FormzInput validators and fix login screen
- Move FormzInput validators to core/form_inputs/form_inputs.dart - Fix login_screen.dart syntax error (missing 'class' keyword) - Remove unused _isLoading field - Fix unnecessary const keywords - Update login_cubit and register_cubit imports - Remove duplicate FormzInput definitions from register_cubit - Add Toast and Banner UI feedback system - Remove legacy login/register screens (login_code, login_email, login_password, register_step2) - Remove unused warning_banner widget - Update tests for new error messages and DI setup
This commit is contained in:
@@ -6,36 +6,42 @@ import '../storage/token_storage.dart';
|
||||
class ApiClient {
|
||||
final Dio _dio;
|
||||
final TokenStorage _tokenStorage;
|
||||
final Future<bool> Function(String)? _refreshToken;
|
||||
final ApiInterceptor _interceptor;
|
||||
|
||||
ApiClient({
|
||||
factory ApiClient({
|
||||
required String baseUrl,
|
||||
required TokenStorage tokenStorage,
|
||||
Dio? dio,
|
||||
Future<bool> Function(String)? refreshToken,
|
||||
}) : _tokenStorage = tokenStorage,
|
||||
_refreshToken = refreshToken,
|
||||
_dio = dio ?? Dio(BaseOptions(baseUrl: baseUrl)) {
|
||||
_dio.interceptors.add(
|
||||
ApiInterceptor(
|
||||
tokenStorage: _tokenStorage,
|
||||
dio: _dio,
|
||||
onTokenRefresh: _handleTokenRefresh,
|
||||
),
|
||||
}) {
|
||||
final effectiveDio = dio ?? Dio(BaseOptions(baseUrl: baseUrl));
|
||||
final interceptor = ApiInterceptor(
|
||||
tokenStorage: tokenStorage,
|
||||
dio: effectiveDio,
|
||||
);
|
||||
effectiveDio.interceptors.add(interceptor);
|
||||
return ApiClient._(
|
||||
dio: effectiveDio,
|
||||
tokenStorage: tokenStorage,
|
||||
interceptor: interceptor,
|
||||
);
|
||||
}
|
||||
|
||||
ApiClient._({
|
||||
required Dio dio,
|
||||
required TokenStorage tokenStorage,
|
||||
required ApiInterceptor interceptor,
|
||||
}) : _dio = dio,
|
||||
_tokenStorage = tokenStorage,
|
||||
_interceptor = interceptor;
|
||||
|
||||
Dio get dio => _dio;
|
||||
|
||||
Future<bool> _handleTokenRefresh() async {
|
||||
final refreshToken = await _tokenStorage.getRefreshToken();
|
||||
if (refreshToken == null || _refreshToken == null) return false;
|
||||
try {
|
||||
final success = await _refreshToken!(refreshToken);
|
||||
return success;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
void setRefreshCallback(Future<bool> Function(String) refresh) {
|
||||
_interceptor.onTokenRefresh = () async {
|
||||
final token = await _tokenStorage.getRefreshToken();
|
||||
if (token == null) return false;
|
||||
return refresh(token);
|
||||
};
|
||||
}
|
||||
|
||||
Future<Response<T>> get<T>(String path, {Options? options}) async {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
abstract class ApiException implements Exception {
|
||||
final String message;
|
||||
final int? statusCode;
|
||||
@@ -6,12 +8,58 @@ abstract class ApiException implements Exception {
|
||||
|
||||
factory ApiException.fromDioError(Object error) {
|
||||
if (error is ApiException) return error;
|
||||
return ServerException('Request failed: ${error.toString()}');
|
||||
}
|
||||
}
|
||||
if (error is DioException) {
|
||||
final response = error.response;
|
||||
final statusCode = response?.statusCode;
|
||||
final data = response?.data;
|
||||
|
||||
class NetworkException extends ApiException {
|
||||
const NetworkException(super.message);
|
||||
String detail;
|
||||
if (data is Map<String, dynamic>) {
|
||||
detail =
|
||||
(data['detail'] ?? data['message'] ?? data['error'])?.toString() ??
|
||||
'请求失败';
|
||||
} else {
|
||||
detail = '请求失败';
|
||||
}
|
||||
|
||||
final localized = _localizeError(detail, statusCode);
|
||||
|
||||
if (statusCode == 401) {
|
||||
return UnauthorizedException(localized);
|
||||
}
|
||||
if (statusCode == 422) {
|
||||
return ValidationException(
|
||||
localized,
|
||||
errors: data,
|
||||
statusCode: statusCode,
|
||||
);
|
||||
}
|
||||
return ServerException(localized, statusCode: statusCode);
|
||||
}
|
||||
return const ServerException('网络错误');
|
||||
}
|
||||
|
||||
static String _localizeError(String detail, int? statusCode) {
|
||||
if (statusCode == 401) {
|
||||
return '邮箱或密码错误';
|
||||
}
|
||||
if (statusCode == 403) {
|
||||
return '没有权限执行此操作';
|
||||
}
|
||||
if (statusCode == 404) {
|
||||
return '请求的资源不存在';
|
||||
}
|
||||
if (statusCode == 429) {
|
||||
return '请求过于频繁,请稍后再试';
|
||||
}
|
||||
if (statusCode != null && statusCode >= 500) {
|
||||
return '服务器错误,请稍后再试';
|
||||
}
|
||||
if (detail.contains('credentials') || detail.contains('password')) {
|
||||
return '邮箱或密码错误';
|
||||
}
|
||||
return detail;
|
||||
}
|
||||
}
|
||||
|
||||
class ServerException extends ApiException {
|
||||
@@ -19,7 +67,7 @@ class ServerException extends ApiException {
|
||||
}
|
||||
|
||||
class UnauthorizedException extends ApiException {
|
||||
const UnauthorizedException([super.message = 'Authentication required'])
|
||||
const UnauthorizedException([super.message = '请重新登录'])
|
||||
: super(statusCode: 401);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ import '../storage/token_storage.dart';
|
||||
|
||||
class ApiInterceptor extends Interceptor {
|
||||
final TokenStorage tokenStorage;
|
||||
final Future<bool> Function()? onTokenRefresh;
|
||||
final Dio dio;
|
||||
Future<bool> Function()? onTokenRefresh;
|
||||
|
||||
ApiInterceptor({
|
||||
required this.tokenStorage,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import 'dart:io';
|
||||
|
||||
class Env {
|
||||
static String get apiUrl {
|
||||
const url = String.fromEnvironment('API_URL');
|
||||
return url.isNotEmpty ? url : 'http://localhost:8000';
|
||||
if (url.isNotEmpty) return url;
|
||||
if (Platform.isAndroid) {
|
||||
return 'http://10.0.2.2:5775';
|
||||
}
|
||||
return 'http://localhost:5775';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,17 +12,22 @@ import '../../features/auth/presentation/bloc/auth_bloc.dart';
|
||||
final sl = GetIt.instance;
|
||||
|
||||
Future<void> configureDependencies() async {
|
||||
if (sl.isRegistered<ApiClient>()) {
|
||||
await sl.reset();
|
||||
}
|
||||
|
||||
final dio = Dio(BaseOptions(baseUrl: Env.apiUrl));
|
||||
final secureStorage = const FlutterSecureStorage();
|
||||
final tokenStorage = SecureTokenStorage(secureStorage);
|
||||
|
||||
sl.registerSingleton<Dio>(dio);
|
||||
sl.registerSingleton<FlutterSecureStorage>(secureStorage);
|
||||
sl.registerSingleton<TokenStorage>(tokenStorage);
|
||||
|
||||
final authApi = AuthApi(
|
||||
ApiClient(baseUrl: Env.apiUrl, tokenStorage: tokenStorage, dio: dio),
|
||||
final apiClient = ApiClient(
|
||||
baseUrl: Env.apiUrl,
|
||||
tokenStorage: tokenStorage,
|
||||
dio: dio,
|
||||
);
|
||||
sl.registerSingleton<ApiClient>(apiClient);
|
||||
|
||||
final authApi = AuthApi(apiClient);
|
||||
sl.registerSingleton<AuthApi>(authApi);
|
||||
|
||||
final authRepository = AuthRepositoryImpl(
|
||||
@@ -31,22 +36,14 @@ Future<void> configureDependencies() async {
|
||||
);
|
||||
sl.registerSingleton<AuthRepository>(authRepository);
|
||||
|
||||
sl.unregister<ApiClient>();
|
||||
sl.registerSingleton<ApiClient>(
|
||||
ApiClient(
|
||||
baseUrl: Env.apiUrl,
|
||||
tokenStorage: tokenStorage,
|
||||
dio: dio,
|
||||
refreshToken: (token) async {
|
||||
try {
|
||||
await authRepository.refresh(token);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
apiClient.setRefreshCallback((token) async {
|
||||
try {
|
||||
await authRepository.refresh(token);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
sl.registerSingleton<AuthBloc>(AuthBloc(authRepository));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:formz/formz.dart';
|
||||
|
||||
class Username extends FormzInput<String, String> {
|
||||
const Username.pure() : super.pure('');
|
||||
const Username.dirty([super.value = '']) : super.dirty();
|
||||
|
||||
@override
|
||||
String? validator(String value) {
|
||||
if (value.isEmpty) return '请输入用户名';
|
||||
if (value.length < 3) return '用户名至少 3 个字符';
|
||||
if (value.length > 30) return '用户名最多 30 个字符';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class Email extends FormzInput<String, String> {
|
||||
const Email.pure() : super.pure('');
|
||||
const Email.dirty([super.value = '']) : super.dirty();
|
||||
|
||||
static final _regex = RegExp(r'^[\w.-]+@[\w.-]+\.\w+$');
|
||||
|
||||
@override
|
||||
String? validator(String value) {
|
||||
if (value.isEmpty) return '请输入邮箱';
|
||||
if (!_regex.hasMatch(value)) return '邮箱格式不正确';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class Password extends FormzInput<String, String> {
|
||||
const Password.pure() : super.pure('');
|
||||
const Password.dirty([super.value = '']) : super.dirty();
|
||||
|
||||
@override
|
||||
String? validator(String value) {
|
||||
if (value.isEmpty) return '请输入密码';
|
||||
if (value.length < 6) return '密码至少 6 个字符';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class VerificationCode extends FormzInput<String, String> {
|
||||
const VerificationCode.pure() : super.pure('');
|
||||
const VerificationCode.dirty([super.value = '']) : super.dirty();
|
||||
|
||||
@override
|
||||
String? validator(String value) {
|
||||
if (value.isEmpty) return '请输入验证码';
|
||||
if (!RegExp(r'^\d{6}$').hasMatch(value)) return '验证码必须是 6 位数字';
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,9 @@ import 'package:go_router/go_router.dart';
|
||||
import '../../features/auth/presentation/bloc/auth_bloc.dart';
|
||||
import '../../features/auth/presentation/bloc/auth_state.dart';
|
||||
import 'go_router_refresh_stream.dart';
|
||||
import '../../features/auth/ui/screens/login_email_screen.dart';
|
||||
import '../../features/auth/ui/screens/login_password_screen.dart';
|
||||
import '../../features/auth/ui/screens/login_code_screen.dart';
|
||||
import '../../features/auth/ui/screens/login_screen.dart';
|
||||
import '../../features/auth/ui/screens/register_screen.dart';
|
||||
import '../../features/auth/ui/screens/register_step2_screen.dart';
|
||||
import '../../features/auth/ui/screens/register_verification_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';
|
||||
@@ -60,22 +58,14 @@ GoRouter createAppRouter(AuthBloc authBloc) {
|
||||
return null;
|
||||
},
|
||||
routes: [
|
||||
GoRoute(path: '/', builder: (context, state) => const LoginEmailScreen()),
|
||||
GoRoute(
|
||||
path: '/login/password',
|
||||
builder: (context, state) => const LoginPasswordScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/login/code',
|
||||
builder: (context, state) => const LoginCodeScreen(),
|
||||
),
|
||||
GoRoute(path: '/', builder: (context, state) => const LoginScreen()),
|
||||
GoRoute(
|
||||
path: '/register',
|
||||
builder: (context, state) => const RegisterScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/register/step2',
|
||||
builder: (context, state) => const RegisterStep2Screen(),
|
||||
path: '/register/verification',
|
||||
builder: (context, state) => const RegisterVerificationScreen(),
|
||||
),
|
||||
GoRoute(path: '/home', builder: (context, state) => const HomeScreen()),
|
||||
GoRoute(
|
||||
|
||||
Reference in New Issue
Block a user