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
|
||||
|
||||
Reference in New Issue
Block a user