feat: Refactor context reading in emergency contact and FAQs widgets

- Updated the context reading method in `EmergencyContactAddButton` and `EmergencyContactFormItem` to use `ReadContext`.
- Modified the `FaqsWidget` to utilize `ReadContext` for fetching FAQs.
- Adjusted the `PrivacySectionWidget` to read from `PrivacySecurityBloc` using `ReadContext`.

feat: Implement Firebase Auth isolation pattern

- Introduced `FirebaseAuthService` and `FirebaseAuthServiceImpl` to abstract Firebase Auth operations.
- Ensured features do not directly import `firebase_auth`, adhering to architecture rules.

feat: Create repository interfaces for billing and coverage

- Added `BillingRepositoryInterface` for billing-related operations.
- Created `CoverageRepositoryInterface` for coverage data access.

feat: Add use cases for order management

- Implemented use cases for fetching hubs, managers, and roles related to orders.
- Created `GetHubsUseCase`, `GetManagersByHubUseCase`, and `GetRolesByVendorUseCase`.

feat: Develop report use cases for client reports

- Added use cases for fetching various reports including coverage, daily operations, forecast, no-show, performance, and spend reports.
- Implemented `GetCoverageReportUseCase`, `GetDailyOpsReportUseCase`, `GetForecastReportUseCase`, `GetNoShowReportUseCase`, `GetPerformanceReportUseCase`, and `GetSpendReportUseCase`.

feat: Establish profile repository and use cases

- Created `ProfileRepositoryInterface` for staff profile data access.
- Implemented use cases for retrieving staff profile and section statuses: `GetStaffProfileUseCase` and `GetProfileSectionsUseCase`.
- Added `SignOutUseCase` for signing out the current user.
This commit is contained in:
Achintha Isuru
2026-03-19 01:10:27 -04:00
parent a45a3f6af1
commit 843eec5692
123 changed files with 2102 additions and 1087 deletions

View File

