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:
@@ -44,6 +44,7 @@ export 'src/services/session/v2_session_service.dart';
|
||||
|
||||
// Auth
|
||||
export 'src/services/auth/auth_token_provider.dart';
|
||||
export 'src/services/auth/firebase_auth_service.dart';
|
||||
|
||||
// Device Services
|
||||
export 'src/services/device/camera/camera_service.dart';
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:image_picker/image_picker.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:krow_core/src/services/auth/auth_token_provider.dart';
|
||||
import 'package:krow_core/src/services/auth/firebase_auth_service.dart';
|
||||
import 'package:krow_core/src/services/auth/firebase_auth_token_provider.dart';
|
||||
|
||||
import '../core.dart';
|
||||
@@ -63,7 +64,10 @@ class CoreModule extends Module {
|
||||
// 6. Auth Token Provider
|
||||
i.addLazySingleton<AuthTokenProvider>(FirebaseAuthTokenProvider.new);
|
||||
|
||||
// 7. Register Geofence Device Services
|
||||
// 7. Firebase Auth Service (so features never import firebase_auth)
|
||||
i.addLazySingleton<FirebaseAuthService>(FirebaseAuthServiceImpl.new);
|
||||
|
||||
// 8. Register Geofence Device Services
|
||||
i.addLazySingleton<LocationService>(() => const LocationService());
|
||||
i.addLazySingleton<NotificationService>(() => NotificationService());
|
||||
i.addLazySingleton<StorageService>(() => StorageService());
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
||||
|
||||
import 'package:krow_domain/krow_domain.dart'
|
||||
show
|
||||
InvalidCredentialsException,
|
||||
NetworkException,
|
||||
SignInFailedException,
|
||||
User,
|
||||
UserStatus;
|
||||
|
||||
/// Abstraction over Firebase Auth client-side operations.
|
||||
///
|
||||
/// Provides phone-based and email-based authentication, sign-out,
|
||||
/// auth state observation, and current user queries. Lives in core
|
||||
/// so feature packages never import `firebase_auth` directly.
|
||||
abstract interface class FirebaseAuthService {
|
||||
/// Stream of the currently signed-in user mapped to a domain [User].
|
||||
///
|
||||
/// Emits `null` when the user signs out.
|
||||
Stream<User?> get authStateChanges;
|
||||
|
||||
/// Returns the current user's phone number, or `null` if unavailable.
|
||||
String? get currentUserPhoneNumber;
|
||||
|
||||
/// Returns the current user's UID, or `null` if not signed in.
|
||||
String? get currentUserUid;
|
||||
|
||||
/// Initiates phone number verification via Firebase Auth SDK.
|
||||
///
|
||||
/// Returns a [Future] that completes with the verification ID when
|
||||
/// the SMS code is sent. The [onAutoVerified] callback fires if the
|
||||
/// device auto-retrieves the credential (Android only).
|
||||
Future<String?> verifyPhoneNumber({
|
||||
required String phoneNumber,
|
||||
void Function()? onAutoVerified,
|
||||
});
|
||||
|
||||
/// Cancels any pending phone verification request.
|
||||
void cancelPendingPhoneVerification();
|
||||
|
||||
/// Signs in with a phone auth credential built from
|
||||
/// [verificationId] and [smsCode].
|
||||
///
|
||||
/// Returns the signed-in domain [User] or throws a domain exception.
|
||||
Future<PhoneSignInResult> signInWithPhoneCredential({
|
||||
required String verificationId,
|
||||
required String smsCode,
|
||||
});
|
||||
|
||||
/// Signs in with email and password via Firebase Auth SDK.
|
||||
///
|
||||
/// Returns the Firebase UID on success or throws a domain exception.
|
||||
Future<String> signInWithEmailAndPassword({
|
||||
required String email,
|
||||
required String password,
|
||||
});
|
||||
|
||||
/// Signs out the current user from Firebase Auth locally.
|
||||
Future<void> signOut();
|
||||
|
||||
/// Returns the current user's Firebase ID token.
|
||||
///
|
||||
/// Returns `null` if no user is signed in.
|
||||
Future<String?> getIdToken();
|
||||
}
|
||||
|
||||
/// Result of a phone credential sign-in.
|
||||
///
|
||||
/// Contains the Firebase user's UID, phone number, and ID token
|
||||
/// so the caller can proceed with V2 API verification without
|
||||
/// importing `firebase_auth`.
|
||||
class PhoneSignInResult {
|
||||
/// Creates a [PhoneSignInResult].
|
||||
const PhoneSignInResult({
|
||||
required this.uid,
|
||||
required this.phoneNumber,
|
||||
required this.idToken,
|
||||
});
|
||||
|
||||
/// The Firebase user UID.
|
||||
final String uid;
|
||||
|
||||
/// The phone number associated with the credential.
|
||||
final String? phoneNumber;
|
||||
|
||||
/// The Firebase ID token for the signed-in user.
|
||||
final String? idToken;
|
||||
}
|
||||
|
||||
/// Firebase-backed implementation of [FirebaseAuthService].
|
||||
///
|
||||
/// Wraps the `firebase_auth` package so that feature packages
|
||||
/// interact with Firebase Auth only through this core service.
|
||||
class FirebaseAuthServiceImpl implements FirebaseAuthService {
|
||||
/// Creates a [FirebaseAuthServiceImpl].
|
||||
///
|
||||
/// Optionally accepts a [firebase.FirebaseAuth] instance for testing.
|
||||
FirebaseAuthServiceImpl({firebase.FirebaseAuth? auth})
|
||||
: _auth = auth ?? firebase.FirebaseAuth.instance;
|
||||
|
||||
/// The Firebase Auth instance.
|
||||
final firebase.FirebaseAuth _auth;
|
||||
|
||||
/// Completer for the pending phone verification request.
|
||||
Completer<String?>? _pendingVerification;
|
||||
|
||||
@override
|
||||
Stream<User?> get authStateChanges =>
|
||||
_auth.authStateChanges().map((firebase.User? firebaseUser) {
|
||||
if (firebaseUser == null) {
|
||||
return null;
|
||||
}
|
||||
return User(
|
||||
id: firebaseUser.uid,
|
||||
email: firebaseUser.email,
|
||||
displayName: firebaseUser.displayName,
|
||||
phone: firebaseUser.phoneNumber,
|
||||
status: UserStatus.active,
|
||||
);
|
||||
});
|
||||
|
||||
@override
|
||||
String? get currentUserPhoneNumber => _auth.currentUser?.phoneNumber;
|
||||
|
||||
@override
|
||||
String? get currentUserUid => _auth.currentUser?.uid;
|
||||
|
||||
@override
|
||||
Future<String?> verifyPhoneNumber({
|
||||
required String phoneNumber,
|
||||
void Function()? onAutoVerified,
|
||||
}) async {
|
||||
final Completer<String?> completer = Completer<String?>();
|
||||
_pendingVerification = completer;
|
||||
|
||||
await _auth.verifyPhoneNumber(
|
||||
phoneNumber: phoneNumber,
|
||||
verificationCompleted: (firebase.PhoneAuthCredential credential) {
|
||||
onAutoVerified?.call();
|
||||
},
|
||||
verificationFailed: (firebase.FirebaseAuthException e) {
|
||||
if (!completer.isCompleted) {
|
||||
if (e.code == 'network-request-failed' ||
|
||||
e.message?.contains('Unable to resolve host') == true) {
|
||||
completer.completeError(
|
||||
const NetworkException(
|
||||
technicalMessage: 'Auth network failure',
|
||||
),
|
||||
);
|
||||
} else {
|
||||
completer.completeError(
|
||||
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);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
@override
|
||||
void cancelPendingPhoneVerification() {
|
||||
final Completer<String?>? completer = _pendingVerification;
|
||||
if (completer != null && !completer.isCompleted) {
|
||||
completer.completeError(Exception('Phone verification cancelled.'));
|
||||
}
|
||||
_pendingVerification = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PhoneSignInResult> signInWithPhoneCredential({
|
||||
required String verificationId,
|
||||
required String smsCode,
|
||||
}) async {
|
||||
final firebase.PhoneAuthCredential credential =
|
||||
firebase.PhoneAuthProvider.credential(
|
||||
verificationId: verificationId,
|
||||
smsCode: smsCode,
|
||||
);
|
||||
|
||||
final firebase.UserCredential userCredential;
|
||||
try {
|
||||
userCredential = await _auth.signInWithCredential(credential);
|
||||
} on firebase.FirebaseAuthException catch (e) {
|
||||
if (e.code == 'invalid-verification-code') {
|
||||
throw const InvalidCredentialsException(
|
||||
technicalMessage: 'Invalid OTP code entered.',
|
||||
);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
|
||||
final firebase.User? firebaseUser = userCredential.user;
|
||||
if (firebaseUser == null) {
|
||||
throw const SignInFailedException(
|
||||
technicalMessage:
|
||||
'Phone verification failed, no Firebase user received.',
|
||||
);
|
||||
}
|
||||
|
||||
final String? idToken = await firebaseUser.getIdToken();
|
||||
if (idToken == null) {
|
||||
throw const SignInFailedException(
|
||||
technicalMessage: 'Failed to obtain Firebase ID token.',
|
||||
);
|
||||
}
|
||||
|
||||
return PhoneSignInResult(
|
||||
uid: firebaseUser.uid,
|
||||
phoneNumber: firebaseUser.phoneNumber,
|
||||
idToken: idToken,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String> signInWithEmailAndPassword({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
final firebase.UserCredential credential =
|
||||
await _auth.signInWithEmailAndPassword(email: email, password: password);
|
||||
|
||||
final firebase.User? firebaseUser = credential.user;
|
||||
if (firebaseUser == null) {
|
||||
throw const SignInFailedException(
|
||||
technicalMessage: 'Local Firebase sign-in returned null user.',
|
||||
);
|
||||
}
|
||||
|
||||
return firebaseUser.uid;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signOut() async {
|
||||
await _auth.signOut();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getIdToken() async {
|
||||
final firebase.User? user = _auth.currentUser;
|
||||
return user?.getIdToken();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,45 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// Converts a break duration label (e.g. `'MIN_30'`) to its value in minutes.
|
||||
///
|
||||
/// Recognised labels: `MIN_10`, `MIN_15`, `MIN_30`, `MIN_45`, `MIN_60`.
|
||||
/// Returns `0` for any unrecognised value (including `'NO_BREAK'`).
|
||||
int breakMinutesFromLabel(String label) {
|
||||
switch (label) {
|
||||
case 'MIN_10':
|
||||
return 10;
|
||||
case 'MIN_15':
|
||||
return 15;
|
||||
case 'MIN_30':
|
||||
return 30;
|
||||
case 'MIN_45':
|
||||
return 45;
|
||||
case 'MIN_60':
|
||||
return 60;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats a [DateTime] to a `yyyy-MM-dd` date string.
|
||||
///
|
||||
/// Example: `DateTime(2026, 3, 5)` -> `'2026-03-05'`.
|
||||
String formatDateToIso(DateTime date) {
|
||||
return '${date.year.toString().padLeft(4, '0')}-'
|
||||
'${date.month.toString().padLeft(2, '0')}-'
|
||||
'${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// Formats a [DateTime] to `HH:mm` (24-hour) time string.
|
||||
///
|
||||
/// Converts to local time before formatting.
|
||||
/// Example: a UTC DateTime of 14:30 in UTC-5 -> `'09:30'`.
|
||||
String formatTimeHHmm(DateTime dt) {
|
||||
final DateTime local = dt.toLocal();
|
||||
return '${local.hour.toString().padLeft(2, '0')}:'
|
||||
'${local.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// Formats a time string (ISO 8601 or HH:mm) into 12-hour format
|
||||
/// (e.g. "9:00 AM").
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user