2026-04-10 10:40:44 +08:00
|
|
|
import 'dart:async';
|
|
|
|
|
|
2026-04-02 18:39:35 +08:00
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
|
|
|
|
|
|
import '../../../../core/logging/logger.dart';
|
|
|
|
|
import '../../data/repositories/auth_repository.dart';
|
|
|
|
|
import 'auth_state.dart';
|
|
|
|
|
|
|
|
|
|
class AuthBloc extends ChangeNotifier {
|
|
|
|
|
AuthBloc({required AuthRepository repository}) : _repository = repository;
|
|
|
|
|
|
|
|
|
|
final AuthRepository _repository;
|
|
|
|
|
final Logger _logger = getLogger('features.auth.bloc');
|
|
|
|
|
AuthState _state = AuthState.initial;
|
|
|
|
|
bool _handlingUnauthorized = false;
|
|
|
|
|
|
|
|
|
|
AuthState get state => _state;
|
|
|
|
|
|
|
|
|
|
Future<void> start() async {
|
|
|
|
|
_state = _state.copyWith(status: AuthStatus.loading, errorMessage: null);
|
|
|
|
|
notifyListeners();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
final user = await _repository.recoverSession();
|
|
|
|
|
if (user == null) {
|
|
|
|
|
_state = const AuthState(status: AuthStatus.unauthenticated);
|
|
|
|
|
} else {
|
|
|
|
|
_state = AuthState(status: AuthStatus.authenticated, user: user);
|
|
|
|
|
}
|
|
|
|
|
notifyListeners();
|
|
|
|
|
} catch (error, stackTrace) {
|
|
|
|
|
_logger.error(
|
2026-04-03 16:56:47 +08:00
|
|
|
message: 'Session recovery failed: ${error.runtimeType}',
|
|
|
|
|
error: error.runtimeType.toString(),
|
2026-04-02 18:39:35 +08:00
|
|
|
stackTrace: stackTrace,
|
|
|
|
|
);
|
|
|
|
|
await _repository.clearLocalSession();
|
|
|
|
|
_state = AuthState(
|
|
|
|
|
status: AuthStatus.unauthenticated,
|
|
|
|
|
errorMessage: _toSafeMessage(error),
|
|
|
|
|
);
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> sendOtp(String email) async {
|
|
|
|
|
await _repository.sendOtp(email);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> loginWithOtp({
|
|
|
|
|
required String email,
|
|
|
|
|
required String otp,
|
2026-04-28 17:18:16 +08:00
|
|
|
String? language,
|
|
|
|
|
String? timezone,
|
2026-04-02 18:39:35 +08:00
|
|
|
}) async {
|
2026-04-28 17:18:16 +08:00
|
|
|
final user = await _repository.loginWithEmailOtp(
|
|
|
|
|
email: email,
|
|
|
|
|
otp: otp,
|
|
|
|
|
language: language,
|
|
|
|
|
timezone: timezone,
|
|
|
|
|
);
|
2026-04-02 18:39:35 +08:00
|
|
|
_logger.info(message: 'User logged in', extra: {'user_id': user.id});
|
|
|
|
|
_state = AuthState(status: AuthStatus.authenticated, user: user);
|
|
|
|
|
notifyListeners();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> logout() async {
|
|
|
|
|
_logger.info(message: 'User logged out');
|
|
|
|
|
_state = const AuthState(status: AuthStatus.unauthenticated);
|
|
|
|
|
notifyListeners();
|
2026-04-10 10:40:44 +08:00
|
|
|
|
|
|
|
|
unawaited(
|
|
|
|
|
_repository.logout().catchError((Object error, StackTrace stackTrace) {
|
|
|
|
|
_logger.error(
|
|
|
|
|
message: 'User logout failed: ${error.runtimeType}',
|
|
|
|
|
error: error.runtimeType.toString(),
|
|
|
|
|
stackTrace: stackTrace,
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
);
|
2026-04-02 18:39:35 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Future<void> handleUnauthorized401() async {
|
|
|
|
|
if (_handlingUnauthorized) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
_handlingUnauthorized = true;
|
|
|
|
|
try {
|
|
|
|
|
await _repository.clearLocalSession();
|
|
|
|
|
_logger.warning(message: 'Session invalidated by 401 callback');
|
|
|
|
|
_state = const AuthState(status: AuthStatus.unauthenticated);
|
|
|
|
|
notifyListeners();
|
|
|
|
|
} finally {
|
|
|
|
|
_handlingUnauthorized = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String _toSafeMessage(Object error) {
|
|
|
|
|
return 'Request failed, please try again';
|
|
|
|
|
}
|
|
|
|
|
}
|