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:
bwnyasse
2026-01-31 18:56:48 -05:00
parent 9517606e7a
commit caac050ac9
30 changed files with 1396 additions and 105 deletions

View File

@@ -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(

View File

@@ -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',
),
);
}

View File

@@ -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)),
);
}
},

View File

@@ -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)),
);
}
},

View File

@@ -15,6 +15,9 @@ import 'presentation/pages/billing_page.dart';
class BillingModule extends Module {
@override
void binds(Injector i) {
// Mock repositories (TODO: Replace with real implementations)
i.addSingleton<FinancialRepositoryMock>(FinancialRepositoryMock.new);
// Repositories
i.addSingleton<BillingRepository>(
() => BillingRepositoryImpl(

View File

@@ -83,7 +83,7 @@ class _BillingViewState extends State<BillingView> {
leading: Center(
child: UiIconButton.secondary(
icon: UiIcons.arrowLeft,
onTap: () => Modular.to.pop(),
onTap: () => Modular.to.navigate('/client-main/home/'),
),
),
title: AnimatedSwitcher(

View File

@@ -68,7 +68,7 @@ class _CoveragePageState extends State<CoveragePage> {
expandedHeight: 300.0,
backgroundColor: UiColors.primary,
leading: IconButton(
onPressed: () => Modular.to.pop(),
onPressed: () => Modular.to.navigate('/client-main/home/'),
icon: Container(
padding: const EdgeInsets.all(UiConstants.space2),
decoration: BoxDecoration(

View File

@@ -67,7 +67,7 @@ class CoverageHeader extends StatelessWidget {
Row(
children: <Widget>[
GestureDetector(
onTap: () => Modular.to.pop(),
onTap: () => Modular.to.navigate('/client-main/home/'),
child: Container(
width: UiConstants.space10,
height: UiConstants.space10,

View File

@@ -18,7 +18,7 @@ class PermanentOrderPage extends StatelessWidget {
backgroundColor: UiColors.bgPrimary,
appBar: UiAppBar(
title: labels.title,
onLeadingPressed: () => Modular.to.pop(),
onLeadingPressed: () => Modular.to.navigate('/client/create-order/'),
),
body: Center(
child: Padding(

View File

@@ -18,7 +18,7 @@ class RecurringOrderPage extends StatelessWidget {
backgroundColor: UiColors.bgPrimary,
appBar: UiAppBar(
title: labels.title,
onLeadingPressed: () => Modular.to.pop(),
onLeadingPressed: () => Modular.to.navigate('/client/create-order/'),
),
body: Center(
child: Padding(

View File

@@ -43,7 +43,7 @@ class CreateOrderView extends StatelessWidget {
backgroundColor: UiColors.bgPrimary,
appBar: UiAppBar(
title: t.client_create_order.title,
onLeadingPressed: () => Modular.to.pop(),
onLeadingPressed: () => Modular.to.navigate('/client-main/home/'),
),
body: SafeArea(
child: Padding(

View File

@@ -50,7 +50,7 @@ class OneTimeOrderView extends StatelessWidget {
OneTimeOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
onBack: () => Modular.to.pop(),
onBack: () => Modular.to.navigate('/client/create-order/'),
),
Expanded(
child: Center(
@@ -89,7 +89,7 @@ class OneTimeOrderView extends StatelessWidget {
OneTimeOrderHeader(
title: labels.title,
subtitle: labels.subtitle,
onBack: () => Modular.to.pop(),
onBack: () => Modular.to.navigate('/client/create-order/'),
),
Expanded(
child: Stack(

View File

@@ -28,7 +28,7 @@ class RapidOrderView extends StatelessWidget {
title: labels.success_title,
message: labels.success_message,
buttonLabel: labels.back_to_orders,
onDone: () => Modular.to.pop(),
onDone: () => Modular.to.navigate('/client-main/orders/'),
);
}
@@ -82,7 +82,7 @@ class _RapidOrderFormState extends State<_RapidOrderForm> {
subtitle: labels.subtitle,
date: dateStr,
time: timeStr,
onBack: () => Modular.to.pop(),
onBack: () => Modular.to.navigate('/client/create-order/'),
),
// Content

View File

@@ -5,6 +5,12 @@ import 'package:firebase_data_connect/firebase_data_connect.dart';
import 'package:http/http.dart' as http;
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
HubHasOrdersException,
HubCreationFailedException,
BusinessNotFoundException,
NotAuthenticatedException;
import '../../domain/repositories/hub_repository_interface.dart';
import '../../util/hubs_constants.dart';
@@ -67,7 +73,9 @@ class HubRepositoryImpl implements HubRepositoryInterface {
.execute();
final String? createdId = result.data?.teamHub_insert.id;
if (createdId == null) {
throw Exception('Hub creation failed.');
throw HubCreationFailedException(
technicalMessage: 'teamHub_insert returned null for hub: $name',
);
}
final List<domain.Hub> hubs = await _fetchHubsForTeam(
@@ -97,7 +105,9 @@ class HubRepositoryImpl implements HubRepositoryInterface {
final String? businessId = dc.ClientSessionStore.instance.session?.business?.id;
if (businessId == null || businessId.isEmpty) {
await _firebaseAuth.signOut();
throw Exception('Business is missing. Please sign in again.');
throw const BusinessNotFoundException(
technicalMessage: 'Business ID missing from session',
);
}
final QueryResult<
@@ -110,7 +120,9 @@ class HubRepositoryImpl implements HubRepositoryInterface {
.execute();
if (result.data.orders.isNotEmpty) {
throw Exception("Sorry this hub has orders, it can't be deleted.");
throw HubHasOrdersException(
technicalMessage: 'Hub $id has ${result.data.orders.length} orders',
);
}
await _dataConnect.deleteTeamHub(id: id).execute();
@@ -151,7 +163,9 @@ class HubRepositoryImpl implements HubRepositoryInterface {
final firebase.User? user = _firebaseAuth.currentUser;
if (user == null) {
throw Exception('User is not authenticated.');
throw const NotAuthenticatedException(
technicalMessage: 'No Firebase user in currentUser',
);
}
final QueryResult<dc.GetBusinessesByUserIdData, dc.GetBusinessesByUserIdVariables> result = await _dataConnect.getBusinessesByUserId(
@@ -159,7 +173,9 @@ class HubRepositoryImpl implements HubRepositoryInterface {
).execute();
if (result.data.businesses.isEmpty) {
await _firebaseAuth.signOut();
throw Exception('No business found for this user. Please sign in again.');
throw BusinessNotFoundException(
technicalMessage: 'No business found for user ${user.uid}',
);
}
final dc.GetBusinessesByUserIdBusinesses business = result.data.businesses.first;
@@ -206,7 +222,9 @@ class HubRepositoryImpl implements HubRepositoryInterface {
final OperationResult<dc.CreateTeamData, dc.CreateTeamVariables> createTeamResult = await createTeamBuilder.execute();
final String? teamId = createTeamResult.data?.team_insert.id;
if (teamId == null) {
throw Exception('Team creation failed.');
throw HubCreationFailedException(
technicalMessage: 'Team creation failed for business ${business.id}',
);
}
return teamId;

View File

@@ -1,3 +1,5 @@
import 'dart:developer' as developer;
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_domain/krow_domain.dart';
@@ -67,11 +69,20 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
try {
final List<Hub> hubs = await _getHubsUseCase();
emit(state.copyWith(status: ClientHubsStatus.success, hubs: hubs));
} catch (e) {
} on AppException catch (e) {
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc');
emit(
state.copyWith(
status: ClientHubsStatus.failure,
errorMessage: e.toString(),
errorMessage: e.messageKey,
),
);
} catch (e) {
developer.log('Unexpected error: $e', name: 'ClientHubsBloc');
emit(
state.copyWith(
status: ClientHubsStatus.failure,
errorMessage: 'errors.generic.unknown',
),
);
}
@@ -106,11 +117,20 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
showAddHubDialog: false,
),
);
} catch (e) {
} on AppException catch (e) {
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc');
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: e.toString(),
errorMessage: e.messageKey,
),
);
} catch (e) {
developer.log('Unexpected error: $e', name: 'ClientHubsBloc');
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: 'errors.generic.unknown',
),
);
}
@@ -131,11 +151,20 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
successMessage: 'Hub deleted successfully',
),
);
} catch (e) {
} on AppException catch (e) {
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc');
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: e.toString(),
errorMessage: e.messageKey,
),
);
} catch (e) {
developer.log('Unexpected error: $e', name: 'ClientHubsBloc');
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: 'errors.generic.unknown',
),
);
}
@@ -159,11 +188,20 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
clearHubToIdentify: true,
),
);
} catch (e) {
} on AppException catch (e) {
developer.log('Error ${e.code}: ${e.technicalMessage}', name: 'ClientHubsBloc');
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: e.toString(),
errorMessage: e.messageKey,
),
);
} catch (e) {
developer.log('Unexpected error: $e', name: 'ClientHubsBloc');
emit(
state.copyWith(
status: ClientHubsStatus.actionFailure,
errorMessage: 'errors.generic.unknown',
),
);
}
@@ -175,8 +213,8 @@ class ClientHubsBloc extends Bloc<ClientHubsEvent, ClientHubsState>
) {
emit(
state.copyWith(
errorMessage: null,
successMessage: null,
clearErrorMessage: true,
clearSuccessMessage: true,
status:
state.status == ClientHubsStatus.actionSuccess ||
state.status == ClientHubsStatus.actionFailure

View File

@@ -43,12 +43,18 @@ class ClientHubsState extends Equatable {
bool? showAddHubDialog,
Hub? hubToIdentify,
bool clearHubToIdentify = false,
bool clearErrorMessage = false,
bool clearSuccessMessage = false,
}) {
return ClientHubsState(
status: status ?? this.status,
hubs: hubs ?? this.hubs,
errorMessage: errorMessage ?? this.errorMessage,
successMessage: successMessage ?? this.successMessage,
errorMessage: clearErrorMessage
? null
: (errorMessage ?? this.errorMessage),
successMessage: clearSuccessMessage
? null
: (successMessage ?? this.successMessage),
showAddHubDialog: showAddHubDialog ?? this.showAddHubDialog,
hubToIdentify: clearHubToIdentify
? null

View File

@@ -33,9 +33,10 @@ class ClientHubsPage extends StatelessWidget {
},
listener: (BuildContext context, ClientHubsState state) {
if (state.errorMessage != null && state.errorMessage!.isNotEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(state.errorMessage!)));
final String errorMessage = translateErrorKey(state.errorMessage!);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(errorMessage)),
);
BlocProvider.of<ClientHubsBloc>(
context,
).add(const ClientHubsMessageCleared());
@@ -178,7 +179,7 @@ class ClientHubsPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
GestureDetector(
onTap: () => Modular.to.pop(),
onTap: () => Modular.to.navigate('/client-main/home/'),
child: Container(
width: 40,
height: 40,

View File

@@ -83,7 +83,7 @@ class SettingsActions extends StatelessWidget {
// Cancel button
UiButton.secondary(
text: t.common.cancel,
onPressed: () => Modular.to.pop(),
onPressed: () => Navigator.of(dialogContext).pop(),
),
],
),

View File

@@ -30,7 +30,7 @@ class SettingsProfileHeader extends StatelessWidget {
shape: const Border(bottom: BorderSide(color: UiColors.border, width: 1)),
leading: IconButton(
icon: const Icon(UiIcons.chevronLeft, color: UiColors.textSecondary),
onPressed: () => Modular.to.pop(),
onPressed: () => Modular.to.navigate('/client-main/home/'),
),
flexibleSpace: FlexibleSpaceBar(
background: Container(

View File

@@ -202,21 +202,38 @@ class _ViewOrderCardState extends State<ViewOrderCard> {
],
),
const SizedBox(height: UiConstants.space2),
// Address
// Location (Hub name + Address)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Icon(
UiIcons.mapPin,
size: 14,
color: UiColors.iconSecondary,
const Padding(
padding: EdgeInsets.only(top: 2),
child: Icon(
UiIcons.mapPin,
size: 14,
color: UiColors.iconSecondary,
),
),
const SizedBox(width: 4),
Expanded(
child: Text(
order.locationAddress,
style: UiTypography.footnote2r.textSecondary,
maxLines: 1,
overflow: TextOverflow.ellipsis,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (order.location.isNotEmpty)
Text(
order.location,
style: UiTypography.footnote1b.textPrimary,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
if (order.locationAddress.isNotEmpty)
Text(
order.locationAddress,
style: UiTypography.footnote2r.textSecondary,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
],