Move apps to mobile directory structure

Relocated all app directories (client, design_system_viewer, staff) and their contents under the new 'apps/mobile' path. This change improves project organization and prepares for future platform-specific structuring.
This commit is contained in:
Achintha Isuru
2026-01-22 10:17:19 -05:00
parent 2f992ae5fa
commit cf59935ec8
982 changed files with 3 additions and 2532 deletions

View File

@@ -0,0 +1,156 @@
import 'package:firebase_auth/firebase_auth.dart' as firebase;
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart' as domain;
import '../../domain/repositories/auth_repository_interface.dart';
/// Production-ready implementation of the [AuthRepositoryInterface] for the client app.
///
/// 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;
/// Creates an [AuthRepositoryImpl] with the real dependencies.
AuthRepositoryImpl({
required firebase.FirebaseAuth firebaseAuth,
required dc.ExampleConnector dataConnect,
}) : _firebaseAuth = firebaseAuth,
_dataConnect = dataConnect;
@override
Future<domain.User> signInWithEmail({
required String email,
required String password,
}) async {
try {
final credential = await _firebaseAuth.signInWithEmailAndPassword(
email: email,
password: password,
);
final firebaseUser = credential.user;
if (firebaseUser == null) {
throw Exception('Sign-in failed, no Firebase user received.');
}
return _getUserProfile(
firebaseUserId: firebaseUser.uid,
fallbackEmail: firebaseUser.email ?? email,
);
//TO-DO: validate that user is business role and has business account
} on firebase.FirebaseAuthException catch (e) {
if (e.code == 'invalid-credential' || e.code == 'wrong-password') {
throw Exception('Incorrect email or password.');
} else {
throw Exception('Authentication error: ${e.message}');
}
} catch (e) {
throw Exception('Failed to sign in and fetch user data: ${e.toString()}');
}
}
@override
Future<domain.User> signUpWithEmail({
required String companyName,
required String email,
required String password,
}) async {
try {
final credential = await _firebaseAuth.createUserWithEmailAndPassword(
email: email,
password: password,
);
final firebaseUser = credential.user;
if (firebaseUser == null) {
throw Exception('Sign-up failed, Firebase user could not be created.');
}
// Client-specific business logic:
// 1. Create a `Business` entity.
// 2. Create a `User` entity associated with the business.
final createBusinessResponse = await _dataConnect.createBusiness(
businessName: companyName,
userId: firebaseUser.uid,
rateGroup: dc.BusinessRateGroup.STANDARD,
status: dc.BusinessStatus.PENDING,
).execute();
final 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.');
}
final createUserResponse = await _dataConnect.createUser(
id: firebaseUser.uid,
role: dc.UserBaseRole.USER,
)
.email(email)
.userRole('BUSINESS')
.execute();
final 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,
);
} 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.');
} else {
throw Exception('Sign-up error: ${e.message}');
}
} catch (e) {
throw Exception('Failed to sign up and create user data: ${e.toString()}');
}
}
@override
Future<void> signOut() async {
try {
await _firebaseAuth.signOut();
} catch (e) {
throw Exception('Error signing out: ${e.toString()}');
}
}
@override
Future<domain.User> signInWithSocial({required String provider}) {
throw UnimplementedError('Social authentication with $provider is not yet implemented.');
}
Future<domain.User> _getUserProfile({
required String firebaseUserId,
required String? fallbackEmail,
}) async {
final response = await _dataConnect.getUserById(id: firebaseUserId).execute();
final user = response.data?.user;
if (user == null) {
throw Exception('Authenticated user profile not found in database.');
}
final email = user.email ?? fallbackEmail;
if (email == null || email.isEmpty) {
throw Exception('User email is missing in profile data.');
}
return domain.User(
id: user.id,
email: email,
role: user.role.stringValue,
);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:krow_core/core.dart';
/// Arguments for the [SignInWithEmailUseCase].
class SignInWithEmailArguments extends UseCaseArgument {
/// 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 => [email, password];
}

View File

@@ -0,0 +1,12 @@
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});
@override
List<Object?> get props => [provider];
}

View File

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

View File

