feat: complete centralized error handling system with documentation
This commit is contained in:
@@ -60,9 +60,17 @@ class AuthRepositoryImpl
|
||||
},
|
||||
verificationFailed: (FirebaseAuthException e) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(
|
||||
Exception(e.message ?? 'Phone verification failed.'),
|
||||
);
|
||||
// Map Firebase network errors to NetworkException
|
||||
if (e.code == 'network-request-failed' ||
|
||||
e.message?.contains('Unable to resolve host') == true) {
|
||||
completer.completeError(
|
||||
const domain.NetworkException(technicalMessage: 'Auth network failure'),
|
||||
);
|
||||
} else {
|
||||
completer.completeError(
|
||||
domain.SignInFailedException(technicalMessage: 'Firebase ${e.code}: ${e.message}'),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
codeSent: (String verificationId, _) {
|
||||
@@ -107,10 +115,25 @@ class AuthRepositoryImpl
|
||||
verificationId: verificationId,
|
||||
smsCode: smsCode,
|
||||
);
|
||||
final UserCredential userCredential = await firebaseAuth.signInWithCredential(credential);
|
||||
final UserCredential userCredential = await executeProtected(
|
||||
() async {
|
||||
try {
|
||||
return await firebaseAuth.signInWithCredential(credential);
|
||||
} on FirebaseAuthException catch (e) {
|
||||
if (e.code == 'invalid-verification-code') {
|
||||
throw const domain.InvalidCredentialsException(
|
||||
technicalMessage: 'Invalid OTP code entered.',
|
||||
);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
},
|
||||
);
|
||||
final User? firebaseUser = userCredential.user;
|
||||
if (firebaseUser == null) {
|
||||
throw Exception('Phone verification failed, no Firebase user received.');
|
||||
throw const domain.SignInFailedException(
|
||||
technicalMessage: 'Phone verification failed, no Firebase user received.',
|
||||
);
|
||||
}
|
||||
|
||||
final QueryResult<GetUserByIdData, GetUserByIdVariables> response =
|
||||
@@ -135,7 +158,9 @@ class AuthRepositoryImpl
|
||||
} else {
|
||||
if (user.userRole != 'STAFF') {
|
||||
await firebaseAuth.signOut();
|
||||
throw Exception('User is not authorized for this app.');
|
||||
throw const domain.UnauthorizedAppException(
|
||||
technicalMessage: 'User is not authorized for this app.',
|
||||
);
|
||||
}
|
||||
final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables>
|
||||
staffResponse = await executeProtected(() => dataConnect
|
||||
@@ -145,19 +170,23 @@ class AuthRepositoryImpl
|
||||
.execute());
|
||||
if (staffResponse.data.staffs.isNotEmpty) {
|
||||
await firebaseAuth.signOut();
|
||||
throw Exception(
|
||||
'This user already has a staff profile. Please log in.',
|
||||
throw const domain.AccountExistsException(
|
||||
technicalMessage: 'This user already has a staff profile. Please log in.',
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (user == null) {
|
||||
await firebaseAuth.signOut();
|
||||
throw Exception('Authenticated user profile not found in database.');
|
||||
throw const domain.UserNotFoundException(
|
||||
technicalMessage: 'Authenticated user profile not found in database.',
|
||||
);
|
||||
}
|
||||
if (user.userRole != 'STAFF') {
|
||||
await firebaseAuth.signOut();
|
||||
throw Exception('User is not authorized for this app.');
|
||||
throw const domain.UnauthorizedAppException(
|
||||
technicalMessage: 'User is not authorized for this app.',
|
||||
);
|
||||
}
|
||||
|
||||
final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables>
|
||||
@@ -168,8 +197,8 @@ class AuthRepositoryImpl
|
||||
.execute());
|
||||
if (staffResponse.data.staffs.isEmpty) {
|
||||
await firebaseAuth.signOut();
|
||||
throw Exception(
|
||||
'Your account is not registered yet. Please register first.',
|
||||
throw const domain.UserNotFoundException(
|
||||
technicalMessage: 'Your account is not registered yet. Please register first.',
|
||||
);
|
||||
}
|
||||
staffRecord = staffResponse.data.staffs.first;
|
||||
|
||||
@@ -38,7 +38,9 @@ class _PhoneVerificationPageState extends State<PhoneVerificationPage> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_authBloc.add(AuthResetRequested(mode: widget.mode));
|
||||
if (!_authBloc.isClosed) {
|
||||
_authBloc.add(AuthResetRequested(mode: widget.mode));
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -105,14 +107,15 @@ class _PhoneVerificationPageState extends State<PhoneVerificationPage> {
|
||||
}
|
||||
} else if (state.status == AuthStatus.error &&
|
||||
state.mode == AuthMode.signup) {
|
||||
final String message = state.errorMessage ?? '';
|
||||
if (message.contains('staff profile')) {
|
||||
final String messageKey = state.errorMessage ?? '';
|
||||
// Handle specific business logic errors for signup
|
||||
if (messageKey == 'errors.auth.account_exists') {
|
||||
final ScaffoldMessengerState messenger =
|
||||
ScaffoldMessenger.of(context);
|
||||
messenger.hideCurrentSnackBar();
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
content: Text(translateErrorKey(messageKey)),
|
||||
duration: const Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
@@ -120,7 +123,7 @@ class _PhoneVerificationPageState extends State<PhoneVerificationPage> {
|
||||
if (!mounted) return;
|
||||
Modular.to.navigate('/');
|
||||
});
|
||||
} else if (message.contains('not authorized')) {
|
||||
} else if (messageKey == 'errors.auth.unauthorized_app') {
|
||||
Modular.to.pop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
|
||||
class GetStartedBackground extends StatelessWidget {
|
||||
class GetStartedBackground extends StatefulWidget {
|
||||
const GetStartedBackground({super.key});
|
||||
|
||||
@override
|
||||
State<GetStartedBackground> createState() => _GetStartedBackgroundState();
|
||||
}
|
||||
|
||||
class _GetStartedBackgroundState extends State<GetStartedBackground> {
|
||||
bool _hasError = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@@ -26,12 +33,48 @@ class GetStartedBackground extends StatelessWidget {
|
||||
),
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ClipOval(
|
||||
child: Image.network(
|
||||
'https://images.unsplash.com/photo-1577219491135-ce391730fb2c?w=400&h=400&fit=crop&crop=faces',
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Image.asset(UiImageAssets.logoBlue);
|
||||
},
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Layer 1: The Fallback Logo (Always visible until image loads)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(48.0),
|
||||
child: Image.asset(UiImageAssets.logoBlue),
|
||||
),
|
||||
|
||||
// Layer 2: The Network Image (Only visible on success)
|
||||
if (!_hasError)
|
||||
Image.network(
|
||||
'https://images.unsplash.com/photo-1577219491135-ce391730fb2c?w=400&h=400&fit=crop&crop=faces',
|
||||
fit: BoxFit.cover,
|
||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||
if (wasSynchronouslyLoaded) return child;
|
||||
// Only animate opacity if we have a frame
|
||||
return AnimatedOpacity(
|
||||
opacity: frame == null ? 0 : 1,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
// While loading, show nothing (transparent) so layer 1 shows
|
||||
if (loadingProgress == null) return child;
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
// On error, show nothing (transparent) so layer 1 shows
|
||||
// Also schedule a state update to prevent retries if needed
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && !_hasError) {
|
||||
setState(() {
|
||||
_hasError = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../blocs/auth_event.dart';
|
||||
import '../../../blocs/auth_bloc.dart';
|
||||
@@ -118,7 +119,10 @@ class _OtpInputFieldState extends State<OtpInputField> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: UiConstants.space4),
|
||||
child: Center(
|
||||
child: Text(widget.error, style: UiTypography.body2r.textError),
|
||||
child: Text(
|
||||
translateErrorKey(widget.error),
|
||||
style: UiTypography.body2r.textError,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:staff_authentication/staff_authentication.dart';
|
||||
|
||||
/// A widget that displays the phone number input field with country code.
|
||||
@@ -100,7 +101,10 @@ class _PhoneInputFormFieldState extends State<PhoneInputFormField> {
|
||||
if (widget.error.isNotEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: UiConstants.space2),
|
||||
child: Text(widget.error, style: UiTypography.body2r.textError),
|
||||
child: Text(
|
||||
translateErrorKey(widget.error),
|
||||
style: UiTypography.body2r.textError,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user