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