@@ -0,0 +1,37 @@
import 'package:krow_domain/krow_domain.dart';
/// Interface for the Client Authentication Repository.
///
/// This abstraction defines the core authentication operations required for
/// the client application, allowing the presentation layer to work with
/// different data sources (e.g., Supabase, Firebase, or Mock) without
/// depending on specific implementations.
abstract class AuthRepositoryInterface {
/// Signs in an existing client user using their email and password.
///
/// Returns a [User] object upon successful authentication.
/// Throws an exception if authentication fails.
Future<User> signInWithEmail({
required String email,
required String password,
});
/// Registers a new client user with their business details.
///
/// Takes [companyName], [email], and [password] to create a new account.
/// Returns the newly created [User].
Future<User> signUpWithEmail({
required String companyName,
required String email,
required String password,
});
/// Authenticates using an OAuth provider.
///
/// [provider] can be 'google' or 'apple'.
/// Returns a [User] upon successful social login.
Future<User> signInWithSocial({required String provider});
/// Terminates the current user session and clears authentication tokens.
Future<void> signOut();
}

View File

@@ -0,0 +1,24 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../arguments/sign_in_with_email_arguments.dart';
import '../repositories/auth_repository_interface.dart';
/// Use case for signing in a client using email and password.
///
/// This use case encapsulates the logic for authenticating an existing user
/// via email/password credentials.
class SignInWithEmailUseCase
implements UseCase<SignInWithEmailArguments, User> {
final AuthRepositoryInterface _repository;
const SignInWithEmailUseCase(this._repository);
/// Executes the sign-in operation.
@override
Future<User> call(SignInWithEmailArguments params) {
return _repository.signInWithEmail(
email: params.email,
password: params.password,
);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../arguments/sign_in_with_social_arguments.dart';
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);
/// Executes the social sign-in operation.
@override
Future<User> call(SignInWithSocialArguments params) {
return _repository.signInWithSocial(provider: params.provider);
}
}

View File

@@ -0,0 +1,18 @@
import 'package:krow_core/core.dart';
import '../repositories/auth_repository_interface.dart';
/// Use case for signing out the current client user.
///
/// 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);
/// Executes the sign-out operation.
@override
Future<void> call() {
return _repository.signOut();
}
}

View File

@@ -0,0 +1,25 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../arguments/sign_up_with_email_arguments.dart';
import '../repositories/auth_repository_interface.dart';
/// Use case for registering a new client user.
///
/// This use case handles the creation of a new client account using
/// email, password, and company details.
class SignUpWithEmailUseCase
implements UseCase<SignUpWithEmailArguments, User> {
final AuthRepositoryInterface _repository;
const SignUpWithEmailUseCase(this._repository);
/// Executes the sign-up operation.
@override
Future<User> call(SignUpWithEmailArguments params) {
return _repository.signUpWithEmail(
companyName: params.companyName,
email: params.email,
password: params.password,
);
}
}

View File

