Merge branch 'dev' into 312-feature-integrate-google-maps-places-autocomplete-for-hub-address-validation
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)),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user