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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 _) =>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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].
|
||||
|
||||
Reference in New Issue
Block a user