Merge branch 'dev' into 312-feature-integrate-google-maps-places-autocomplete-for-hub-address-validation

This commit is contained in:
Achintha Isuru
2026-02-01 01:42:41 -05:00
74 changed files with 2639 additions and 321 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,
),
],
),
),
],