@@ -0,0 +1,131 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/arguments/sign_in_with_email_arguments.dart';
import '../../domain/arguments/sign_in_with_social_arguments.dart';
import '../../domain/arguments/sign_up_with_email_arguments.dart';
import '../../domain/usecases/sign_in_with_email_use_case.dart';
import '../../domain/usecases/sign_up_with_email_use_case.dart';
import '../../domain/usecases/sign_in_with_social_use_case.dart';
import '../../domain/usecases/sign_out_use_case.dart';
import 'client_auth_event.dart';
import 'client_auth_state.dart';
/// Business Logic Component for Client Authentication.
///
/// This BLoC manages the state transitions for the authentication flow in
/// the client application. It handles user inputs (events), interacts with
/// domain use cases, and emits corresponding [ClientAuthState]s.
///
/// Use this BLoC to handle:
/// * Email/Password Sign In
/// * Business Account Registration
/// * Social Authentication
/// * Session Termination
class ClientAuthBloc extends Bloc<ClientAuthEvent, 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({
required SignInWithEmailUseCase signInWithEmail,
required SignUpWithEmailUseCase signUpWithEmail,
required SignInWithSocialUseCase signInWithSocial,
required SignOutUseCase signOut,
}) : _signInWithEmail = signInWithEmail,
_signUpWithEmail = signUpWithEmail,
_signInWithSocial = signInWithSocial,
_signOut = signOut,
super(const ClientAuthState()) {
on<ClientSignInRequested>(_onSignInRequested);
on<ClientSignUpRequested>(_onSignUpRequested);
on<ClientSocialSignInRequested>(_onSocialSignInRequested);
on<ClientSignOutRequested>(_onSignOutRequested);
}
/// Handles the [ClientSignInRequested] event.
Future<void> _onSignInRequested(
ClientSignInRequested event,
Emitter<ClientAuthState> emit,
) async {
emit(state.copyWith(status: ClientAuthStatus.loading));
try {
final user = await _signInWithEmail(
SignInWithEmailArguments(email: event.email, password: event.password),
);
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
} catch (e) {
emit(
state.copyWith(
status: ClientAuthStatus.error,
errorMessage: e.toString(),
),
);
}
}
/// Handles the [ClientSignUpRequested] event.
Future<void> _onSignUpRequested(
ClientSignUpRequested event,
Emitter<ClientAuthState> emit,
) async {
emit(state.copyWith(status: ClientAuthStatus.loading));
try {
final user = await _signUpWithEmail(
SignUpWithEmailArguments(
companyName: event.companyName,
email: event.email,
password: event.password,
),
);
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
} catch (e) {
emit(
state.copyWith(
status: ClientAuthStatus.error,
errorMessage: e.toString(),
),
);
}
}
/// Handles the [ClientSocialSignInRequested] event.
Future<void> _onSocialSignInRequested(
ClientSocialSignInRequested event,
Emitter<ClientAuthState> emit,
) async {
emit(state.copyWith(status: ClientAuthStatus.loading));
try {
final user = await _signInWithSocial(
SignInWithSocialArguments(provider: event.provider),
);
emit(state.copyWith(status: ClientAuthStatus.authenticated, user: user));
} catch (e) {
emit(
state.copyWith(
status: ClientAuthStatus.error,
errorMessage: e.toString(),
),
);
}
}
/// Handles the [ClientSignOutRequested] event.
Future<void> _onSignOutRequested(
ClientSignOutRequested event,
Emitter<ClientAuthState> emit,
) async {
emit(state.copyWith(status: ClientAuthStatus.loading));
try {
await _signOut();
emit(state.copyWith(status: ClientAuthStatus.signedOut, user: null));
} catch (e) {
emit(
state.copyWith(
status: ClientAuthStatus.error,
errorMessage: e.toString(),
),
);
}
}
}

View File

@@ -0,0 +1,51 @@
import 'package:equatable/equatable.dart';
/// Base class for all authentication events in the client feature.
abstract class ClientAuthEvent extends Equatable {
const ClientAuthEvent();
@override
List<Object?> get props => [];
}
/// 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});
@override
List<Object?> get props => [email, password];
}
/// 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,
});
@override
List<Object?> get props => [companyName, email, password];
}
/// Event dispatched for third-party authentication (Google/Apple).
class ClientSocialSignInRequested extends ClientAuthEvent {
final String provider;
const ClientSocialSignInRequested({required this.provider});
@override
List<Object?> get props => [provider];
}
/// Event dispatched when the user requests to terminate their session.
class ClientSignOutRequested extends ClientAuthEvent {
const ClientSignOutRequested();
}

View File

