Merge pull request #384 from Oloodi/feature/centralized-data-error-handling

refactor: centralize data connect error handling and stabilize mobile applications
This commit is contained in:
Achintha Isuru
2026-02-06 10:05:21 -05:00
committed by GitHub
150 changed files with 1506 additions and 2547 deletions

View File

@@ -1,4 +1,4 @@
library client_authentication;
library;
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:flutter_modular/flutter_modular.dart';

View File

@@ -21,9 +21,9 @@ import '../../domain/repositories/auth_repository_interface.dart';
///
/// This implementation integrates with Firebase Authentication for user
/// identity management and Krow's Data Connect SDK for storing user profile data.
class AuthRepositoryImpl implements AuthRepositoryInterface {
final firebase.FirebaseAuth _firebaseAuth;
final dc.ExampleConnector _dataConnect;
class AuthRepositoryImpl
with dc.DataErrorHandler
implements AuthRepositoryInterface {
/// Creates an [AuthRepositoryImpl] with the real dependencies.
AuthRepositoryImpl({
@@ -31,6 +31,8 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
required dc.ExampleConnector dataConnect,
}) : _firebaseAuth = firebaseAuth,
_dataConnect = dataConnect;
final firebase.FirebaseAuth _firebaseAuth;
final dc.ExampleConnector _dataConnect;
@override
Future<domain.User> signInWithEmail({
@@ -221,16 +223,12 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
}
/// 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) {
developer.log('Error checking business user: $e', name: 'AuthRepository');
return false;
}
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response =
await executeProtected(() => _dataConnect.getUserById(id: firebaseUserId).execute());
final dc.GetUserByIdUser? user = response.data.user;
return user != null && user.userRole == 'BUSINESS';
}
/// Creates Business and User entities in PostgreSQL for a Firebase user.
@@ -241,38 +239,30 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
required void Function(String businessId) onBusinessCreated,
}) async {
// Create Business entity in PostgreSQL
final OperationResult<dc.CreateBusinessData, dc.CreateBusinessVariables> createBusinessResponse =
await _dataConnect.createBusiness(
await executeProtected(() => _dataConnect.createBusiness(
businessName: companyName,
userId: firebaseUser.uid,
rateGroup: dc.BusinessRateGroup.STANDARD,
status: dc.BusinessStatus.PENDING,
).execute();
).execute());
final dc.CreateBusinessBusinessInsert? businessData = createBusinessResponse.data?.business_insert;
if (businessData == null) {
throw const SignUpFailedException(
technicalMessage: 'Business creation failed in PostgreSQL',
);
}
final dc.CreateBusinessBusinessInsert businessData = createBusinessResponse.data.business_insert;
onBusinessCreated(businessData.id);
// Create User entity in PostgreSQL
final OperationResult<dc.CreateUserData, dc.CreateUserVariables> createUserResponse =
await _dataConnect.createUser(
await executeProtected(() => _dataConnect.createUser(
id: firebaseUser.uid,
role: dc.UserBaseRole.USER,
)
.email(email)
.userRole('BUSINESS')
.execute();
.execute());
final dc.CreateUserUserInsert? newUserData = createUserResponse.data?.user_insert;
if (newUserData == null) {
throw const SignUpFailedException(
technicalMessage: 'User profile creation failed in PostgreSQL',
);
}
final dc.CreateUserUserInsert newUserData = createUserResponse.data.user_insert;
return _getUserProfile(
firebaseUserId: firebaseUser.uid,
@@ -323,8 +313,9 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
required String? fallbackEmail,
bool requireBusinessRole = false,
}) async {
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response = await _dataConnect.getUserById(id: firebaseUserId).execute();
final dc.GetUserByIdUser? user = response.data?.user;
final QueryResult<dc.GetUserByIdData, dc.GetUserByIdVariables> response =
await executeProtected(() => _dataConnect.getUserById(id: firebaseUserId).execute());
final dc.GetUserByIdUser? user = response.data.user;
if (user == null) {
throw UserNotFoundException(
technicalMessage: 'Firebase UID $firebaseUserId not found in users table',
@@ -351,9 +342,10 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
role: user.role.stringValue,
);
final QueryResult<dc.GetBusinessesByUserIdData, dc.GetBusinessesByUserIdVariables> businessResponse = await _dataConnect.getBusinessesByUserId(
final QueryResult<dc.GetBusinessesByUserIdData, dc.GetBusinessesByUserIdVariables> businessResponse =
await executeProtected(() => _dataConnect.getBusinessesByUserId(
userId: firebaseUserId,
).execute();
).execute());
final dc.GetBusinessesByUserIdBusinesses? business = businessResponse.data.businesses.isNotEmpty
? businessResponse.data.businesses.first
: null;

View File

@@ -2,14 +2,14 @@ import 'package:krow_core/core.dart';
/// Arguments for the [SignInWithEmailUseCase].
class SignInWithEmailArguments extends UseCaseArgument {
const SignInWithEmailArguments({required this.email, required this.password});
/// The user's email address.
final String email;
/// The user's password.
final String password;
const SignInWithEmailArguments({required this.email, required this.password});
@override
List<Object?> get props => <Object?>[email, password];
}

View File

@@ -2,10 +2,10 @@ import 'package:krow_core/core.dart';
/// Arguments for the [SignInWithSocialUseCase].
class SignInWithSocialArguments extends UseCaseArgument {
/// The social provider name (e.g. 'google' or 'apple').
final String provider;
const SignInWithSocialArguments({required this.provider});
/// The social provider name (e.g. 'google' or 'apple').
final String provider;
@override
List<Object?> get props => <Object?>[provider];

View File

@@ -2,6 +2,12 @@ import 'package:krow_core/core.dart';
/// Arguments for the [SignUpWithEmailUseCase].
class SignUpWithEmailArguments extends UseCaseArgument {
const SignUpWithEmailArguments({
required this.companyName,
required this.email,
required this.password,
});
/// The name of the company.
final String companyName;
@@ -11,12 +17,6 @@ class SignUpWithEmailArguments extends UseCaseArgument {
/// The user's password.
final String password;
const SignUpWithEmailArguments({
required this.companyName,
required this.email,
required this.password,
});
@override
List<Object?> get props => <Object?>[companyName, email, password];
}

View File

@@ -9,9 +9,9 @@ import '../repositories/auth_repository_interface.dart';
/// via email/password credentials.
class SignInWithEmailUseCase
implements UseCase<SignInWithEmailArguments, User> {
final AuthRepositoryInterface _repository;
const SignInWithEmailUseCase(this._repository);
final AuthRepositoryInterface _repository;
/// Executes the sign-in operation.
@override

View File

@@ -6,9 +6,9 @@ import '../repositories/auth_repository_interface.dart';
/// Use case for signing in a client via social providers (Google/Apple).
class SignInWithSocialUseCase
implements UseCase<SignInWithSocialArguments, User> {
final AuthRepositoryInterface _repository;
const SignInWithSocialUseCase(this._repository);
final AuthRepositoryInterface _repository;
/// Executes the social sign-in operation.
@override

View File

@@ -6,9 +6,9 @@ import '../repositories/auth_repository_interface.dart';
/// This use case handles the termination of the user's session and
/// clearing of any local authentication tokens.
class SignOutUseCase implements NoInputUseCase<void> {
final AuthRepositoryInterface _repository;
const SignOutUseCase(this._repository);
final AuthRepositoryInterface _repository;
/// Executes the sign-out operation.
@override

View File

@@ -9,9 +9,9 @@ import '../repositories/auth_repository_interface.dart';
/// email, password, and company details.
class SignUpWithEmailUseCase
implements UseCase<SignUpWithEmailArguments, User> {
final AuthRepositoryInterface _repository;
const SignUpWithEmailUseCase(this._repository);
final AuthRepositoryInterface _repository;
/// Executes the sign-up operation.
@override

View File

@@ -25,10 +25,6 @@ import 'client_auth_state.dart';
/// * Session Termination
class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
with BlocErrorHandler<ClientAuthState> {
final SignInWithEmailUseCase _signInWithEmail;
final SignUpWithEmailUseCase _signUpWithEmail;
final SignInWithSocialUseCase _signInWithSocial;
final SignOutUseCase _signOut;
/// Initializes the BLoC with the required use cases and initial state.
ClientAuthBloc({
@@ -46,6 +42,10 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
on<ClientSocialSignInRequested>(_onSocialSignInRequested);
on<ClientSignOutRequested>(_onSignOutRequested);
}
final SignInWithEmailUseCase _signInWithEmail;
final SignUpWithEmailUseCase _signUpWithEmail;
final SignInWithSocialUseCase _signInWithSocial;
final SignOutUseCase _signOut;
/// Handles the [ClientSignInRequested] event.
Future<void> _onSignInRequested(
@@ -57,12 +57,12 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
await handleError(
emit: emit,
action: () async {
final user = await _signInWithEmail(
final User user = await _signInWithEmail(
SignInWithEmailArguments(email: event.email, password: event.password),
);
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
},
onError: (errorKey) => state.copyWith(
onError: (String errorKey) => state.copyWith(
status: ClientAuthStatus.error,
errorMessage: errorKey,
),
@@ -79,7 +79,7 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
await handleError(
emit: emit,
action: () async {
final user = await _signUpWithEmail(
final User user = await _signUpWithEmail(
SignUpWithEmailArguments(
companyName: event.companyName,
email: event.email,
@@ -88,7 +88,7 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
);
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
},
onError: (errorKey) => state.copyWith(
onError: (String errorKey) => state.copyWith(
status: ClientAuthStatus.error,
errorMessage: errorKey,
),
@@ -105,12 +105,12 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
await handleError(
emit: emit,
action: () async {
final user = await _signInWithSocial(
final User user = await _signInWithSocial(
SignInWithSocialArguments(provider: event.provider),
);
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
},
onError: (errorKey) => state.copyWith(
onError: (String errorKey) => state.copyWith(
status: ClientAuthStatus.error,
errorMessage: errorKey,
),
@@ -130,7 +130,7 @@ class ClientAuthBloc extends Bloc<ClientAuthEvent, ClientAuthState>
await _signOut();
emit(state.copyWith(status: ClientAuthStatus.signedOut, user: null));
},
onError: (errorKey) => state.copyWith(
onError: (String errorKey) => state.copyWith(
status: ClientAuthStatus.error,
errorMessage: errorKey,
),

View File

@@ -10,10 +10,10 @@ abstract class ClientAuthEvent extends Equatable {
/// Event dispatched when a user attempts to sign in with email and password.
class ClientSignInRequested extends ClientAuthEvent {
final String email;
final String password;
const ClientSignInRequested({required this.email, required this.password});
final String email;
final String password;
@override
List<Object?> get props => <Object?>[email, password];
@@ -21,15 +21,15 @@ class ClientSignInRequested extends ClientAuthEvent {
/// Event dispatched when a user attempts to create a new business account.
class ClientSignUpRequested extends ClientAuthEvent {
final String companyName;
final String email;
final String password;
const ClientSignUpRequested({
required this.companyName,
required this.email,
required this.password,
});
final String companyName;
final String email;
final String password;
@override
List<Object?> get props => <Object?>[companyName, email, password];
@@ -37,9 +37,9 @@ class ClientSignUpRequested extends ClientAuthEvent {
/// Event dispatched for third-party authentication (Google/Apple).
class ClientSocialSignInRequested extends ClientAuthEvent {
final String provider;
const ClientSocialSignInRequested({required this.provider});
final String provider;
@override
List<Object?> get props => <Object?>[provider];

View File

@@ -21,6 +21,12 @@ enum ClientAuthStatus {
/// Represents the state of the client authentication flow.
class ClientAuthState extends Equatable {
const ClientAuthState({
this.status = ClientAuthStatus.initial,
this.user,
this.errorMessage,
});
/// Current status of the authentication process.
final ClientAuthStatus status;
@@ -30,12 +36,6 @@ class ClientAuthState extends Equatable {
/// Optional error message when status is [ClientAuthStatus.error].
final String? errorMessage;
const ClientAuthState({
this.status = ClientAuthStatus.initial,
this.user,
this.errorMessage,
});
/// Creates a copy of this state with the given fields replaced by the new values.
ClientAuthState copyWith({
ClientAuthStatus? status,

View File

@@ -11,7 +11,6 @@ import '../blocs/client_auth_event.dart';
import '../blocs/client_auth_state.dart';
import '../widgets/client_sign_up_page/client_sign_up_form.dart';
import '../widgets/common/auth_divider.dart';
import '../widgets/common/auth_social_button.dart';
/// Page for client users to sign up for a new account.
///

View File

@@ -7,12 +7,6 @@ import 'package:flutter/material.dart';
/// This widget handles user input for email and password and delegates
/// authentication events to the parent via callbacks.
class ClientSignInForm extends StatefulWidget {
/// Callback when the sign-in button is pressed.
final void Function({required String email, required String password})
onSignIn;
/// Whether the authentication is currently loading.
final bool isLoading;
/// Creates a [ClientSignInForm].
const ClientSignInForm({
@@ -20,6 +14,12 @@ class ClientSignInForm extends StatefulWidget {
required this.onSignIn,
this.isLoading = false,
});
/// Callback when the sign-in button is pressed.
final void Function({required String email, required String password})
onSignIn;
/// Whether the authentication is currently loading.
final bool isLoading;
@override
State<ClientSignInForm> createState() => _ClientSignInFormState();

View File

@@ -7,6 +7,13 @@ import 'package:flutter/material.dart';
/// This widget handles user input for company name, email, and password,
/// and delegates registration events to the parent via callbacks.
class ClientSignUpForm extends StatefulWidget {
/// Creates a [ClientSignUpForm].
const ClientSignUpForm({
super.key,
required this.onSignUp,
this.isLoading = false,
});
/// Callback when the sign-up button is pressed.
final void Function({
required String companyName,
@@ -18,13 +25,6 @@ class ClientSignUpForm extends StatefulWidget {
/// Whether the authentication is currently loading.
final bool isLoading;
/// Creates a [ClientSignUpForm].
const ClientSignUpForm({
super.key,
required this.onSignUp,
this.isLoading = false,
});
@override
State<ClientSignUpForm> createState() => _ClientSignUpFormState();
}

View File

@@ -6,11 +6,11 @@ import 'package:flutter/material.dart';
///
/// Displays a horizontal line with text in the middle (e.g., "Or continue with").
class AuthDivider extends StatelessWidget {
/// The text to display in the center of the divider.
final String text;
/// Creates an [AuthDivider].
const AuthDivider({super.key, required this.text});
/// The text to display in the center of the divider.
final String text;
@override
Widget build(BuildContext context) {

View File

@@ -6,14 +6,6 @@ import 'package:flutter/material.dart';
/// This widget wraps [UiButton.secondary] to provide a consistent look and feel
/// for social sign-in/sign-up buttons (e.g., Google, Apple).
class AuthSocialButton extends StatelessWidget {
/// The localizable text to display on the button (e.g., "Continue with Google").
final String text;
/// The icon representing the social provider.
final IconData icon;
/// Callback to execute when the button is tapped.
final VoidCallback onPressed;
/// Creates an [AuthSocialButton].
///
@@ -24,6 +16,14 @@ class AuthSocialButton extends StatelessWidget {
required this.icon,
required this.onPressed,
});
/// The localizable text to display on the button (e.g., "Continue with Google").
final String text;
/// The icon representing the social provider.
final IconData icon;
/// Callback to execute when the button is tapped.
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {

View File

@@ -3,14 +3,14 @@ import 'package:flutter/material.dart';
/// A widget that displays a section title with a leading icon.
class SectionTitle extends StatelessWidget {
const SectionTitle({super.key, required this.title, required this.subtitle});
/// The title of the section.
final String title;
/// The subtitle of the section.
final String subtitle;
const SectionTitle({super.key, required this.title, required this.subtitle});
@override
Widget build(BuildContext context) {
return Column(