@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart' as domain;
import 'package:staff_authentication/src/domain/repositories/auth_repository_interface.dart';
@@ -9,55 +8,42 @@ import 'package:staff_authentication/src/utils/test_phone_numbers.dart';
/// V2 API implementation of [AuthRepositoryInterface].
///
/// Uses the Firebase Auth SDK for client-side phone verification,
/// Uses [FirebaseAuthService] from core for client-side phone verification,
/// then calls the V2 unified API to hydrate the session context.
/// All Data Connect dependencies have been removed.
/// All direct `firebase_auth` imports have been removed in favour of the
/// core abstraction.
class AuthRepositoryImpl implements AuthRepositoryInterface {
/// Creates an [AuthRepositoryImpl].
///
/// Requires a [domain.BaseApiService] for V2 API calls.
AuthRepositoryImpl({required domain.BaseApiService apiService})
: _apiService = apiService;
/// Requires a [domain.BaseApiService] for V2 API calls and a
/// [FirebaseAuthService] for client-side Firebase Auth operations.
AuthRepositoryImpl({
required domain.BaseApiService apiService,
required FirebaseAuthService firebaseAuthService,
}) : _apiService = apiService,
_firebaseAuthService = firebaseAuthService;
/// The V2 API service for backend calls.
final domain.BaseApiService _apiService;
/// Firebase Auth instance for client-side phone verification.
final FirebaseAuth _auth = FirebaseAuth.instance;
/// Completer for the pending phone verification request.
Completer<String?>? _pendingVerification;
/// Core Firebase Auth service abstraction.
final FirebaseAuthService _firebaseAuthService;
@override
Stream<domain.User?> get currentUser =>
_auth.authStateChanges().map((User? firebaseUser) {
if (firebaseUser == null) {
return null;
}
return domain.User(
id: firebaseUser.uid,
email: firebaseUser.email,
displayName: firebaseUser.displayName,
phone: firebaseUser.phoneNumber,
status: domain.UserStatus.active,
);
});
Stream<domain.User?> get currentUser => _firebaseAuthService.authStateChanges;
/// Initiates phone verification via the V2 API.
///
/// Calls `POST /auth/staff/phone/start` first. The server decides the
/// verification mode:
/// - `CLIENT_FIREBASE_SDK` mobile must do Firebase phone auth client-side
/// - `IDENTITY_TOOLKIT_SMS` server sent the SMS, returns `sessionInfo`
/// - `CLIENT_FIREBASE_SDK` -- mobile must do Firebase phone auth client-side
/// - `IDENTITY_TOOLKIT_SMS` -- server sent the SMS, returns `sessionInfo`
///
/// For mobile without recaptcha tokens, the server returns
/// `CLIENT_FIREBASE_SDK` and we fall back to the Firebase Auth SDK.
@override
Future<String?> signInWithPhone({required String phoneNumber}) async {
// Step 1: Try V2 to let the server decide the auth mode.
// Falls back to CLIENT_FIREBASE_SDK if the API call fails (e.g. server
// down, 500, or non-JSON response).
String mode = 'CLIENT_FIREBASE_SDK';
String? sessionInfo;
@@ -74,7 +60,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
mode = startData['mode'] as String? ?? 'CLIENT_FIREBASE_SDK';
sessionInfo = startData['sessionInfo'] as String?;
} catch (_) {
// V2 start call failed fall back to client-side Firebase SDK.
// V2 start call failed -- fall back to client-side Firebase SDK.
}
// Step 2: If server sent the SMS, return the sessionInfo for verify step.
@@ -82,55 +68,16 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
return sessionInfo;
}
// Step 3: CLIENT_FIREBASE_SDK mode do Firebase phone auth client-side.
final Completer<String?> completer = Completer<String?>();
_pendingVerification = completer;
await _auth.verifyPhoneNumber(
// Step 3: CLIENT_FIREBASE_SDK mode -- do Firebase phone auth client-side.
return _firebaseAuthService.verifyPhoneNumber(
phoneNumber: phoneNumber,
verificationCompleted: (PhoneAuthCredential credential) {
if (TestPhoneNumbers.isTestNumber(phoneNumber)) return;
},
verificationFailed: (FirebaseAuthException e) {
if (!completer.isCompleted) {
if (e.code == 'network-request-failed' ||
e.message?.contains('Unable to resolve host') == true) {
completer.completeError(
const domain.NetworkException(
technicalMessage: 'Auth network failure',
),
);
} else {
completer.completeError(
domain.SignInFailedException(
technicalMessage: 'Firebase ${e.code}: ${e.message}',
),
);
}
}
},
codeSent: (String verificationId, _) {
if (!completer.isCompleted) {
completer.complete(verificationId);
}
},
codeAutoRetrievalTimeout: (String verificationId) {
if (!completer.isCompleted) {
completer.complete(verificationId);
}
},
onAutoVerified: TestPhoneNumbers.isTestNumber(phoneNumber) ? null : null,
);
return completer.future;
}
@override
void cancelPendingPhoneVerification() {
final Completer<String?>? completer = _pendingVerification;
if (completer != null && !completer.isCompleted) {
completer.completeError(Exception('Phone verification cancelled.'));
}
_pendingVerification = null;
_firebaseAuthService.cancelPendingPhoneVerification();
}
/// Verifies the OTP and completes authentication via the V2 API.
@@ -145,53 +92,26 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
required String smsCode,
required AuthMode mode,
}) async {
// Step 1: Sign in with Firebase credential (client-side).
final PhoneAuthCredential credential = PhoneAuthProvider.credential(
// Step 1: Sign in with Firebase credential via core service.
final PhoneSignInResult signInResult =
await _firebaseAuthService.signInWithPhoneCredential(
verificationId: verificationId,
smsCode: smsCode,
);
final UserCredential userCredential;
try {
userCredential = await _auth.signInWithCredential(credential);
} on FirebaseAuthException catch (e) {
if (e.code == 'invalid-verification-code') {
throw const domain.InvalidCredentialsException(
technicalMessage: 'Invalid OTP code entered.',
);
}
rethrow;
}
final User? firebaseUser = userCredential.user;
if (firebaseUser == null) {
throw const domain.SignInFailedException(
technicalMessage:
'Phone verification failed, no Firebase user received.',
);
}
// Step 2: Get the Firebase ID token.
final String? idToken = await firebaseUser.getIdToken();
if (idToken == null) {
throw const domain.SignInFailedException(
technicalMessage: 'Failed to obtain Firebase ID token.',
);
}
// Step 3: Call V2 verify endpoint with the Firebase ID token.
// Step 2: Call V2 verify endpoint with the Firebase ID token.
final String v2Mode = mode == AuthMode.signup ? 'sign-up' : 'sign-in';
final domain.ApiResponse response = await _apiService.post(
AuthEndpoints.staffPhoneVerify,
data: <String, dynamic>{
'idToken': idToken,
'idToken': signInResult.idToken,
'mode': v2Mode,
},
);
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
// Step 4: Check for business logic errors from the V2 API.
// Step 3: Check for business logic errors from the V2 API.
final Map<String, dynamic>? staffData =
data['staff'] as Map<String, dynamic>?;
final Map<String, dynamic>? userData =
@@ -202,7 +122,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
// - Sign-in: staff must exist
if (mode == AuthMode.login) {
if (staffData == null) {
await _auth.signOut();
await _firebaseAuthService.signOut();
throw const domain.UserNotFoundException(
technicalMessage:
'Your account is not registered yet. Please register first.',
@@ -210,7 +130,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
}
}
// Step 5: Populate StaffSessionStore from the V2 auth envelope.
// Step 4: Populate StaffSessionStore from the V2 auth envelope.
if (staffData != null) {
final domain.StaffSession staffSession =
domain.StaffSession.fromJson(data);
@@ -219,10 +139,10 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
// Build the domain user from the V2 response.
final domain.User domainUser = domain.User(
id: userData?['id'] as String? ?? firebaseUser.uid,
id: userData?['id'] as String? ?? signInResult.uid,
email: userData?['email'] as String?,
displayName: userData?['displayName'] as String?,
phone: userData?['phone'] as String? ?? firebaseUser.phoneNumber,
phone: userData?['phone'] as String? ?? signInResult.phoneNumber,
status: domain.UserStatus.active,
);
@@ -238,7 +158,7 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
// Sign-out should not fail even if the API call fails.
// The local sign-out below will clear the session regardless.
}
await _auth.signOut();
await _firebaseAuthService.signOut();
StaffSessionStore.instance.clear();
}
}

View File

@@ -1,4 +1,3 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
@@ -11,13 +10,20 @@ import 'package:staff_authentication/src/domain/repositories/profile_setup_repos
class ProfileSetupRepositoryImpl implements ProfileSetupRepository {
/// Creates a [ProfileSetupRepositoryImpl].
///
/// Requires a [BaseApiService] for V2 API calls.
ProfileSetupRepositoryImpl({required BaseApiService apiService})
: _apiService = apiService;
/// Requires a [BaseApiService] for V2 API calls and a
/// [FirebaseAuthService] to resolve the current user's phone number.
ProfileSetupRepositoryImpl({
required BaseApiService apiService,
required FirebaseAuthService firebaseAuthService,
}) : _apiService = apiService,
_firebaseAuthService = firebaseAuthService;
/// The V2 API service for backend calls.
final BaseApiService _apiService;
/// Core Firebase Auth service for querying current user info.
final FirebaseAuthService _firebaseAuthService;
@override
Future<void> submitProfile({
required String fullName,
@@ -38,7 +44,7 @@ class ProfileSetupRepositoryImpl implements ProfileSetupRepository {
// to the Firebase Auth current user's phone if the caller passed empty.
final String resolvedPhone = phoneNumber.isNotEmpty
? phoneNumber
: (FirebaseAuth.instance.currentUser?.phoneNumber ?? '');
: (_firebaseAuthService.currentUserPhoneNumber ?? '');
final ApiResponse response = await _apiService.post(
StaffEndpoints.profileSetup,

View File

@@ -36,7 +36,7 @@ class _PhoneInputState extends State<PhoneInput> {
if (!mounted) return;
_currentPhone = value;
final AuthBloc bloc = context.read<AuthBloc>();
final AuthBloc bloc = ReadContext(context).read<AuthBloc>();
if (!bloc.isClosed) {
bloc.add(AuthPhoneUpdated(value));
}

View File

@@ -48,7 +48,7 @@ class _ProfileSetupLocationState extends State<ProfileSetupLocation> {
void _onSearchChanged(String query) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 300), () {
context.read<ProfileSetupBloc>().add(
ReadContext(context).read<ProfileSetupBloc>().add(
ProfileSetupLocationQueryChanged(query),
);
});
@@ -62,7 +62,7 @@ class _ProfileSetupLocationState extends State<ProfileSetupLocation> {
)..add(location);
widget.onLocationsChanged(updatedList);
_locationController.clear();
context.read<ProfileSetupBloc>().add(
ReadContext(context).read<ProfileSetupBloc>().add(
const ProfileSetupClearLocationSuggestions(),
);
}

View File

@@ -32,10 +32,16 @@ class StaffAuthenticationModule extends Module {
void binds(Injector i) {
// Repositories
i.addLazySingleton<AuthRepositoryInterface>(
() => AuthRepositoryImpl(apiService: i.get<BaseApiService>()),
() => AuthRepositoryImpl(
apiService: i.get<BaseApiService>(),
firebaseAuthService: i.get<FirebaseAuthService>(),
),
);
i.addLazySingleton<ProfileSetupRepository>(
() => ProfileSetupRepositoryImpl(apiService: i.get<BaseApiService>()),
() => ProfileSetupRepositoryImpl(
apiService: i.get<BaseApiService>(),
firebaseAuthService: i.get<FirebaseAuthService>(),
),
);
i.addLazySingleton<PlaceRepository>(PlaceRepositoryImpl.new);

View File

@@ -14,7 +14,6 @@ dependencies:
flutter_bloc: ^8.1.0
flutter_modular: ^6.3.0
equatable: ^2.0.5
firebase_auth: ^6.1.2
http: ^1.2.0
# Architecture Packages

View File

@@ -201,7 +201,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
height: 32,
child: OutlinedButton(
onPressed: () =>
context.read<AvailabilityBloc>().add(PerformQuickSet(type)),
ReadContext(context).read<AvailabilityBloc>().add(PerformQuickSet(type)),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.zero,
side: BorderSide(
@@ -252,14 +252,14 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
children: <Widget>[
_buildNavButton(
UiIcons.chevronLeft,
() => context.read<AvailabilityBloc>().add(
() => ReadContext(context).read<AvailabilityBloc>().add(
const NavigateWeek(-1),
),
),
Text(monthYear, style: UiTypography.title2b),
_buildNavButton(
UiIcons.chevronRight,
() => context.read<AvailabilityBloc>().add(
() => ReadContext(context).read<AvailabilityBloc>().add(
const NavigateWeek(1),
),
),
@@ -307,7 +307,7 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
return Expanded(
child: GestureDetector(
onTap: () =>
context.read<AvailabilityBloc>().add(SelectDate(dayDate)),
ReadContext(context).read<AvailabilityBloc>().add(SelectDate(dayDate)),
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
padding: const EdgeInsets.symmetric(vertical: UiConstants.space3),

View File

@@ -2,8 +2,8 @@ import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_home/src/domain/repositories/home_repository.dart';
import 'package:staff_home/src/domain/usecases/get_benefits_history_usecase.dart';
import 'package:staff_home/src/domain/usecases/get_home_shifts.dart';
part 'benefits_overview_state.dart';
@@ -14,14 +14,14 @@ class BenefitsOverviewCubit extends Cubit<BenefitsOverviewState>
with BlocErrorHandler<BenefitsOverviewState> {
/// Creates a [BenefitsOverviewCubit].
BenefitsOverviewCubit({
required HomeRepository repository,
required GetDashboardUseCase getDashboard,
required GetBenefitsHistoryUseCase getBenefitsHistory,
}) : _repository = repository,
}) : _getDashboard = getDashboard,
_getBenefitsHistory = getBenefitsHistory,
super(const BenefitsOverviewState.initial());
/// The repository used for dashboard data access.
final HomeRepository _repository;
/// Use case for fetching dashboard data.
final GetDashboardUseCase _getDashboard;
/// Use case for fetching benefit history.
final GetBenefitsHistoryUseCase _getBenefitsHistory;
@@ -33,7 +33,7 @@ class BenefitsOverviewCubit extends Cubit<BenefitsOverviewState>
await handleError(
emit: emit,
action: () async {
final StaffDashboard dashboard = await _repository.getDashboard();
final StaffDashboard dashboard = await _getDashboard();
if (isClosed) return;
emit(
state.copyWith(

View File

@@ -50,7 +50,7 @@ class StaffHomeModule extends Module {
// Cubit for benefits overview page (includes history support)
i.addLazySingleton<BenefitsOverviewCubit>(
() => BenefitsOverviewCubit(
repository: i.get<HomeRepository>(),
getDashboard: i.get<GetDashboardUseCase>(),
getBenefitsHistory: i.get<GetBenefitsHistoryUseCase>(),
),
);

View File

@@ -1,17 +1,19 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_profile/src/domain/repositories/profile_repository_interface.dart';
/// Repository implementation for the main profile page.
///
/// Uses the V2 API to fetch staff profile, section statuses, and completion.
class ProfileRepositoryImpl {
class ProfileRepositoryImpl implements ProfileRepositoryInterface {
/// Creates a [ProfileRepositoryImpl].
ProfileRepositoryImpl({required BaseApiService apiService})
: _api = apiService;
final BaseApiService _api;
/// Fetches the staff profile from the V2 session endpoint.
@override
Future<Staff> getStaffProfile() async {
final ApiResponse response =
await _api.get(StaffEndpoints.session);
@@ -20,7 +22,7 @@ class ProfileRepositoryImpl {
return Staff.fromJson(json);
}
/// Fetches the profile section completion statuses.
@override
Future<ProfileSectionStatus> getProfileSections() async {
final ApiResponse response =
await _api.get(StaffEndpoints.profileSections);
@@ -29,7 +31,7 @@ class ProfileRepositoryImpl {
return ProfileSectionStatus.fromJson(json);
}
/// Signs out the current user.
@override
Future<void> signOut() async {
await _api.post(AuthEndpoints.signOut);
}

View File

@@ -0,0 +1,16 @@
import 'package:krow_domain/krow_domain.dart';
/// Abstract interface for the staff profile repository.
///
/// Defines the contract for fetching staff profile data,
/// section completion statuses, and signing out.
abstract interface class ProfileRepositoryInterface {
/// Fetches the staff profile from the backend.
Future<Staff> getStaffProfile();
/// Fetches the profile section completion statuses.
Future<ProfileSectionStatus> getProfileSections();
/// Signs out the current user.
Future<void> signOut();
}

View File

@@ -0,0 +1,17 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_profile/src/domain/repositories/profile_repository_interface.dart';
/// Use case for retrieving profile section completion statuses.
class GetProfileSectionsUseCase implements NoInputUseCase<ProfileSectionStatus> {
/// Creates a [GetProfileSectionsUseCase] with the required [repository].
GetProfileSectionsUseCase(this._repository);
final ProfileRepositoryInterface _repository;
@override
Future<ProfileSectionStatus> call() {
return _repository.getProfileSections();
}
}

View File

@@ -0,0 +1,17 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_profile/src/domain/repositories/profile_repository_interface.dart';
/// Use case for retrieving the staff member's profile.
class GetStaffProfileUseCase implements NoInputUseCase<Staff> {
/// Creates a [GetStaffProfileUseCase] with the required [repository].
GetStaffProfileUseCase(this._repository);
final ProfileRepositoryInterface _repository;
@override
Future<Staff> call() {
return _repository.getStaffProfile();
}
}

View File

@@ -0,0 +1,16 @@
import 'package:krow_core/core.dart';
import 'package:staff_profile/src/domain/repositories/profile_repository_interface.dart';
/// Use case for signing out the current user.
class SignOutUseCase implements NoInputUseCase<void> {
/// Creates a [SignOutUseCase] with the required [repository].
SignOutUseCase(this._repository);
final ProfileRepositoryInterface _repository;
@override
Future<void> call() {
return _repository.signOut();
}
}

View File

@@ -2,19 +2,30 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_profile/src/data/repositories/profile_repository_impl.dart';
import 'package:staff_profile/src/domain/usecases/get_profile_sections_usecase.dart';
import 'package:staff_profile/src/domain/usecases/get_staff_profile_usecase.dart';
import 'package:staff_profile/src/domain/usecases/sign_out_usecase.dart';
import 'package:staff_profile/src/presentation/blocs/profile_state.dart';
/// Cubit for managing the Profile feature state.
///
/// Uses the V2 API via [ProfileRepositoryImpl] for all data fetching.
/// Delegates all data fetching to use cases, following Clean Architecture.
/// Loads the staff profile and section completion statuses in a single flow.
class ProfileCubit extends Cubit<ProfileState>
with BlocErrorHandler<ProfileState> {
/// Creates a [ProfileCubit] with the required repository.
ProfileCubit(this._repository) : super(const ProfileState());
/// Creates a [ProfileCubit] with the required use cases.
ProfileCubit({
required GetStaffProfileUseCase getStaffProfileUseCase,
required GetProfileSectionsUseCase getProfileSectionsUseCase,
required SignOutUseCase signOutUseCase,
}) : _getStaffProfileUseCase = getStaffProfileUseCase,
_getProfileSectionsUseCase = getProfileSectionsUseCase,
_signOutUseCase = signOutUseCase,
super(const ProfileState());
final ProfileRepositoryImpl _repository;
final GetStaffProfileUseCase _getStaffProfileUseCase;
final GetProfileSectionsUseCase _getProfileSectionsUseCase;
final SignOutUseCase _signOutUseCase;
/// Loads the staff member's profile.
Future<void> loadProfile() async {
@@ -23,7 +34,7 @@ class ProfileCubit extends Cubit<ProfileState>
await handleError(
emit: emit,
action: () async {
final Staff profile = await _repository.getStaffProfile();
final Staff profile = await _getStaffProfileUseCase();
emit(state.copyWith(status: ProfileStatus.loaded, profile: profile));
},
onError: (String errorKey) =>
@@ -37,7 +48,7 @@ class ProfileCubit extends Cubit<ProfileState>
emit: emit,
action: () async {
final ProfileSectionStatus sections =
await _repository.getProfileSections();
await _getProfileSectionsUseCase();
emit(state.copyWith(
personalInfoComplete: sections.personalInfoCompleted,
emergencyContactsComplete: sections.emergencyContactCompleted,
@@ -62,7 +73,7 @@ class ProfileCubit extends Cubit<ProfileState>
await handleError(
emit: emit,
action: () async {
await _repository.signOut();
await _signOutUseCase();
emit(state.copyWith(status: ProfileStatus.signedOut));
},
onError: (String _) =>

View File

@@ -19,7 +19,7 @@ class LogoutButton extends StatelessWidget {
/// sign-out process via the ProfileCubit.
void _handleSignOut(BuildContext context, ProfileState state) {
if (state.status != ProfileStatus.loading) {
context.read<ProfileCubit>().signOut();
ReadContext(context).read<ProfileCubit>().signOut();
}
}
@@ -47,7 +47,7 @@ class LogoutButton extends StatelessWidget {
onTap: () {
_handleSignOut(
context,
context.read<ProfileCubit>().state,
ReadContext(context).read<ProfileCubit>().state,
);
},
borderRadius: UiConstants.radiusLg,

View File

@@ -4,13 +4,17 @@ import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_profile/src/data/repositories/profile_repository_impl.dart';
import 'package:staff_profile/src/domain/repositories/profile_repository_interface.dart';
import 'package:staff_profile/src/domain/usecases/get_profile_sections_usecase.dart';
import 'package:staff_profile/src/domain/usecases/get_staff_profile_usecase.dart';
import 'package:staff_profile/src/domain/usecases/sign_out_usecase.dart';
import 'package:staff_profile/src/presentation/blocs/profile_cubit.dart';
import 'package:staff_profile/src/presentation/pages/staff_profile_page.dart';
/// The entry module for the Staff Profile feature.
///
/// Uses the V2 REST API via [BaseApiService] for all backend access.
/// Section completion statuses are fetched in a single API call.
/// Registers repository interface, use cases, and cubit for DI.
class StaffProfileModule extends Module {
@override
List<Module> get imports => <Module>[CoreModule()];
@@ -18,15 +22,36 @@ class StaffProfileModule extends Module {
@override
void binds(Injector i) {
// Repository
i.addLazySingleton<ProfileRepositoryImpl>(
i.addLazySingleton<ProfileRepositoryInterface>(
() => ProfileRepositoryImpl(
apiService: i.get<BaseApiService>(),
),
);
// Use Cases
i.addLazySingleton<GetStaffProfileUseCase>(
() => GetStaffProfileUseCase(
i.get<ProfileRepositoryInterface>(),
),
);
i.addLazySingleton<GetProfileSectionsUseCase>(
() => GetProfileSectionsUseCase(
i.get<ProfileRepositoryInterface>(),
),
);
i.addLazySingleton<SignOutUseCase>(
() => SignOutUseCase(
i.get<ProfileRepositoryInterface>(),
),
);
// Cubit
i.addLazySingleton<ProfileCubit>(
() => ProfileCubit(i.get<ProfileRepositoryImpl>()),
() => ProfileCubit(
getStaffProfileUseCase: i.get<GetStaffProfileUseCase>(),
getProfileSectionsUseCase: i.get<GetProfileSectionsUseCase>(),
signOutUseCase: i.get<SignOutUseCase>(),
),
);
}

View File

@@ -121,14 +121,14 @@ class _FormI9PageState extends State<FormI9Page> {
void _handleNext(BuildContext context, int currentStep) {
if (currentStep < _steps.length - 1) {
context.read<FormI9Cubit>().nextStep(_steps.length);
ReadContext(context).read<FormI9Cubit>().nextStep(_steps.length);
} else {
context.read<FormI9Cubit>().submit();
ReadContext(context).read<FormI9Cubit>().submit();
}
}
void _handleBack(BuildContext context) {
context.read<FormI9Cubit>().previousStep();
ReadContext(context).read<FormI9Cubit>().previousStep();
}
@override
@@ -459,7 +459,7 @@ class _FormI9PageState extends State<FormI9Page> {
i18n.fields.first_name,
value: state.firstName,
onChanged: (String val) =>
context.read<FormI9Cubit>().firstNameChanged(val),
ReadContext(context).read<FormI9Cubit>().firstNameChanged(val),
placeholder: i18n.fields.hints.first_name,
),
),
@@ -469,7 +469,7 @@ class _FormI9PageState extends State<FormI9Page> {
i18n.fields.last_name,
value: state.lastName,
onChanged: (String val) =>
context.read<FormI9Cubit>().lastNameChanged(val),
ReadContext(context).read<FormI9Cubit>().lastNameChanged(val),
placeholder: i18n.fields.hints.last_name,
),
),
@@ -483,7 +483,7 @@ class _FormI9PageState extends State<FormI9Page> {
i18n.fields.middle_initial,
value: state.middleInitial,
onChanged: (String val) =>
context.read<FormI9Cubit>().middleInitialChanged(val),
ReadContext(context).read<FormI9Cubit>().middleInitialChanged(val),
placeholder: i18n.fields.hints.middle_initial,
),
),
@@ -494,7 +494,7 @@ class _FormI9PageState extends State<FormI9Page> {
i18n.fields.other_last_names,
value: state.otherLastNames,
onChanged: (String val) =>
context.read<FormI9Cubit>().otherLastNamesChanged(val),
ReadContext(context).read<FormI9Cubit>().otherLastNamesChanged(val),
placeholder: i18n.fields.maiden_name,
),
),
@@ -505,7 +505,7 @@ class _FormI9PageState extends State<FormI9Page> {
i18n.fields.dob,
value: state.dob,
onChanged: (String val) =>
context.read<FormI9Cubit>().dobChanged(val),
ReadContext(context).read<FormI9Cubit>().dobChanged(val),
placeholder: i18n.fields.hints.dob,
keyboardType: TextInputType.datetime,
),
@@ -518,7 +518,7 @@ class _FormI9PageState extends State<FormI9Page> {
onChanged: (String val) {
String text = val.replaceAll(RegExp(r'\D'), '');
if (text.length > 9) text = text.substring(0, 9);
context.read<FormI9Cubit>().ssnChanged(text);
ReadContext(context).read<FormI9Cubit>().ssnChanged(text);
},
),
const SizedBox(height: UiConstants.space4),
@@ -526,7 +526,7 @@ class _FormI9PageState extends State<FormI9Page> {
i18n.fields.email,
value: state.email,
onChanged: (String val) =>
context.read<FormI9Cubit>().emailChanged(val),
ReadContext(context).read<FormI9Cubit>().emailChanged(val),
keyboardType: TextInputType.emailAddress,
placeholder: i18n.fields.hints.email,
),
@@ -535,7 +535,7 @@ class _FormI9PageState extends State<FormI9Page> {
i18n.fields.phone,
value: state.phone,
onChanged: (String val) =>
context.read<FormI9Cubit>().phoneChanged(val),
ReadContext(context).read<FormI9Cubit>().phoneChanged(val),
keyboardType: TextInputType.phone,
placeholder: i18n.fields.hints.phone,
),
@@ -554,7 +554,7 @@ class _FormI9PageState extends State<FormI9Page> {
i18n.fields.address_long,
value: state.address,
onChanged: (String val) =>
context.read<FormI9Cubit>().addressChanged(val),
ReadContext(context).read<FormI9Cubit>().addressChanged(val),
placeholder: i18n.fields.hints.address,
),
const SizedBox(height: UiConstants.space4),
@@ -562,7 +562,7 @@ class _FormI9PageState extends State<FormI9Page> {
i18n.fields.apt,
value: state.aptNumber,
onChanged: (String val) =>
context.read<FormI9Cubit>().aptNumberChanged(val),
ReadContext(context).read<FormI9Cubit>().aptNumberChanged(val),
placeholder: i18n.fields.hints.apt,
),
const SizedBox(height: UiConstants.space4),
@@ -574,7 +574,7 @@ class _FormI9PageState extends State<FormI9Page> {
i18n.fields.city,
value: state.city,
onChanged: (String val) =>
context.read<FormI9Cubit>().cityChanged(val),
ReadContext(context).read<FormI9Cubit>().cityChanged(val),
placeholder: i18n.fields.hints.city,
),
),
@@ -593,7 +593,7 @@ class _FormI9PageState extends State<FormI9Page> {
DropdownButtonFormField<String>(
initialValue: state.state.isEmpty ? null : state.state,
onChanged: (String? val) =>
context.read<FormI9Cubit>().stateChanged(val ?? ''),
ReadContext(context).read<FormI9Cubit>().stateChanged(val ?? ''),
items: _usStates.map((String stateAbbr) {
return DropdownMenuItem<String>(
value: stateAbbr,
@@ -626,7 +626,7 @@ class _FormI9PageState extends State<FormI9Page> {
i18n.fields.zip,
value: state.zipCode,
onChanged: (String val) =>
context.read<FormI9Cubit>().zipCodeChanged(val),
ReadContext(context).read<FormI9Cubit>().zipCodeChanged(val),
placeholder: i18n.fields.hints.zip,
keyboardType: TextInputType.number,
),
@@ -660,7 +660,7 @@ class _FormI9PageState extends State<FormI9Page> {
i18n.fields.uscis_number_label,
value: state.uscisNumber,
onChanged: (String val) =>
context.read<FormI9Cubit>().uscisNumberChanged(val),
ReadContext(context).read<FormI9Cubit>().uscisNumberChanged(val),
placeholder: i18n.fields.hints.uscis,
),
)
@@ -718,7 +718,7 @@ class _FormI9PageState extends State<FormI9Page> {
}) {
final bool isSelected = state.citizenshipStatus == value;
return GestureDetector(
onTap: () => context.read<FormI9Cubit>().citizenshipStatusChanged(value),
onTap: () => ReadContext(context).read<FormI9Cubit>().citizenshipStatusChanged(value),
child: Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
@@ -803,7 +803,7 @@ class _FormI9PageState extends State<FormI9Page> {
CheckboxListTile(
value: state.preparerUsed,
onChanged: (bool? val) {
context.read<FormI9Cubit>().preparerUsedChanged(val ?? false);
ReadContext(context).read<FormI9Cubit>().preparerUsedChanged(val ?? false);
},
contentPadding: EdgeInsets.zero,
title: Text(
@@ -837,7 +837,7 @@ class _FormI9PageState extends State<FormI9Page> {
TextPosition(offset: state.signature.length),
),
onChanged: (String val) =>
context.read<FormI9Cubit>().signatureChanged(val),
ReadContext(context).read<FormI9Cubit>().signatureChanged(val),
decoration: InputDecoration(
hintText: i18n.fields.signature_hint,
filled: true,

View File

@@ -111,14 +111,14 @@ class _FormW4PageState extends State<FormW4Page> {
void _handleNext(BuildContext context, int currentStep) {
if (currentStep < _steps.length - 1) {
context.read<FormW4Cubit>().nextStep(_steps.length);
ReadContext(context).read<FormW4Cubit>().nextStep(_steps.length);
} else {
context.read<FormW4Cubit>().submit();
ReadContext(context).read<FormW4Cubit>().submit();
}
}
void _handleBack(BuildContext context) {
context.read<FormW4Cubit>().previousStep();
ReadContext(context).read<FormW4Cubit>().previousStep();
}
int _totalCredits(FormW4State state) {
@@ -458,7 +458,7 @@ class _FormW4PageState extends State<FormW4Page> {
i18n.fields.first_name,
value: state.firstName,
onChanged: (String val) =>
context.read<FormW4Cubit>().firstNameChanged(val),
ReadContext(context).read<FormW4Cubit>().firstNameChanged(val),
placeholder: i18n.fields.placeholder_john,
),
),
@@ -468,7 +468,7 @@ class _FormW4PageState extends State<FormW4Page> {
i18n.fields.last_name,
value: state.lastName,
onChanged: (String val) =>
context.read<FormW4Cubit>().lastNameChanged(val),
ReadContext(context).read<FormW4Cubit>().lastNameChanged(val),
placeholder: i18n.fields.placeholder_smith,
),
),
@@ -483,7 +483,7 @@ class _FormW4PageState extends State<FormW4Page> {
onChanged: (String val) {
String text = val.replaceAll(RegExp(r'\D'), '');
if (text.length > 9) text = text.substring(0, 9);
context.read<FormW4Cubit>().ssnChanged(text);
ReadContext(context).read<FormW4Cubit>().ssnChanged(text);
},
),
const SizedBox(height: UiConstants.space4),
@@ -491,7 +491,7 @@ class _FormW4PageState extends State<FormW4Page> {
i18n.fields.address,
value: state.address,
onChanged: (String val) =>
context.read<FormW4Cubit>().addressChanged(val),
ReadContext(context).read<FormW4Cubit>().addressChanged(val),
placeholder: i18n.fields.placeholder_address,
),
const SizedBox(height: UiConstants.space4),
@@ -499,7 +499,7 @@ class _FormW4PageState extends State<FormW4Page> {
i18n.fields.city_state_zip,
value: state.cityStateZip,
onChanged: (String val) =>
context.read<FormW4Cubit>().cityStateZipChanged(val),
ReadContext(context).read<FormW4Cubit>().cityStateZipChanged(val),
placeholder: i18n.fields.placeholder_csz,
),
],
@@ -557,7 +557,7 @@ class _FormW4PageState extends State<FormW4Page> {
) {
final bool isSelected = state.filingStatus == value;
return GestureDetector(
onTap: () => context.read<FormW4Cubit>().filingStatusChanged(value),
onTap: () => ReadContext(context).read<FormW4Cubit>().filingStatusChanged(value),
child: Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
@@ -641,7 +641,7 @@ class _FormW4PageState extends State<FormW4Page> {
),
const SizedBox(height: UiConstants.space6),
GestureDetector(
onTap: () => context.read<FormW4Cubit>().multipleJobsChanged(
onTap: () => ReadContext(context).read<FormW4Cubit>().multipleJobsChanged(
!state.multipleJobs,
),
child: Container(
@@ -752,7 +752,7 @@ class _FormW4PageState extends State<FormW4Page> {
i18n.fields.children_each,
(FormW4State s) => s.qualifyingChildren,
(int val) =>
context.read<FormW4Cubit>().qualifyingChildrenChanged(val),
ReadContext(context).read<FormW4Cubit>().qualifyingChildrenChanged(val),
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
@@ -765,7 +765,7 @@ class _FormW4PageState extends State<FormW4Page> {
i18n.fields.other_each,
(FormW4State s) => s.otherDependents,
(int val) =>
context.read<FormW4Cubit>().otherDependentsChanged(val),
ReadContext(context).read<FormW4Cubit>().otherDependentsChanged(val),
),
],
),
@@ -881,7 +881,7 @@ class _FormW4PageState extends State<FormW4Page> {
i18n.fields.other_income,
value: state.otherIncome,
onChanged: (String val) =>
context.read<FormW4Cubit>().otherIncomeChanged(val),
ReadContext(context).read<FormW4Cubit>().otherIncomeChanged(val),
placeholder: i18n.fields.hints.zero,
keyboardType: TextInputType.number,
),
@@ -897,7 +897,7 @@ class _FormW4PageState extends State<FormW4Page> {
i18n.fields.deductions,
value: state.deductions,
onChanged: (String val) =>
context.read<FormW4Cubit>().deductionsChanged(val),
ReadContext(context).read<FormW4Cubit>().deductionsChanged(val),
placeholder: i18n.fields.hints.zero,
keyboardType: TextInputType.number,
),
@@ -913,7 +913,7 @@ class _FormW4PageState extends State<FormW4Page> {
i18n.fields.extra_withholding,
value: state.extraWithholding,
onChanged: (String val) =>
context.read<FormW4Cubit>().extraWithholdingChanged(val),
ReadContext(context).read<FormW4Cubit>().extraWithholdingChanged(val),
placeholder: i18n.fields.hints.zero,
keyboardType: TextInputType.number,
),
@@ -996,7 +996,7 @@ class _FormW4PageState extends State<FormW4Page> {
TextPosition(offset: state.signature.length),
),
onChanged: (String val) =>
context.read<FormW4Cubit>().signatureChanged(val),
ReadContext(context).read<FormW4Cubit>().signatureChanged(val),
decoration: InputDecoration(
hintText: i18n.fields.signature_hint,
filled: true,

View File

@@ -11,7 +11,7 @@ class EmergencyContactAddButton extends StatelessWidget {
return Center(
child: TextButton.icon(
onPressed: () =>
context.read<EmergencyContactBloc>().add(EmergencyContactAdded()),
ReadContext(context).read<EmergencyContactBloc>().add(EmergencyContactAdded()),
icon: const Icon(UiIcons.add, size: 20.0),
label: Text(
'Add Another Contact',

View File

@@ -44,7 +44,7 @@ class EmergencyContactFormItem extends StatelessWidget {
initialValue: contact.fullName,
hint: 'Contact name',
icon: UiIcons.user,
onChanged: (val) => context.read<EmergencyContactBloc>().add(
onChanged: (val) => ReadContext(context).read<EmergencyContactBloc>().add(
EmergencyContactUpdated(index, contact.copyWith(fullName: val)),
),
),
@@ -54,7 +54,7 @@ class EmergencyContactFormItem extends StatelessWidget {
initialValue: contact.phone,
hint: '+1 (555) 000-0000',
icon: UiIcons.phone,
onChanged: (val) => context.read<EmergencyContactBloc>().add(
onChanged: (val) => ReadContext(context).read<EmergencyContactBloc>().add(
EmergencyContactUpdated(index, contact.copyWith(phone: val)),
),
),
@@ -66,7 +66,7 @@ class EmergencyContactFormItem extends StatelessWidget {
items: _kRelationshipTypes,
onChanged: (val) {
if (val != null) {
context.read<EmergencyContactBloc>().add(
ReadContext(context).read<EmergencyContactBloc>().add(
EmergencyContactUpdated(
index,
contact.copyWith(relationshipType: val),
@@ -144,7 +144,7 @@ class EmergencyContactFormItem extends StatelessWidget {
color: UiColors.textError,
size: 20.0,
),
onPressed: () => context.read<EmergencyContactBloc>().add(
onPressed: () => ReadContext(context).read<EmergencyContactBloc>().add(
EmergencyContactRemoved(index),
),
),

View File

@@ -37,9 +37,9 @@ class _FaqsWidgetState extends State<FaqsWidget> {
void _onSearchChanged(String value) {
if (value.isEmpty) {
context.read<FaqsBloc>().add(const FetchFaqsEvent());
ReadContext(context).read<FaqsBloc>().add(const FetchFaqsEvent());
} else {
context.read<FaqsBloc>().add(SearchFaqsEvent(query: value));
ReadContext(context).read<FaqsBloc>().add(SearchFaqsEvent(query: value));
}
}

View File

@@ -23,7 +23,7 @@ class PrivacySectionWidget extends StatelessWidget {
type: UiSnackbarType.success,
);
// Clear the flag after showing the snackbar
context.read<PrivacySecurityBloc>().add(
ReadContext(context).read<PrivacySecurityBloc>().add(
const ClearProfileVisibilityUpdatedEvent(),
);
}

View File

@@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
@@ -9,7 +8,9 @@ import 'package:staff_main/src/presentation/blocs/staff_main_state.dart';
///
/// Tracks the active bottom-bar tab index, profile completion status, and
/// bottom bar visibility based on the current route.
class StaffMainCubit extends Cubit<StaffMainState> implements Disposable {
class StaffMainCubit extends Cubit<StaffMainState>
with BlocErrorHandler<StaffMainState>
implements Disposable {
/// Creates a [StaffMainCubit].
StaffMainCubit({
required GetProfileCompletionUseCase getProfileCompletionUsecase,
@@ -67,20 +68,21 @@ class StaffMainCubit extends Cubit<StaffMainState> implements Disposable {
if (_isLoadingCompletion || isClosed) return;
_isLoadingCompletion = true;
try {
final bool isComplete = await _getProfileCompletionUsecase();
if (!isClosed) {
emit(state.copyWith(isProfileComplete: isComplete));
}
} catch (e) {
// If there's an error, allow access to all features
debugPrint('Error loading profile completion: $e');
if (!isClosed) {
emit(state.copyWith(isProfileComplete: true));
}
} finally {
_isLoadingCompletion = false;
}
await handleError(
emit: emit,
action: () async {
final bool isComplete = await _getProfileCompletionUsecase();
if (!isClosed) {
emit(state.copyWith(isProfileComplete: isComplete));
}
},
onError: (String errorKey) {
// If there's an error, allow access to all features
_isLoadingCompletion = false;
return state.copyWith(isProfileComplete: true);
},
);
_isLoadingCompletion = false;
}
/// Navigates to the tab at [index].