@@ -0,0 +1,54 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
/// Enum representing the various states of the authentication process.
enum ClientAuthStatus {
/// Initial state before any action is taken.
initial,
/// An authentication operation is in progress.
loading,
/// The user has successfully authenticated.
authenticated,
/// The user has successfully signed out.
signedOut,
/// An error occurred during authentication.
error,
}
/// Represents the state of the client authentication flow.
class ClientAuthState extends Equatable {
/// Current status of the authentication process.
final ClientAuthStatus status;
/// The authenticated user (if status is [ClientAuthStatus.authenticated]).
final User? user;
/// 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,
User? user,
String? errorMessage,
}) {
return ClientAuthState(
status: status ?? this.status,
user: user ?? this.user,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [status, user, errorMessage];
}

View File

@@ -0,0 +1,25 @@
import 'package:flutter_modular/flutter_modular.dart';
/// Typed navigation for the Client Authentication feature.
///
/// This extension on [IModularNavigator] provides named methods for
/// navigating between authentication pages, reducing magic strings and
/// improving maintainability.
extension ClientAuthNavigator on IModularNavigator {
/// Navigates to the sign in page using a push named route.
void pushClientSignIn() {
pushNamed('/client-sign-in');
}
/// Navigates to the sign up page using a push named route.
void pushClientSignUp() {
pushNamed('/client-sign-up');
}
/// Navigates to the main client home dashboard.
///
/// Uses absolute path navigation to reset the navigation stack if necessary.
void navigateClientHome() {
navigate('/client-home');
}
}

View File

@@ -0,0 +1,252 @@
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import '../navigation/client_auth_navigator.dart';
class ClientGetStartedPage extends StatelessWidget {
const ClientGetStartedPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
// Background Illustration/Visuals from prototype
Positioned(
top: -100,
right: -100,
child: Container(
width: 400,
height: 400,
decoration: BoxDecoration(
color: UiColors.secondary.withAlpha(50),
shape: BoxShape.circle,
),
),
),
SafeArea(
child: Column(
children: [
const SizedBox(height: UiConstants.space10),
// Logo
Center(
child: Image.asset(
UiImageAssets.logoBlue,
height: 40,
fit: BoxFit.contain,
),
),
const Spacer(),
// Content Cards Area (Keeping prototype layout)
Container(
height: 300,
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space6,
),
child: Stack(
children: [
// Representative cards from prototype
Positioned(
top: 20,
left: 0,
right: 20,
child: _ShiftOrderCard(),
),
Positioned(
bottom: 40,
right: 0,
left: 40,
child: _WorkerProfileCard(),
),
Positioned(top: 60, right: 10, child: _CalendarCard()),
],
),
),
const Spacer(),
// Bottom Content
Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space6,
vertical: UiConstants.space10,
),
child: Column(
children: [
Text(
t.client_authentication.get_started_page.title,
textAlign: TextAlign.center,
style: UiTypography.displayM,
),
const SizedBox(height: UiConstants.space3),
Text(
t.client_authentication.get_started_page.subtitle,
textAlign: TextAlign.center,
style: UiTypography.body2r.textSecondary,
),
const SizedBox(height: UiConstants.space8),
// Sign In Button
UiButton.primary(
text: t
.client_authentication
.get_started_page
.sign_in_button,
onPressed: () => Modular.to.pushClientSignIn(),
),
const SizedBox(height: UiConstants.space3),
// Create Account Button
UiButton.secondary(
text: t
.client_authentication
.get_started_page
.create_account_button,
onPressed: () => Modular.to.pushClientSignUp(),
),
],
),
),
],
),
),
],
),
);
}
}
// Internal Prototype Widgets Updated with Design System Primitives
class _ShiftOrderCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
boxShadow: [
BoxShadow(
color: UiColors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(UiConstants.space1),
decoration: BoxDecoration(
color: UiColors.primary.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
UiIcons.briefcase,
size: 14,
color: UiColors.primary,
),
),
const SizedBox(width: UiConstants.space2),
Text('Shift Order #824', style: UiTypography.footnote1b),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: UiColors.tagPending,
borderRadius: UiConstants.radiusFull,
),
child: Text(
'Pending',
style: UiTypography.footnote2m.copyWith(
color: UiColors.textWarning,
),
),
),
],
),
const SizedBox(height: UiConstants.space2),
Text(
'Event Staffing - Hilton Hotel',
style: UiTypography.footnote2r.textSecondary,
),
],
),
);
}
}
class _WorkerProfileCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
boxShadow: [
BoxShadow(
color: UiColors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
CircleAvatar(
radius: 16,
backgroundColor: UiColors.primary.withOpacity(0.1),
child: const Icon(UiIcons.user, size: 16, color: UiColors.primary),
),
const SizedBox(width: UiConstants.space2),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Alex Thompson', style: UiTypography.footnote1b),
Text(
'Professional Waiter • 4.9★',
style: UiTypography.footnote2r.textSecondary,
),
],
),
],
),
);
}
}
class _CalendarCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(UiConstants.space2),
decoration: BoxDecoration(
color: UiColors.accent,
borderRadius: UiConstants.radiusMd,
boxShadow: [
BoxShadow(
color: UiColors.black.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(4, 4),
),
],
),
child: const Icon(
UiIcons.calendar,
size: 20,
color: UiColors.accentForeground,
),
);
}
}

