initalizing the mobile apps
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
library client_authentication;
|
||||
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'src/data/repositories_impl/auth_repository_impl.dart';
|
||||
import 'src/domain/repositories/auth_repository_interface.dart';
|
||||
import 'src/domain/usecases/sign_in_with_email_use_case.dart';
|
||||
import 'src/domain/usecases/sign_in_with_social_use_case.dart';
|
||||
import 'src/domain/usecases/sign_out_use_case.dart';
|
||||
import 'src/domain/usecases/sign_up_with_email_use_case.dart';
|
||||
import 'src/presentation/blocs/client_auth_bloc.dart';
|
||||
import 'src/presentation/pages/client_get_started_page.dart';
|
||||
import 'src/presentation/pages/client_sign_in_page.dart';
|
||||
import 'src/presentation/pages/client_sign_up_page.dart';
|
||||
|
||||
export 'src/presentation/pages/client_get_started_page.dart';
|
||||
export 'src/presentation/pages/client_sign_in_page.dart';
|
||||
export 'src/presentation/pages/client_sign_up_page.dart';
|
||||
export 'src/presentation/navigation/client_auth_navigator.dart';
|
||||
export 'package:core_localization/core_localization.dart';
|
||||
|
||||
/// A [Module] for the client authentication feature.
|
||||
class ClientAuthenticationModule extends Module {
|
||||
@override
|
||||
List<Module> get imports => [DataConnectModule()];
|
||||
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repositories
|
||||
i.addLazySingleton<AuthRepositoryInterface>(
|
||||
() => AuthRepositoryImpl(dataSource: i.get<AuthRepositoryMock>()),
|
||||
);
|
||||
|
||||
// UseCases
|
||||
i.addLazySingleton(
|
||||
() => SignInWithEmailUseCase(i.get<AuthRepositoryInterface>()),
|
||||
);
|
||||
i.addLazySingleton(
|
||||
() => SignUpWithEmailUseCase(i.get<AuthRepositoryInterface>()),
|
||||
);
|
||||
i.addLazySingleton(
|
||||
() => SignInWithSocialUseCase(i.get<AuthRepositoryInterface>()),
|
||||
);
|
||||
i.addLazySingleton(() => SignOutUseCase(i.get<AuthRepositoryInterface>()));
|
||||
|
||||
// BLoCs
|
||||
i.addLazySingleton<ClientAuthBloc>(
|
||||
() => ClientAuthBloc(
|
||||
signInWithEmail: i.get<SignInWithEmailUseCase>(),
|
||||
signUpWithEmail: i.get<SignUpWithEmailUseCase>(),
|
||||
signInWithSocial: i.get<SignInWithSocialUseCase>(),
|
||||
signOut: i.get<SignOutUseCase>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void routes(r) {
|
||||
r.child('/', child: (_) => const ClientGetStartedPage());
|
||||
r.child('/client-sign-in', child: (_) => const ClientSignInPage());
|
||||
r.child('/client-sign-up', child: (_) => const ClientSignUpPage());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import '../../domain/repositories/auth_repository_interface.dart';
|
||||
|
||||
/// Production-ready implementation of the [AuthRepositoryInterface].
|
||||
///
|
||||
/// This implementation integrates with the [krow_data_connect] package to provide
|
||||
/// authentication services. It delegates actual data operations to the
|
||||
/// [AuthRepositoryMock] (or eventually the real implementation) injected from the app layer.
|
||||
class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
/// The data source used for authentication operations.
|
||||
final AuthRepositoryMock _dataSource;
|
||||
|
||||
/// Creates an [AuthRepositoryImpl] with the injected [dataSource] dependency.
|
||||
AuthRepositoryImpl({required AuthRepositoryMock dataSource})
|
||||
: _dataSource = dataSource;
|
||||
|
||||
@override
|
||||
Future<User> signInWithEmail({
|
||||
required String email,
|
||||
required String password,
|
||||
}) {
|
||||
return _dataSource.signInWithEmail(email, password);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<User> signUpWithEmail({
|
||||
required String companyName,
|
||||
required String email,
|
||||
required String password,
|
||||
}) {
|
||||
return _dataSource.signUpWithEmail(email, password, companyName);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<User> signInWithSocial({required String provider}) {
|
||||
return _dataSource.signInWithSocial(provider);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signOut() => _dataSource.signOut();
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user