feat(core_localization): add error translation utility and new error messages
feat(client_auth): implement error handling with localized messages feat(client_hubs): implement error handling with localized messages feat(client_billing): navigate to home after billing feat(client_coverage): navigate to home after coverage feat(client_create_order): navigate to home after create order feat(client_settings): navigate to home after settings feat(client_view_orders): show hub name in order card fix(client_auth): handle existing firebase accounts during sign-up This commit introduces a new utility function, `translateErrorKey`, to translate error message keys to localized strings. It also adds new error messages to the localization files for both English and Spanish. The commit also implements error handling with localized messages in the client authentication and hubs features. This makes it easier for users to understand what went wrong and how to fix it. Additionally, the commit updates the navigation flow for the billing, coverage, create order, and settings features to navigate to the home page after the user completes the action. Finally, the commit fixes a bug where the hub name was not being displayed in the order card.
This commit is contained in:
@@ -1,7 +1,20 @@
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||
import 'package:krow_domain/krow_domain.dart'
|
||||
show
|
||||
InvalidCredentialsException,
|
||||
SignInFailedException,
|
||||
SignUpFailedException,
|
||||
WeakPasswordException,
|
||||
AccountExistsException,
|
||||
UserNotFoundException,
|
||||
UnauthorizedAppException,
|
||||
PasswordMismatchException,
|
||||
GoogleOnlyAccountException;
|
||||
|
||||
import '../../domain/repositories/auth_repository_interface.dart';
|
||||
|
||||
@@ -33,7 +46,9 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
|
||||
final firebase.User? firebaseUser = credential.user;
|
||||
if (firebaseUser == null) {
|
||||
throw Exception('Sign-in failed, no Firebase user received.');
|
||||
throw const SignInFailedException(
|
||||
technicalMessage: 'No Firebase user received after sign-in',
|
||||
);
|
||||
}
|
||||
|
||||
return _getUserProfile(
|
||||
@@ -44,12 +59,20 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
|
||||
} on firebase.FirebaseAuthException catch (e) {
|
||||
if (e.code == 'invalid-credential' || e.code == 'wrong-password') {
|
||||
throw Exception('Incorrect email or password.');
|
||||
throw InvalidCredentialsException(
|
||||
technicalMessage: 'Firebase error code: ${e.code}',
|
||||
);
|
||||
} else {
|
||||
throw Exception('Authentication error: ${e.message}');
|
||||
throw SignInFailedException(
|
||||
technicalMessage: 'Firebase auth error: ${e.message}',
|
||||
);
|
||||
}
|
||||
} on domain.AppException {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw Exception('Failed to sign in and fetch user data: ${e.toString()}');
|
||||
throw SignInFailedException(
|
||||
technicalMessage: 'Unexpected error: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,63 +82,225 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
firebase.User? firebaseUser;
|
||||
String? createdBusinessId;
|
||||
|
||||
try {
|
||||
// Step 1: Try to create Firebase Auth user
|
||||
final firebase.UserCredential credential = await _firebaseAuth.createUserWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
firebaseUser = credential.user;
|
||||
if (firebaseUser == null) {
|
||||
throw const SignUpFailedException(
|
||||
technicalMessage: 'Firebase user could not be created',
|
||||
);
|
||||
}
|
||||
|
||||
// New user created successfully, proceed to create PostgreSQL entities
|
||||
return await _createBusinessAndUser(
|
||||
firebaseUser: firebaseUser,
|
||||
companyName: companyName,
|
||||
email: email,
|
||||
onBusinessCreated: (String businessId) => createdBusinessId = businessId,
|
||||
);
|
||||
|
||||
} on firebase.FirebaseAuthException catch (e) {
|
||||
if (e.code == 'weak-password') {
|
||||
throw WeakPasswordException(
|
||||
technicalMessage: 'Firebase: ${e.message}',
|
||||
);
|
||||
} else if (e.code == 'email-already-in-use') {
|
||||
// Email exists in Firebase Auth - try to sign in and complete registration
|
||||
return await _handleExistingFirebaseAccount(
|
||||
email: email,
|
||||
password: password,
|
||||
companyName: companyName,
|
||||
);
|
||||
} else {
|
||||
throw SignUpFailedException(
|
||||
technicalMessage: 'Firebase auth error: ${e.message}',
|
||||
);
|
||||
}
|
||||
} on domain.AppException {
|
||||
// Rollback for our known exceptions
|
||||
await _rollbackSignUp(firebaseUser: firebaseUser, businessId: createdBusinessId);
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
// Rollback: Clean up any partially created resources
|
||||
await _rollbackSignUp(firebaseUser: firebaseUser, businessId: createdBusinessId);
|
||||
throw SignUpFailedException(
|
||||
technicalMessage: 'Unexpected error: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the case where email already exists in Firebase Auth.
|
||||
///
|
||||
/// This can happen when:
|
||||
/// 1. User signed up with Google in another app sharing the same Firebase project
|
||||
/// 2. User already has a KROW account
|
||||
///
|
||||
/// The flow:
|
||||
/// 1. Try to sign in with provided password
|
||||
/// 2. If sign-in succeeds, check if BUSINESS user exists in PostgreSQL
|
||||
/// 3. If not, create Business + User (user is new to KROW)
|
||||
/// 4. If yes, they already have a KROW account
|
||||
Future<domain.User> _handleExistingFirebaseAccount({
|
||||
required String email,
|
||||
required String password,
|
||||
required String companyName,
|
||||
}) async {
|
||||
developer.log('Email exists in Firebase, attempting sign-in: $email', name: 'AuthRepository');
|
||||
|
||||
try {
|
||||
// Try to sign in with the provided password
|
||||
final firebase.UserCredential credential = await _firebaseAuth.signInWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
final firebase.User? firebaseUser = credential.user;
|
||||
if (firebaseUser == null) {
|
||||
throw Exception('Sign-up failed, Firebase user could not be created.');
|
||||
throw const SignUpFailedException(
|
||||
technicalMessage: 'Sign-in succeeded but no user returned',
|
||||
);
|
||||
}
|
||||
|
||||
// Client-specific business logic:
|
||||
// 1. Create a `Business` entity.
|
||||
// 2. Create a `User` entity associated with the business.
|
||||
final OperationResult<dc.CreateBusinessData, dc.CreateBusinessVariables> createBusinessResponse = await _dataConnect.createBusiness(
|
||||
businessName: companyName,
|
||||
userId: firebaseUser.uid,
|
||||
rateGroup: dc.BusinessRateGroup.STANDARD,
|
||||
status: dc.BusinessStatus.PENDING,
|
||||
).execute();
|
||||
// Sign-in succeeded! Check if user already has a BUSINESS account in PostgreSQL
|
||||
final bool hasBusinessAccount = await _checkBusinessUserExists(firebaseUser.uid);
|
||||
|
||||
final dc.CreateBusinessBusinessInsert? businessData = createBusinessResponse.data?.business_insert;
|
||||
if (businessData == null) {
|
||||
await firebaseUser.delete(); // Rollback if business creation fails
|
||||
throw Exception('Business creation failed after Firebase user registration.');
|
||||
if (hasBusinessAccount) {
|
||||
// User already has a KROW Client account
|
||||
developer.log('User already has BUSINESS account: ${firebaseUser.uid}', name: 'AuthRepository');
|
||||
throw AccountExistsException(
|
||||
technicalMessage: 'User ${firebaseUser.uid} already has BUSINESS role',
|
||||
);
|
||||
}
|
||||
|
||||
final OperationResult<dc.CreateUserData, dc.CreateUserVariables> createUserResponse = await _dataConnect.createUser(
|
||||
id: firebaseUser.uid,
|
||||
role: dc.UserBaseRole.USER,
|
||||
)
|
||||
.email(email)
|
||||
.userRole('BUSINESS')
|
||||
.execute();
|
||||
|
||||
final dc.CreateUserUserInsert? newUserData = createUserResponse.data?.user_insert;
|
||||
if (newUserData == null) {
|
||||
await firebaseUser.delete(); // Rollback if user profile creation fails
|
||||
// TO-DO: Also delete the created Business if this fails
|
||||
throw Exception('User profile creation failed after Firebase user registration.');
|
||||
}
|
||||
|
||||
return _getUserProfile(
|
||||
firebaseUserId: firebaseUser.uid,
|
||||
fallbackEmail: firebaseUser.email ?? email,
|
||||
// User exists in Firebase but not in KROW PostgreSQL - create the entities
|
||||
developer.log('Creating BUSINESS account for existing Firebase user: ${firebaseUser.uid}', name: 'AuthRepository');
|
||||
return await _createBusinessAndUser(
|
||||
firebaseUser: firebaseUser,
|
||||
companyName: companyName,
|
||||
email: email,
|
||||
onBusinessCreated: (_) {}, // No rollback needed for existing Firebase user
|
||||
);
|
||||
|
||||
} on firebase.FirebaseAuthException catch (e) {
|
||||
if (e.code == 'weak-password') {
|
||||
throw Exception('The password provided is too weak.');
|
||||
} else if (e.code == 'email-already-in-use') {
|
||||
throw Exception('An account already exists for that email address.');
|
||||
// Sign-in failed - check why
|
||||
developer.log('Sign-in failed with code: ${e.code}', name: 'AuthRepository');
|
||||
|
||||
if (e.code == 'wrong-password' || e.code == 'invalid-credential') {
|
||||
// Password doesn't match - check what providers are available
|
||||
return await _handlePasswordMismatch(email);
|
||||
} else {
|
||||
throw Exception('Sign-up error: ${e.message}');
|
||||
throw SignUpFailedException(
|
||||
technicalMessage: 'Firebase sign-in error: ${e.message}',
|
||||
);
|
||||
}
|
||||
} on domain.AppException {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the case where the password doesn't match the existing account.
|
||||
///
|
||||
/// Note: fetchSignInMethodsForEmail was deprecated by Firebase for security
|
||||
/// reasons (email enumeration). We show a combined message that covers both
|
||||
/// cases: wrong password OR account uses different sign-in method (Google).
|
||||
Future<Never> _handlePasswordMismatch(String email) async {
|
||||
// We can't distinguish between "wrong password" and "no password provider"
|
||||
// due to Firebase deprecating fetchSignInMethodsForEmail.
|
||||
// The PasswordMismatchException message covers both scenarios.
|
||||
developer.log('Password mismatch or different provider for: $email', name: 'AuthRepository');
|
||||
throw PasswordMismatchException(
|
||||
technicalMessage: 'Email $email: password mismatch or different auth provider',
|
||||
);
|
||||
}
|
||||
|
||||
/// Checks if a user with BUSINESS role exists in PostgreSQL.
|
||||
Future<bool> _checkBusinessUserExists(String firebaseUserId) async {
|
||||
try {
|
||||
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response =
|
||||
await _dataConnect.getUserById(id: firebaseUserId).execute();
|
||||
final dc.GetUserByIdUser? user = response.data?.user;
|
||||
return user != null && user.userRole == 'BUSINESS';
|
||||
} catch (e) {
|
||||
throw Exception('Failed to sign up and create user data: ${e.toString()}');
|
||||
developer.log('Error checking business user: $e', name: 'AuthRepository');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates Business and User entities in PostgreSQL for a Firebase user.
|
||||
Future<domain.User> _createBusinessAndUser({
|
||||
required firebase.User firebaseUser,
|
||||
required String companyName,
|
||||
required String email,
|
||||
required void Function(String businessId) onBusinessCreated,
|
||||
}) async {
|
||||
// Create Business entity in PostgreSQL
|
||||
final OperationResult<dc.CreateBusinessData, dc.CreateBusinessVariables> createBusinessResponse =
|
||||
await _dataConnect.createBusiness(
|
||||
businessName: companyName,
|
||||
userId: firebaseUser.uid,
|
||||
rateGroup: dc.BusinessRateGroup.STANDARD,
|
||||
status: dc.BusinessStatus.PENDING,
|
||||
).execute();
|
||||
|
||||
final dc.CreateBusinessBusinessInsert? businessData = createBusinessResponse.data?.business_insert;
|
||||
if (businessData == null) {
|
||||
throw const SignUpFailedException(
|
||||
technicalMessage: 'Business creation failed in PostgreSQL',
|
||||
);
|
||||
}
|
||||
onBusinessCreated(businessData.id);
|
||||
|
||||
// Create User entity in PostgreSQL
|
||||
final OperationResult<dc.CreateUserData, dc.CreateUserVariables> createUserResponse =
|
||||
await _dataConnect.createUser(
|
||||
id: firebaseUser.uid,
|
||||
role: dc.UserBaseRole.USER,
|
||||
)
|
||||
.email(email)
|
||||
.userRole('BUSINESS')
|
||||
.execute();
|
||||
|
||||
final dc.CreateUserUserInsert? newUserData = createUserResponse.data?.user_insert;
|
||||
if (newUserData == null) {
|
||||
throw const SignUpFailedException(
|
||||
technicalMessage: 'User profile creation failed in PostgreSQL',
|
||||
);
|
||||
}
|
||||
|
||||
return _getUserProfile(
|
||||
firebaseUserId: firebaseUser.uid,
|
||||
fallbackEmail: firebaseUser.email ?? email,
|
||||
);
|
||||
}
|
||||
|
||||
/// Rollback helper to clean up partially created resources during sign-up.
|
||||
Future<void> _rollbackSignUp({
|
||||
firebase.User? firebaseUser,
|
||||
String? businessId,
|
||||
}) async {
|
||||
// Delete business first (if created)
|
||||
if (businessId != null) {
|
||||
try {
|
||||
await _dataConnect.deleteBusiness(id: businessId).execute();
|
||||
} catch (_) {
|
||||
// Log but don't throw - we're already in error recovery
|
||||
}
|
||||
}
|
||||
// Delete Firebase user (if created)
|
||||
if (firebaseUser != null) {
|
||||
try {
|
||||
await firebaseUser.delete();
|
||||
} catch (_) {
|
||||
// Log but don't throw - we're already in error recovery
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,17 +327,23 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response = await _dataConnect.getUserById(id: firebaseUserId).execute();
|
||||
final dc.GetUserByIdUser? user = response.data?.user;
|
||||
if (user == null) {
|
||||
throw Exception('Authenticated user profile not found in database.');
|
||||
throw UserNotFoundException(
|
||||
technicalMessage: 'Firebase UID $firebaseUserId not found in users table',
|
||||
);
|
||||
}
|
||||
if (requireBusinessRole && user.userRole != 'BUSINESS') {
|
||||
await _firebaseAuth.signOut();
|
||||
dc.ClientSessionStore.instance.clear();
|
||||
throw Exception('User is not authorized for this app.');
|
||||
throw UnauthorizedAppException(
|
||||
technicalMessage: 'User role is ${user.userRole}, expected BUSINESS',
|
||||
);
|
||||
}
|
||||
|
||||
final String? email = user.email ?? fallbackEmail;
|
||||
if (email == null || email.isEmpty) {
|
||||
throw Exception('User email is missing in profile data.');
|
||||
throw UserNotFoundException(
|
||||
technicalMessage: 'User email missing for UID $firebaseUserId',
|
||||
);
|
||||
}
|
||||
|
||||
final domain.User domainUser = domain.User(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
@@ -56,11 +58,20 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState> {
|
||||
SignInWithEmailArguments(email: event.email, password: event.password),
|
||||
);
|
||||
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
|
||||
} catch (e) {
|
||||
} on AppException catch (e) {
|
||||
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc');
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ClientAuthStatus.error,
|
||||
errorMessage: e.toString(),
|
||||
errorMessage: e.messageKey,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
developer.log('Unexpected error: $e', name: 'ClientAuthBloc');
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ClientAuthStatus.error,
|
||||
errorMessage: 'errors.generic.unknown',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -81,11 +92,20 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState> {
|
||||
),
|
||||
);
|
||||
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
|
||||
} catch (e) {
|
||||
} on AppException catch (e) {
|
||||
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc');
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ClientAuthStatus.error,
|
||||
errorMessage: e.toString(),
|
||||
errorMessage: e.messageKey,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
developer.log('Unexpected error: $e', name: 'ClientAuthBloc');
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ClientAuthStatus.error,
|
||||
errorMessage: 'errors.generic.unknown',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -102,11 +122,20 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState> {
|
||||
SignInWithSocialArguments(provider: event.provider),
|
||||
);
|
||||
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
|
||||
} catch (e) {
|
||||
} on AppException catch (e) {
|
||||
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc');
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ClientAuthStatus.error,
|
||||
errorMessage: e.toString(),
|
||||
errorMessage: e.messageKey,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
developer.log('Unexpected error: $e', name: 'ClientAuthBloc');
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ClientAuthStatus.error,
|
||||
errorMessage: 'errors.generic.unknown',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -121,11 +150,20 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState> {
|
||||
try {
|
||||
await _signOut();
|
||||
emit(state.copyWith(status: ClientAuthStatus.signedOut, user: null));
|
||||
} catch (e) {
|
||||
} on AppException catch (e) {
|
||||
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientAuthBloc');
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ClientAuthStatus.error,
|
||||
errorMessage: e.toString(),
|
||||
errorMessage: e.messageKey,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
developer.log('Unexpected error: $e', name: 'ClientAuthBloc');
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ClientAuthStatus.error,
|
||||
errorMessage: 'errors.generic.unknown',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -45,10 +45,11 @@ class ClientSignInPage extends StatelessWidget {
|
||||
if (state.status == ClientAuthStatus.authenticated) {
|
||||
Modular.to.navigateClientHome();
|
||||
} else if (state.status == ClientAuthStatus.error) {
|
||||
final String errorMessage = state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: t.errors.generic.unknown;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage ?? 'Authentication Error'),
|
||||
),
|
||||
SnackBar(content: Text(errorMessage)),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -49,10 +49,11 @@ class ClientSignUpPage extends StatelessWidget {
|
||||
if (state.status == ClientAuthStatus.authenticated) {
|
||||
Modular.to.navigateClientHome();
|
||||
} else if (state.status == ClientAuthStatus.error) {
|
||||
final String errorMessage = state.errorMessage != null
|
||||
? translateErrorKey(state.errorMessage!)
|
||||
: t.errors.generic.unknown;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.errorMessage ?? 'Authentication Error'),
|
||||
),
|
||||
SnackBar(content: Text(errorMessage)),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user