View File

@@ -0,0 +1,147 @@
import 'package:client_authentication/src/presentation/widgets/common/section_titles.dart';
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/client_auth_bloc.dart';
import '../blocs/client_auth_event.dart';
import '../blocs/client_auth_state.dart';
import '../navigation/client_auth_navigator.dart';
import '../widgets/client_sign_in_page/client_sign_in_form.dart';
import '../widgets/common/auth_divider.dart';
import '../widgets/common/auth_social_button.dart';
/// Page for client users to sign in to their account.
///
/// This page provides email/password authentication as well as social sign-in
/// options via Apple and Google. It matches the design system standards
/// for client-facing authentication flows.
class ClientSignInPage extends StatelessWidget {
/// Creates a [ClientSignInPage].
const ClientSignInPage({super.key});
/// Dispatches the sign in event to the BLoC.
void _handleSignIn(
BuildContext context, {
required String email,
required String password,
}) {
BlocProvider.of<ClientAuthBloc>(
context,
).add(ClientSignInRequested(email: email, password: password));
}
/// Dispatches the social sign in event to the BLoC.
void _handleSocialSignIn(BuildContext context, {required String provider}) {
BlocProvider.of<ClientAuthBloc>(
context,
).add(ClientSocialSignInRequested(provider: provider));
}
@override
Widget build(BuildContext context) {
final i18n = t.client_authentication.sign_in_page;
final authBloc = Modular.get<ClientAuthBloc>();
return BlocProvider.value(
value: authBloc,
child: BlocConsumer<ClientAuthBloc, ClientAuthState>(
listener: (context, state) {
if (state.status == ClientAuthStatus.authenticated) {
Modular.to.navigateClientHome();
} else if (state.status == ClientAuthStatus.error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage ?? 'Authentication Error'),
),
);
}
},
builder: (context, state) {
final isLoading = state.status == ClientAuthStatus.loading;
return Scaffold(
appBar: const UiAppBar(showBackButton: true),
body: SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(
UiConstants.space6,
UiConstants.space8,
UiConstants.space6,
0,
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SectionTitle(title: i18n.title, subtitle: i18n.subtitle),
const SizedBox(height: UiConstants.space8),
// Sign In Form
ClientSignInForm(
isLoading: isLoading,
onSignIn: ({required email, required password}) =>
_handleSignIn(
context,
email: email,
password: password,
),
),
const SizedBox(height: UiConstants.space6),
// Divider
AuthDivider(text: i18n.or_divider),
const SizedBox(height: UiConstants.space6),
// Social Buttons
AuthSocialButton(
text: i18n.social_apple,
icon: UiIcons.apple,
onPressed: () =>
_handleSocialSignIn(context, provider: 'apple'),
),
const SizedBox(height: UiConstants.space3),
AuthSocialButton(
text: i18n.social_google,
icon: UiIcons.google,
onPressed: () =>
_handleSocialSignIn(context, provider: 'google'),
),
const SizedBox(height: UiConstants.space8),
// Sign Up Link
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
i18n.no_account,
style: UiTypography.body2r.textSecondary,
),
const SizedBox(width: UiConstants.space1),
GestureDetector(
onTap: () => Modular.to.pushClientSignUp(),
child: Text(
i18n.sign_up_link,
style: UiTypography.body2m.textLink,
),
),
],
),
const SizedBox(height: UiConstants.space10),
],
),
),
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,158 @@
import 'package:client_authentication/src/presentation/widgets/common/section_titles.dart';
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import '../blocs/client_auth_bloc.dart';
import '../blocs/client_auth_event.dart';
import '../blocs/client_auth_state.dart';
import '../navigation/client_auth_navigator.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.
///
/// This page collects company details, email, and password, and offers
/// social sign-up options. It adheres to the design system standards.
class ClientSignUpPage extends StatelessWidget {
/// Creates a [ClientSignUpPage].
const ClientSignUpPage({super.key});
/// Validates inputs and dispatches the sign up event.
void _handleSignUp(
BuildContext context, {
required String companyName,
required String email,
required String password,
}) {
BlocProvider.of<ClientAuthBloc>(context).add(
ClientSignUpRequested(
companyName: companyName,
email: email,
password: password,
),
);
}
/// Dispatches the social sign up event.
void _handleSocialSignUp(BuildContext context, {required String provider}) {
BlocProvider.of<ClientAuthBloc>(
context,
).add(ClientSocialSignInRequested(provider: provider));
}
@override
Widget build(BuildContext context) {
final i18n = t.client_authentication.sign_up_page;
final authBloc = Modular.get<ClientAuthBloc>();
return BlocProvider.value(
value: authBloc,
child: BlocConsumer<ClientAuthBloc, ClientAuthState>(
listener: (context, state) {
if (state.status == ClientAuthStatus.authenticated) {
Modular.to.navigateClientHome();
} else if (state.status == ClientAuthStatus.error) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage ?? 'Authentication Error'),
),
);
}
},
builder: (context, state) {
final isLoading = state.status == ClientAuthStatus.loading;
return Scaffold(
appBar: const UiAppBar(showBackButton: true),
body: SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(
UiConstants.space6,
UiConstants.space8,
UiConstants.space6,
0,
),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SectionTitle(title: i18n.title, subtitle: i18n.subtitle),
const SizedBox(height: UiConstants.space8),
// Sign Up Form
ClientSignUpForm(
isLoading: isLoading,
onSignUp:
({
required companyName,
required email,
required password,
}) => _handleSignUp(
context,
companyName: companyName,
email: email,
password: password,
),
),
const SizedBox(height: UiConstants.space6),
// Divider
// Divider
AuthDivider(text: i18n.or_divider),
const SizedBox(height: UiConstants.space6),
// Social Buttons
// Social Buttons
AuthSocialButton(
text: i18n.social_apple,
icon: UiIcons.apple,
onPressed: () =>
_handleSocialSignUp(context, provider: 'apple'),
),
const SizedBox(height: UiConstants.space3),
AuthSocialButton(
text: i18n.social_google,
icon: UiIcons.google,
onPressed: () =>
_handleSocialSignUp(context, provider: 'google'),
),
const SizedBox(height: UiConstants.space8),
// Sign In Link
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
i18n.has_account,
style: UiTypography.body2r.textSecondary,
),
const SizedBox(width: UiConstants.space1),
GestureDetector(
onTap: () => Modular.to.pushClientSignIn(),
child: Text(
i18n.sign_in_link,
style: UiTypography.body2m.textLink,
),
),
],
),
const SizedBox(height: UiConstants.space10),
],
),
),
),
),
);
},
),
);
}
}

