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:
@@ -1,4 +1,4 @@
|
||||
library client_authentication;
|
||||
library;
|
||||
|
||||
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user