Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions src/mobile/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'dart:async';
import 'package:airqo/core/utils/logging_bloc_observer.dart';
import 'package:airqo/src/app/auth/bloc/ForgotPasswordBloc/forgot_password_bloc.dart';
import 'package:airqo/src/app/auth/bloc/auth_bloc.dart';
import 'package:airqo/src/app/auth/pages/welcome_screen.dart';
import 'package:airqo/src/app/auth/repository/auth_repository_impl.dart';
import 'package:airqo/src/app/auth/repository/auth_repository.dart';
import 'package:airqo/src/app/auth/repository/social_auth_repository.dart';
Expand Down Expand Up @@ -237,6 +236,8 @@ class Decider extends StatefulWidget {
}

class _DeciderState extends State<Decider> with WidgetsBindingObserver {
bool _hasRequestedUserLoad = false;

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -275,14 +276,11 @@ class _DeciderState extends State<Decider> with WidgetsBindingObserver {

Future<void> _checkTokenExpiryOnResume() async {
final authBloc = context.read<AuthBloc>();
// Only refresh when the user is authenticated; guests have no token,
// and if already expired we don't need to fire again.
final currentState = authBloc.state;
if (currentState is! AuthLoaded) return;

final token = await AuthHelper.refreshTokenIfNeeded();
if (token == null && mounted) {
// Route through GlobalAuthManager so the de-duplication guard applies.
GlobalAuthManager.instance.notifySessionExpired();
}
}
Expand All @@ -297,13 +295,23 @@ class _DeciderState extends State<Decider> with WidgetsBindingObserver {
BlocConsumer<AuthBloc, AuthState>(
listener: (context, authState) {
if (authState is SessionExpiredState) {
_hasRequestedUserLoad = false;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Your session has expired. Please log in again.'),
duration: Duration(seconds: 4),
),
);
} else if (authState is AuthLoaded) {
if (!_hasRequestedUserLoad) {
_hasRequestedUserLoad = true;
context.read<UserBloc>().add(LoadUser());
}
} else if (authState is GuestUser ||
authState is AuthLoadingError ||
authState is OAuthCancelled) {
_hasRequestedUserLoad = false;
}
},
builder: (context, authState) {
Expand All @@ -316,20 +324,19 @@ class _DeciderState extends State<Decider> with WidgetsBindingObserver {
}

if (authState is SessionExpiredState) {
return const WelcomeScreen();
return NavPage();
}

if (authState is GuestUser) {
return NavPage();
}

if (authState is AuthLoaded) {
context.read<UserBloc>().add(LoadUser());
return NavPage();
}

if (authState is OAuthCancelled) {
return const WelcomeScreen();
return NavPage();
}

if (authState is AuthLoadingError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import '../../repository/auth_repository.dart';
import 'forgot_password_event.dart';
import 'forgot_password_state.dart';


class PasswordResetBloc extends Bloc<PasswordResetEvent, PasswordResetState> {
final AuthRepository authRepository;

PasswordResetBloc({required this.authRepository}) : super(PasswordResetInitial()) {
PasswordResetBloc({required this.authRepository})
: super(PasswordResetInitial()) {
on<RequestPasswordReset>((event, emit) async {
try {
emit(PasswordResetLoading(email: event.email));
await authRepository.requestPasswordReset(event.email);
emit(PasswordResetSuccess(message: '',email: event.email ));
emit(PasswordResetSuccess(message: '', email: event.email));
} catch (e) {
String errorMessage = _getErrorMessage(e.toString());
emit(PasswordResetError(message: errorMessage));
Expand All @@ -29,7 +29,9 @@ class PasswordResetBloc extends Bloc<PasswordResetEvent, PasswordResetState> {
);
emit(PasswordResetSuccess(message: message));
} catch (error) {
emit(PasswordResetError(message: 'Failed to update password. \nPlease re-check the code you entered'));
emit(PasswordResetError(
message:
'Failed to update password. \nPlease re-check the code you entered'));
}
});

Expand All @@ -43,18 +45,18 @@ class PasswordResetBloc extends Bloc<PasswordResetEvent, PasswordResetState> {
emit(PasswordResetLoading(email: email));

try {

if (email == null || email.isEmpty) {
emit(const PasswordResetError(
message: "Session expired. Please start the password reset process again.",
message:
"Session expired. Please start the password reset process again.",
));
return;
}

final token = await authRepository.validatePinFormat(event.pin, email);
final token = await authRepository.validateResetCodeFormat(event.pin);

emit(PasswordResetVerified(
message: "PIN successfully verified. Proceed to reset password.",
message: "Code format accepted. Proceed to reset password.",
token: token,
));
} catch (e) {
Expand All @@ -66,54 +68,55 @@ class PasswordResetBloc extends Bloc<PasswordResetEvent, PasswordResetState> {

String _getErrorMessage(String error) {
String cleanError = error.replaceAll('Exception: ', '');
if (cleanError.toLowerCase().contains('user not found') ||

if (cleanError.toLowerCase().contains('user not found') ||
cleanError.toLowerCase().contains('email not found') ||
cleanError.toLowerCase().contains('no user found')) {
return 'No account found with this email address.';
}

if (cleanError.toLowerCase().contains('invalid email') ||
cleanError.toLowerCase().contains('email format')) {
return 'Please enter a valid email address.';
}

if (cleanError.toLowerCase().contains('network') ||
cleanError.toLowerCase().contains('connection') ||
cleanError.toLowerCase().contains('timeout')) {
return 'Network error. Please check your connection and try again.';
}

if (cleanError.toLowerCase().contains('server error') ||
cleanError.toLowerCase().contains('internal server error') ||
cleanError.toLowerCase().contains('500')) {
return 'Server error. Please try again later.';
}

if (cleanError.toLowerCase().contains('too many requests') ||
cleanError.toLowerCase().contains('rate limit')) {
return 'Too many requests. Please wait a moment and try again.';
}

if (cleanError.toLowerCase().contains('unauthorized') ||
cleanError.toLowerCase().contains('401')) {
return 'Authentication error. Please try again.';
}

if (cleanError.toLowerCase().contains('forbidden') ||
cleanError.toLowerCase().contains('403')) {
return 'Access denied. Please contact support.';
}

if (cleanError.toLowerCase().contains('bad request') ||
cleanError.toLowerCase().contains('400')) {
return 'Invalid request. Please check your email and try again.';
}

if (cleanError.isEmpty || cleanError.toLowerCase().contains('something went wrong')) {

if (cleanError.isEmpty ||
cleanError.toLowerCase().contains('something went wrong')) {
return 'Unable to send reset code. Please try again.';
}

return cleanError;
}
}
17 changes: 12 additions & 5 deletions src/mobile/lib/src/app/auth/bloc/auth_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> with UiLoggy {
AuthBloc({
required this.authRepository,
required this.socialAuthRepository,
}) : super(AuthInitial()) {
}) : super(GuestUser()) {
on<AppStarted>(_onAppStarted);

on<LoginUser>(_onLoginUser);
Expand All @@ -48,18 +48,19 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> with UiLoggy {
}

Future<void> _onAppStarted(AppStarted event, Emitter<AuthState> emit) async {
emit(AuthLoading());
try {
final token = await SecureStorageRepository.instance
.getSecureData(SecureStorageKeys.authToken);

if (token != null && token.isNotEmpty) {
emit(AuthLoading());
final validToken = await AuthHelper.refreshTokenIfNeeded();
if (validToken == null) {
loggy.warning(
'Token found on app start but silent refresh failed — treating as session expiry');
await _clearAuthData();
emit(SessionExpiredState());
emit(GuestUser());
} else {
final userId =
await AuthHelper.getCurrentUserId(suppressGuestWarning: true);
Expand All @@ -68,12 +69,16 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> with UiLoggy {
}
emit(AuthLoaded(AuthPurpose.login));
}
} else {
emit(GuestUser());
}
} catch (e) {
debugPrint("Error checking auth state: $e");
emit(AuthLoadingError("Failed to check authentication state."));
try {
await _clearAuthData();
} catch (clearError) {
loggy.error(
"Failed to clear auth data after startup error: $clearError");
}
emit(GuestUser());
}
Comment thread
Mozart299 marked this conversation as resolved.
}

Expand Down Expand Up @@ -179,10 +184,12 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> with UiLoggy {
loggy.info('Session expired - clearing auth tokens and cached data');
await _clearAuthData();
emit(SessionExpiredState());
emit(GuestUser());
} catch (e) {
debugPrint("Session expiry cleanup error: $e");
loggy.error("Session expiry cleanup error: $e");
emit(SessionExpiredState());
emit(GuestUser());
}
}

Expand Down
Loading
Loading