Implements authentication flow with user registration

This commit implements a complete authentication flow, including user registration.

- Introduces an `AuthRegisterRequested` event to handle user registration.
- Persists the user ID in settings upon successful authentication.
- Modifies `AuthBloc` to load the user based on the stored ID, improving app launch persistence.
- Refactors `UserCubit` to only manage the user state and removes authentication logic.
- Removes `UserCubit` initialization from `main.dart` and triggers `AuthStarted` to initiate the authentication process.
This commit is contained in:
2025-07-12 22:40:31 +03:00
parent fd0c3dd9a8
commit 85daede4a9
8 changed files with 183 additions and 242 deletions
+5 -1
View File
@@ -59,7 +59,11 @@ Future<void> initGlobalDependencies() async {
null, // Временно null, будет заменен в initUserSpecificDependencies
),
);
getIt.registerFactory<AuthBloc>(() => AuthBloc(userCubit: getIt()));
getIt.registerFactory<AuthBloc>(() => AuthBloc(
userCubit: getIt(),
settingsRepository: getIt(),
userRepository: getIt(),
));
}
/// Инициализация зависимостей, специфичных для пользователя.
+59 -8
View File
@@ -3,27 +3,52 @@ import 'package:equatable/equatable.dart';
import 'package:budget_app/models/user.dart';
import 'package:budget_app/logic/user/user_cubit.dart';
import 'package:budget_app/injection_container.dart' as di;
import 'package:budget_app/data/repositories/interfaces/iglobal_settings_repository.dart';
import 'package:budget_app/data/repositories/interfaces/iuser_repository.dart';
part 'auth_event.dart';
part 'auth_state.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final UserCubit _userCubit;
final IGlobalSettingsRepository _settingsRepository;
final IUserRepository _userRepository;
AuthBloc({required UserCubit userCubit}) : _userCubit = userCubit, super(AuthInitial()) {
AuthBloc({
required UserCubit userCubit,
required IGlobalSettingsRepository settingsRepository,
required IUserRepository userRepository,
}) : _userCubit = userCubit,
_settingsRepository = settingsRepository,
_userRepository = userRepository,
super(AuthInitial()) {
on<AuthStarted>(_onAuthStarted);
on<AuthLoggedIn>(_onAuthLoggedIn);
on<AuthLoggedOut>(_onAuthLoggedOut);
on<AuthRegisterRequested>(_onAuthRegisterRequested);
}
void _onAuthStarted(AuthStarted event, Emitter<AuthState> emit) async {
final userState = _userCubit.state;
if (userState is UserLoaded && userState.user != null) {
// Если пользователь уже загружен, инициализируем его зависимости.
await di.initUserSpecificDependencies(userState.user!.id);
emit(AuthAuthenticated(user: userState.user!));
} else {
emit(AuthUnauthenticated());
try {
final userId = await _settingsRepository.getCurrentUserId();
if (userId != null) {
final user = await _userRepository.getById(userId);
if (user != null) {
// Пользователь найден, инициализируем зависимости и аутентифицируем
await di.initUserSpecificDependencies(user.id);
_userCubit.setUser(user);
emit(AuthAuthenticated(user: user));
} else {
// ID есть, а пользователя нет (ошибка) -> сбрасываем
await _settingsRepository.setCurrentUserId(null);
emit(AuthUnauthenticated());
}
} else {
// ID не найден, пользователь не аутентифицирован
emit(AuthUnauthenticated());
}
} catch (e) {
emit(AuthError(e.toString()));
}
}
@@ -31,14 +56,40 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
// Инициализируем зависимости для вошедшего пользователя.
await di.initUserSpecificDependencies(event.user.id);
_userCubit.setUser(event.user);
await _settingsRepository.setCurrentUserId(event.user.id);
emit(AuthAuthenticated(user: event.user));
}
void _onAuthLoggedOut(AuthLoggedOut event, Emitter<AuthState> emit) async {
// Сбрасываем пользовательские зависимости.
await di.resetUserSpecificDependencies();
await _settingsRepository.setCurrentUserId(null);
_userCubit.logout();
emit(AuthUnauthenticated());
}
Future<void> _onAuthRegisterRequested(
AuthRegisterRequested event, Emitter<AuthState> emit) async {
emit(AuthLoading());
try {
// 1. Создаем пользователя
final user = User(name: event.name, email: event.email);
await _userRepository.add(user);
// 2. Инициализируем его зависимости
await di.initUserSpecificDependencies(user.id);
// 3. Создаем начальные данные (теперь это сработает)
await _userCubit.createInitialData(user.id);
// 4. Сохраняем и устанавливаем пользователя
await _settingsRepository.setCurrentUserId(user.id);
_userCubit.setUser(user);
emit(AuthAuthenticated(user: user));
} catch (e) {
emit(AuthError(e.toString()));
}
}
}
+11 -3
View File
@@ -7,10 +7,8 @@ abstract class AuthEvent extends Equatable {
List<Object> get props => [];
}
// Событие, которое будет вызываться при инициализации BLoC
class AuthStarted extends AuthEvent {}
// Событие, которое будет вызываться при входе пользователя
class AuthLoggedIn extends AuthEvent {
final User user;
@@ -20,5 +18,15 @@ class AuthLoggedIn extends AuthEvent {
List<Object> get props => [user];
}
// Событие, которое будет вызываться при выходе пользователя
class AuthLoggedOut extends AuthEvent {}
class AuthRegisterRequested extends AuthEvent {
final String name;
final String email;
const AuthRegisterRequested({required this.name, required this.email});
@override
List<Object> get props => [name, email];
}
+12 -3
View File
@@ -7,10 +7,10 @@ abstract class AuthState extends Equatable {
List<Object> get props => [];
}
// Начальное состояние, пока мы не знаем, аутентифицирован ли пользователь
class AuthInitial extends AuthState {}
// Состояние, когда пользователь аутентифицирован
class AuthLoading extends AuthState {}
class AuthAuthenticated extends AuthState {
final User user;
@@ -20,5 +20,14 @@ class AuthAuthenticated extends AuthState {
List<Object> get props => [user];
}
// Состояние, когда пользователь не аутентифицирован
class AuthUnauthenticated extends AuthState {}
class AuthError extends AuthState {
final String message;
const AuthError(this.message);
@override
List<Object> get props => [message];
}
+3 -65
View File
@@ -16,7 +16,8 @@ import '/utils/transaction_utils.dart';
part 'user_state.dart';
/// Cubit для управления состоянием пользователя
/// Cubit для управления состоянием пользователя.
/// Больше не управляет процессом входа, а только хранит состояние пользователя.
class UserCubit extends Cubit<UserState> {
final IGlobalSettingsRepository _settingsRepository;
final IUserRepository _userRepository;
@@ -56,37 +57,6 @@ class UserCubit extends Cubit<UserState> {
set transactionRepository(ITransactionRepository? repo) =>
_transactionRepository = repo;
// Метод для инициализации UserCubit
Future<void> init() async {
await _init();
}
Future<void> _init() async {
emit(UserLoading(progress: 0.1, message: 'Инициализация...'));
try {
final userId = await _settingsRepository.getCurrentUserId();
User? currentUser;
if (userId != null) {
emit(UserLoading(progress: 0.3, message: 'Поиск пользователя...'));
currentUser = await _userRepository.getById(userId);
if (currentUser == null) {
await _settingsRepository.setCurrentUserId(null);
}
}
emit(UserLoading(progress: 1.0, message: 'Завершение...'));
await Future.delayed(const Duration(milliseconds: 300));
emit(UserLoaded(currentUser));
} catch (e, stack) {
_logger.e(
'--- UserCubit: FATAL ERROR in _init ---',
error: e,
stackTrace: stack,
);
emit(UserError('Ошибка загрузки пользователя: ${e.toString()}'));
}
}
Future<void> createInitialData(String userId) async {
// Проверяем, что репозитории были установлены, прежде чем их использовать.
if (_categoryRepository == null ||
@@ -120,22 +90,6 @@ class UserCubit extends Cubit<UserState> {
}
}
Future<void> _setCurrentUser(User user) async {
await _settingsRepository.setCurrentUserId(user.id);
emit(UserLoaded(user));
}
/// Устанавливает текущего пользователя
Future<void> setCurrentUser(User user) async {
emit(UserLoading());
try {
await _setCurrentUser(user);
} catch (e, stack) {
_logger.e('Error setting current user', error: e, stackTrace: stack);
emit(UserError('Ошибка установки пользователя: ${e.toString()}'));
}
}
/// Устанавливает текущего пользователя (прямая установка без загрузки)
void setUser(User user) {
emit(UserLoaded(user));
@@ -153,23 +107,6 @@ class UserCubit extends Cubit<UserState> {
}
}
/// Создает нового пользователя и устанавливает его как текущего
Future<void> createAndSetUser(String name, String email) async {
emit(UserLoading());
try {
final user = User(name: name, email: email);
await _userRepository.add(user);
// Создаем начальные данные для нового пользователя
await createInitialData(user.id);
await _setCurrentUser(user);
} catch (e, stack) {
_logger.e('Error creating user', error: e, stackTrace: stack);
emit(UserError('Ошибка создания пользователя: ${e.toString()}'));
}
}
/// Возвращает всех пользователей
Future<List<User>> getAllUsers() async {
try {
@@ -180,3 +117,4 @@ class UserCubit extends Cubit<UserState> {
}
}
}
+59 -74
View File
@@ -26,91 +26,76 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 1. Предоставляем глобальные Blocs, которые доступны всегда.
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => GetIt.instance<UserCubit>()..init(),
create: (context) => GetIt.instance<UserCubit>(),
),
BlocProvider(
create: (context) => GetIt.instance<AuthBloc>(),
create: (context) => GetIt.instance<AuthBloc>()..add(AuthStarted()),
),
],
child: BlocListener<UserCubit, UserState>(
// 2. Запускаем проверку аутентификации, как только UserCubit загрузил данные.
listenWhen: (previous, current) => current is UserLoaded,
listener: (context, userState) {
context.read<AuthBloc>().add(AuthStarted());
},
// 3. В зависимости от статуса аутентификации, строим разное дерево виджетов.
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, authState) {
// 4. Если пользователь аутентифицирован...
if (authState is AuthAuthenticated) {
// ...предоставляем все пользовательские зависимости.
return MultiBlocProvider(
providers: [
BlocProvider(create: (context) => GetIt.instance<SettingsCubit>()),
BlocProvider(create: (context) => GetIt.instance<TransactionBloc>()),
BlocProvider(create: (context) => GetIt.instance<SmsCubit>()),
// Можно добавить и остальные, если они нужны глобально в авторизованной зоне
],
// И строим приложение с пользовательской темой.
child: BlocBuilder<SettingsCubit, SettingsState>(
builder: (context, settingsState) {
final isDarkMode = settingsState is SettingsLoaded
? settingsState.isDarkMode
: false;
final languageCode = settingsState is SettingsLoaded
? settingsState.languageCode
: 'ru';
return MaterialApp(
title: 'Budget App',
theme: AppTheme.lightTheme(),
darkTheme: AppTheme.darkTheme(),
themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale(languageCode),
home: const HomePage(),
);
},
),
);
}
// 5. Если пользователь НЕ аутентифицирован, показываем SplashScreen или LoginPage
// с темой по умолчанию.
return MaterialApp(
title: 'Budget App',
theme: AppTheme.lightTheme(),
darkTheme: AppTheme.darkTheme(),
themeMode: ThemeMode.light, // Тема по умолчанию
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
child: BlocBuilder<AuthBloc, AuthState>(
builder: (context, authState) {
if (authState is AuthAuthenticated) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (context) => GetIt.instance<SettingsCubit>()),
BlocProvider(create: (context) => GetIt.instance<TransactionBloc>()),
BlocProvider(create: (context) => GetIt.instance<SmsCubit>()),
],
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('ru'), // Язык по умолчанию
home: BlocBuilder<UserCubit, UserState>(
builder: (context, userState) {
if (userState is UserLoading || userState is UserInitial) {
return const SplashScreen();
}
return const LoginPage();
child: BlocBuilder<SettingsCubit, SettingsState>(
builder: (context, settingsState) {
final isDarkMode = settingsState is SettingsLoaded
? settingsState.isDarkMode
: false;
final languageCode = settingsState is SettingsLoaded
? settingsState.languageCode
: 'ru';
return MaterialApp(
title: 'Budget App',
theme: AppTheme.lightTheme(),
darkTheme: AppTheme.darkTheme(),
themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale(languageCode),
home: const HomePage(),
);
},
),
);
},
),
}
Widget homeWidget;
if (authState is AuthInitial) {
homeWidget = const SplashScreen();
} else {
homeWidget = const LoginPage();
}
return MaterialApp(
title: 'Budget App',
theme: AppTheme.lightTheme(),
darkTheme: AppTheme.darkTheme(),
themeMode: ThemeMode.light, // Тема по умолчанию
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
locale: const Locale('ru'), // Язык по умолчанию
home: homeWidget,
);
},
),
);
}
+24 -38
View File
@@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '/l10n/app_localizations.dart';
import '../../logic/auth/auth_bloc.dart';
import '../../logic/user/user_cubit.dart'; // Импортируем UserCubit вместо UserService
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@@ -17,26 +16,26 @@ class _LoginPageState extends State<LoginPage> {
final _emailController = TextEditingController();
final _nameController = TextEditingController();
void _login() {
if (_formKey.currentState!.validate()) {
context.read<AuthBloc>().add(AuthRegisterRequested(
name: _nameController.text,
email: _emailController.text,
));
}
}
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(
context,
)!; // Получаем экземпляр локализации
final localizations = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
title: Text(localizations.loginPageTitle),
), // Локализованный заголовок
body: BlocListener<UserCubit, UserState>(
// Слушаем изменения состояния UserCubit
listenWhen: (previous, current) =>
current is UserLoaded || current is UserError,
),
body: BlocListener<AuthBloc, AuthState>(
listener: (context, state) {
if (state is UserLoaded && state.user != null) {
// Комментарий: При успешном создании пользователя отправляем событие в AuthBloc
context.read<AuthBloc>().add(AuthLoggedIn(user: state.user!));
} else if (state is UserError) {
// Комментарий: Показываем ошибку пользователю
if (state is AuthError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
@@ -52,11 +51,10 @@ class _LoginPageState extends State<LoginPage> {
controller: _nameController,
decoration: InputDecoration(
labelText: localizations.nameFieldLabel,
), // Локализованный текст
),
validator: (value) {
if (value == null || value.isEmpty) {
return localizations
.nameFieldEmptyError; // Локализованный текст
return localizations.nameFieldEmptyError;
}
return null;
},
@@ -65,41 +63,28 @@ class _LoginPageState extends State<LoginPage> {
controller: _emailController,
decoration: InputDecoration(
labelText: localizations.emailFieldLabel,
), // Локализованный текст
),
validator: (value) {
if (value == null || value.isEmpty) {
return localizations
.emailFieldEmptyError; // Локализованный текст
return localizations.emailFieldEmptyError;
}
return null;
},
),
const SizedBox(height: 20),
BlocBuilder<UserCubit, UserState>(
// Строим кнопку в зависимости от состояния UserCubit
BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
final isLoading = state is UserLoading;
final isLoading = state is AuthLoading;
return ElevatedButton(
onPressed: isLoading ? null : () {
// Комментарий: Блокируем кнопку во время загрузки
if (_formKey.currentState!.validate()) {
// Комментарий: Используем UserCubit для создания пользователя
context.read<UserCubit>().createAndSetUser(
_nameController.text,
_emailController.text,
);
}
},
onPressed: isLoading ? null : _login,
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
) // Показываем индикатор загрузки
: Text(
localizations.loginButtonText,
), // Локализованный текст
)
: Text(localizations.loginButtonText),
);
},
),
@@ -111,3 +96,4 @@ class _LoginPageState extends State<LoginPage> {
);
}
}
+10 -50
View File
@@ -1,60 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '/logic/user/user_cubit.dart';
class SplashScreen extends StatelessWidget {
const SplashScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: BlocBuilder<UserCubit, UserState>(
builder: (context, state) {
if (state is UserLoading) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 20),
Text(
state.message,
style: Theme.of(context).textTheme.bodyLarge,
),
const SizedBox(height: 10),
Text(
'${(state.progress * 100).toInt()}%',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
);
}
if (state is UserError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Ошибка загрузки',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 20),
Text(state.message),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () => context.read<UserCubit>().init(),
child: const Text('Повторить'),
),
],
),
);
}
return const Center(child: CircularProgressIndicator());
},
return const Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 20),
Text('Загрузка...'),
],
),
),
);
}