View File

@@ -0,0 +1,113 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A form widget for client sign-in.
///
/// 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({
super.key,
required this.onSignIn,
this.isLoading = false,
});
@override
State<ClientSignInForm> createState() => _ClientSignInFormState();
}
class _ClientSignInFormState extends State<ClientSignInForm> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
void _handleSubmit() {
widget.onSignIn(
email: _emailController.text,
password: _passwordController.text,
);
}
@override
Widget build(BuildContext context) {
final i18n = t.client_authentication.sign_in_page;
return Column(
children: [
// Email Field
UiTextField(
label: i18n.email_label,
hintText: i18n.email_hint,
controller: _emailController,
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: UiConstants.space5),
// Password Field
UiTextField(
label: i18n.password_label,
hintText: i18n.password_hint,
controller: _passwordController,
obscureText: _obscurePassword,
suffix: IconButton(
icon: Icon(
_obscurePassword ? UiIcons.eyeOff : UiIcons.eye,
color: UiColors.iconSecondary,
size: 20,
),
onPressed: () =>
setState(() => _obscurePassword = !_obscurePassword),
),
),
const SizedBox(height: UiConstants.space2),
// Forgot Password
Align(
alignment: Alignment.centerLeft,
child: GestureDetector(
onTap: () {},
child: Text(
i18n.forgot_password,
style: UiTypography.body2r.textLink,
),
),
),
const SizedBox(height: UiConstants.space8),
// Sign In Button
UiButton.primary(
text: widget.isLoading ? null : i18n.sign_in_button,
onPressed: widget.isLoading ? null : _handleSubmit,
child: widget.isLoading
? const SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
color: UiColors.white,
strokeWidth: 2,
),
)
: null,
),
],
);
}
}

