BLoC/Cubit state management, ADHD-friendly theme (calming teal, no red), GetIt DI, GoRouter navigation. Screens: task dashboard, focus mode, task create/detail, streaks, time perception, settings, onboarding, auth. Custom widgets: TaskCard, RewardPopup, StreakRing, GentleNudgeCard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
152 lines
4.7 KiB
Dart
152 lines
4.7 KiB
Dart
import 'package:equatable/equatable.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:focusflow_shared/focusflow_shared.dart';
|
|
|
|
import '../../../../core/network/api_client.dart';
|
|
import '../../../../core/network/interceptors/auth_interceptor.dart';
|
|
import '../../../../main.dart';
|
|
|
|
// ── Events ─────────────────────────────────────────────────────────
|
|
|
|
sealed class AuthEvent extends Equatable {
|
|
const AuthEvent();
|
|
@override
|
|
List<Object?> get props => [];
|
|
}
|
|
|
|
class AuthLoginRequested extends AuthEvent {
|
|
final String email;
|
|
final String password;
|
|
const AuthLoginRequested({required this.email, required this.password});
|
|
@override
|
|
List<Object?> get props => [email, password];
|
|
}
|
|
|
|
class AuthSignupRequested extends AuthEvent {
|
|
final String displayName;
|
|
final String email;
|
|
final String password;
|
|
const AuthSignupRequested({
|
|
required this.displayName,
|
|
required this.email,
|
|
required this.password,
|
|
});
|
|
@override
|
|
List<Object?> get props => [displayName, email, password];
|
|
}
|
|
|
|
class AuthLogoutRequested extends AuthEvent {
|
|
const AuthLogoutRequested();
|
|
}
|
|
|
|
class AuthCheckRequested extends AuthEvent {}
|
|
|
|
// ── States ─────────────────────────────────────────────────────────
|
|
|
|
sealed class AuthState extends Equatable {
|
|
const AuthState();
|
|
@override
|
|
List<Object?> get props => [];
|
|
}
|
|
|
|
class AuthInitial extends AuthState {}
|
|
|
|
class AuthLoading extends AuthState {}
|
|
|
|
class AuthAuthenticated extends AuthState {
|
|
final User user;
|
|
const AuthAuthenticated(this.user);
|
|
@override
|
|
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];
|
|
}
|
|
|
|
// ── BLoC ───────────────────────────────────────────────────────────
|
|
|
|
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
|
final ApiClient _api = getIt<ApiClient>();
|
|
|
|
AuthBloc() : super(AuthInitial()) {
|
|
on<AuthCheckRequested>(_onCheck);
|
|
on<AuthLoginRequested>(_onLogin);
|
|
on<AuthSignupRequested>(_onSignup);
|
|
on<AuthLogoutRequested>(_onLogout);
|
|
}
|
|
|
|
Future<void> _onCheck(AuthCheckRequested event, Emitter<AuthState> emit) async {
|
|
final hasToken = await AuthInterceptor.hasToken();
|
|
if (hasToken) {
|
|
// In a production app we would validate the token and fetch the user profile.
|
|
// For now we emit a placeholder user.
|
|
emit(AuthAuthenticated(
|
|
User(
|
|
id: 'local',
|
|
email: '',
|
|
displayName: 'Friend',
|
|
createdAt: DateTime.now(),
|
|
),
|
|
));
|
|
} else {
|
|
emit(AuthUnauthenticated());
|
|
}
|
|
}
|
|
|
|
Future<void> _onLogin(AuthLoginRequested event, Emitter<AuthState> emit) async {
|
|
emit(AuthLoading());
|
|
try {
|
|
final response = await _api.login(event.email, event.password);
|
|
if (response.status == 'success' && response.data != null) {
|
|
final data = response.data!;
|
|
await AuthInterceptor.saveTokens(
|
|
accessToken: data['accessToken'] as String,
|
|
refreshToken: data['refreshToken'] as String,
|
|
);
|
|
final user = User.fromJson(data['user'] as Map<String, dynamic>);
|
|
emit(AuthAuthenticated(user));
|
|
} else {
|
|
emit(AuthError(response.error?.message ?? 'Login failed'));
|
|
}
|
|
} catch (e) {
|
|
emit(AuthError('Something went wrong. Please try again.'));
|
|
}
|
|
}
|
|
|
|
Future<void> _onSignup(AuthSignupRequested event, Emitter<AuthState> emit) async {
|
|
emit(AuthLoading());
|
|
try {
|
|
final response = await _api.register(event.displayName, event.email, event.password);
|
|
if (response.status == 'success' && response.data != null) {
|
|
final data = response.data!;
|
|
await AuthInterceptor.saveTokens(
|
|
accessToken: data['accessToken'] as String,
|
|
refreshToken: data['refreshToken'] as String,
|
|
);
|
|
final user = User.fromJson(data['user'] as Map<String, dynamic>);
|
|
emit(AuthAuthenticated(user));
|
|
} else {
|
|
emit(AuthError(response.error?.message ?? 'Sign up failed'));
|
|
}
|
|
} catch (e) {
|
|
emit(AuthError('Something went wrong. Please try again.'));
|
|
}
|
|
}
|
|
|
|
Future<void> _onLogout(AuthLogoutRequested event, Emitter<AuthState> emit) async {
|
|
try {
|
|
await _api.logout();
|
|
} catch (_) {
|
|
// Best-effort server logout — clear local tokens regardless.
|
|
}
|
|
await AuthInterceptor.clearTokens();
|
|
emit(AuthUnauthenticated());
|
|
}
|
|
}
|