View File

@@ -0,0 +1,137 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A form widget for client sign-up.
///
/// This widget handles user input for company name, email, and password,
/// and delegates registration events to the parent via callbacks.
class ClientSignUpForm extends StatefulWidget {
/// Callback when the sign-up button is pressed.
final void Function({
required String companyName,
required String email,
required String password,
})
onSignUp;
/// 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();
}
class _ClientSignUpFormState extends State<ClientSignUpForm> {
final _companyController = TextEditingController();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
bool _obscurePassword = true;
@override
void dispose() {
_companyController.dispose();
_emailController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
void _handleSubmit() {
if (_passwordController.text != _confirmPasswordController.text) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Passwords do not match')));
return;
}
widget.onSignUp(
companyName: _companyController.text,
email: _emailController.text,
password: _passwordController.text,
);
}
@override
Widget build(BuildContext context) {
final i18n = t.client_authentication.sign_up_page;
return Column(
children: [
// Company Name Field
UiTextField(
label: i18n.company_label,
hintText: i18n.company_hint,
controller: _companyController,
textInputAction: TextInputAction.next,
),
const SizedBox(height: UiConstants.space4),
// Email Field
UiTextField(
label: i18n.email_label,
hintText: i18n.email_hint,
controller: _emailController,
keyboardType: TextInputType.emailAddress,
textInputAction: TextInputAction.next,
),
const SizedBox(height: UiConstants.space4),
// Password Field
UiTextField(
label: i18n.password_label,
hintText: i18n.password_hint,
controller: _passwordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.next,
suffix: IconButton(
icon: Icon(
_obscurePassword ? UiIcons.eyeOff : UiIcons.eye,
color: UiColors.iconSecondary,
size: 20,
),
onPressed: () =>
setState(() => _obscurePassword = !_obscurePassword),
),
),
const SizedBox(height: UiConstants.space4),
// Confirm Password Field
UiTextField(
label: i18n.confirm_password_label,
hintText: i18n.confirm_password_hint,
controller: _confirmPasswordController,
obscureText: _obscurePassword,
textInputAction: TextInputAction.done,
onSubmitted: (_) => _handleSubmit(),
),
const SizedBox(height: UiConstants.space8),
// Create Account Button
UiButton.primary(
text: widget.isLoading ? null : i18n.create_account_button,
onPressed: widget.isLoading ? null : _handleSubmit,
child: widget.isLoading
? const SizedBox(
height: 24,
width: 24,
child: CircularProgressIndicator(
color: UiColors.white,
strokeWidth: 2,
),
)
: null,
),
],
);
}
}

View File

@@ -0,0 +1,28 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A divider widget with centered text, typically used to separate
/// email/password auth from social auth headers.
///
/// 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});
@override
Widget build(BuildContext context) {
return Row(
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space4),
child: Text(text, style: UiTypography.footnote1r.textSecondary),
),
const Expanded(child: Divider()),
],
);
}
}

View File

@@ -0,0 +1,38 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A specialized button for social authentication integration.
///
/// 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].
///
/// The [text], [icon], and [onPressed] arguments must not be null.
const AuthSocialButton({
super.key,
required this.text,
required this.icon,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return UiButton.secondary(
onPressed: onPressed,
leadingIcon: icon,
text: text,
// Ensure the button spans the full width available
size: UiButtonSize.large,
);
}
}

View File

@@ -0,0 +1,24 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
/// A widget that displays a section title with a leading icon.
class SectionTitle extends StatelessWidget {
/// 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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: UiTypography.headline1m),
Text(subtitle, style: UiTypography.body2r.textSecondary),
],
);
}
}