feat: add entities for staff personal info, reports, shifts, and user sessions

- Implemented StaffPersonalInfo entity for staff profile data.
- Created ReportSummary entity for summarizing report metrics.
- Added SpendReport and SpendDataPoint entities for spend reporting.
- Introduced AssignedShift, CancelledShift, CompletedShift, OpenShift, PendingAssignment, ShiftDetail, TodayShift entities for shift management.
- Developed ClientSession and StaffSession entities for user session management.
This commit is contained in:
Achintha Isuru
2026-03-16 15:59:22 -04:00
parent 641dfac73d
commit 4834266986
159 changed files with 6857 additions and 3937 deletions

View File

@@ -16,8 +16,13 @@ export 'src/routing/routing.dart';
export 'src/services/api_service/api_service.dart';
export 'src/services/api_service/dio_client.dart';
// API Mixins
export 'src/services/api_service/mixins/api_error_handler.dart';
export 'src/services/api_service/mixins/session_handler_mixin.dart';
// Core API Services
export 'src/services/api_service/core_api_services/core_api_endpoints.dart';
export 'src/services/api_service/core_api_services/v2_api_endpoints.dart';
export 'src/services/api_service/core_api_services/file_upload/file_upload_service.dart';
export 'src/services/api_service/core_api_services/file_upload/file_upload_response.dart';
export 'src/services/api_service/core_api_services/signed_url/signed_url_service.dart';

View File

@@ -13,4 +13,10 @@ class AppConfig {
static const String coreApiBaseUrl = String.fromEnvironment(
'CORE_API_BASE_URL',
);
/// The base URL for the V2 Unified API gateway.
static const String v2ApiBaseUrl = String.fromEnvironment(
'V2_API_BASE_URL',
defaultValue: 'https://krow-api-v2-933560802882.us-central1.run.app',
);
}

View File

@@ -86,6 +86,25 @@ class ApiService implements BaseApiService {
}
}
/// Performs a DELETE request to the specified [endpoint].
@override
Future<ApiResponse> delete(
String endpoint, {
dynamic data,
Map<String, dynamic>? params,
}) async {
try {
final Response<dynamic> response = await _dio.delete<dynamic>(
endpoint,
data: data,
queryParameters: params,
);
return _handleResponse(response);
} on DioException catch (e) {
return _handleError(e);
}
}
/// Extracts [ApiResponse] from a successful [Response].
ApiResponse _handleResponse(Response<dynamic> response) {
return ApiResponse(
@@ -96,6 +115,9 @@ class ApiService implements BaseApiService {
}
/// Extracts [ApiResponse] from a [DioException].
///
/// Supports both legacy error format and V2 API error envelope
/// (`{ code, message, details, requestId }`).
ApiResponse _handleError(DioException e) {
if (e.response?.data is Map<String, dynamic>) {
final Map<String, dynamic> body =
@@ -106,7 +128,7 @@ class ApiService implements BaseApiService {
e.response?.statusCode?.toString() ??
'error',
message: body['message']?.toString() ?? e.message ?? 'Error occurred',
data: body['data'],
data: body['data'] ?? body['details'],
errors: _parseErrors(body['errors']),
);
}

View File

@@ -0,0 +1,340 @@
import '../../../config/app_config.dart';
/// Constants for V2 Unified API endpoints.
///
/// All mobile read/write operations go through the V2 gateway which proxies
/// to the internal Query API and Command API services.
class V2ApiEndpoints {
V2ApiEndpoints._();
/// The base URL for the V2 Unified API gateway.
static const String baseUrl = AppConfig.v2ApiBaseUrl;
// ── Auth ──────────────────────────────────────────────────────────────
/// Client email/password sign-in.
static const String clientSignIn = '$baseUrl/auth/client/sign-in';
/// Client business registration.
static const String clientSignUp = '$baseUrl/auth/client/sign-up';
/// Client sign-out.
static const String clientSignOut = '$baseUrl/auth/client/sign-out';
/// Start staff phone verification (SMS).
static const String staffPhoneStart = '$baseUrl/auth/staff/phone/start';
/// Complete staff phone verification.
static const String staffPhoneVerify = '$baseUrl/auth/staff/phone/verify';
/// Generic sign-out.
static const String signOut = '$baseUrl/auth/sign-out';
/// Get current session data.
static const String session = '$baseUrl/auth/session';
// ── Staff Read ────────────────────────────────────────────────────────
/// Staff session data.
static const String staffSession = '$baseUrl/staff/session';
/// Staff dashboard overview.
static const String staffDashboard = '$baseUrl/staff/dashboard';
/// Staff profile completion status.
static const String staffProfileCompletion =
'$baseUrl/staff/profile-completion';
/// Staff availability schedule.
static const String staffAvailability = '$baseUrl/staff/availability';
/// Today's shifts for clock-in.
static const String staffClockInShiftsToday =
'$baseUrl/staff/clock-in/shifts/today';
/// Current clock-in status.
static const String staffClockInStatus = '$baseUrl/staff/clock-in/status';
/// Payments summary.
static const String staffPaymentsSummary = '$baseUrl/staff/payments/summary';
/// Payments history.
static const String staffPaymentsHistory = '$baseUrl/staff/payments/history';
/// Payments chart data.
static const String staffPaymentsChart = '$baseUrl/staff/payments/chart';
/// Assigned shifts.
static const String staffShiftsAssigned = '$baseUrl/staff/shifts/assigned';
/// Open shifts available to apply.
static const String staffShiftsOpen = '$baseUrl/staff/shifts/open';
/// Pending shift assignments.
static const String staffShiftsPending = '$baseUrl/staff/shifts/pending';
/// Cancelled shifts.
static const String staffShiftsCancelled = '$baseUrl/staff/shifts/cancelled';
/// Completed shifts.
static const String staffShiftsCompleted = '$baseUrl/staff/shifts/completed';
/// Shift details by ID.
static String staffShiftDetails(String shiftId) =>
'$baseUrl/staff/shifts/$shiftId';
/// Staff profile sections overview.
static const String staffProfileSections = '$baseUrl/staff/profile/sections';
/// Personal info.
static const String staffPersonalInfo = '$baseUrl/staff/profile/personal-info';
/// Industries/experience.
static const String staffIndustries = '$baseUrl/staff/profile/industries';
/// Skills.
static const String staffSkills = '$baseUrl/staff/profile/skills';
/// Documents.
static const String staffDocuments = '$baseUrl/staff/profile/documents';
/// Attire items.
static const String staffAttire = '$baseUrl/staff/profile/attire';
/// Tax forms.
static const String staffTaxForms = '$baseUrl/staff/profile/tax-forms';
/// Emergency contacts.
static const String staffEmergencyContacts =
'$baseUrl/staff/profile/emergency-contacts';
/// Certificates.
static const String staffCertificates = '$baseUrl/staff/profile/certificates';
/// Bank accounts.
static const String staffBankAccounts = '$baseUrl/staff/profile/bank-accounts';
/// Benefits.
static const String staffBenefits = '$baseUrl/staff/profile/benefits';
/// Time card.
static const String staffTimeCard = '$baseUrl/staff/profile/time-card';
/// Privacy settings.
static const String staffPrivacy = '$baseUrl/staff/profile/privacy';
/// FAQs.
static const String staffFaqs = '$baseUrl/staff/faqs';
/// FAQs search.
static const String staffFaqsSearch = '$baseUrl/staff/faqs/search';
// ── Staff Write ───────────────────────────────────────────────────────
/// Staff profile setup.
static const String staffProfileSetup = '$baseUrl/staff/profile/setup';
/// Clock in.
static const String staffClockIn = '$baseUrl/staff/clock-in';
/// Clock out.
static const String staffClockOut = '$baseUrl/staff/clock-out';
/// Quick-set availability.
static const String staffAvailabilityQuickSet =
'$baseUrl/staff/availability/quick-set';
/// Apply for a shift.
static String staffShiftApply(String shiftId) =>
'$baseUrl/staff/shifts/$shiftId/apply';
/// Accept a shift.
static String staffShiftAccept(String shiftId) =>
'$baseUrl/staff/shifts/$shiftId/accept';
/// Decline a shift.
static String staffShiftDecline(String shiftId) =>
'$baseUrl/staff/shifts/$shiftId/decline';
/// Request a shift swap.
static String staffShiftRequestSwap(String shiftId) =>
'$baseUrl/staff/shifts/$shiftId/request-swap';
/// Update emergency contact by ID.
static String staffEmergencyContactUpdate(String contactId) =>
'$baseUrl/staff/profile/emergency-contacts/$contactId';
/// Update tax form by type.
static String staffTaxFormUpdate(String formType) =>
'$baseUrl/staff/profile/tax-forms/$formType';
/// Submit tax form by type.
static String staffTaxFormSubmit(String formType) =>
'$baseUrl/staff/profile/tax-forms/$formType/submit';
/// Upload staff profile photo.
static const String staffProfilePhoto = '$baseUrl/staff/profile/photo';
/// Upload document by ID.
static String staffDocumentUpload(String documentId) =>
'$baseUrl/staff/profile/documents/$documentId/upload';
/// Upload attire by ID.
static String staffAttireUpload(String documentId) =>
'$baseUrl/staff/profile/attire/$documentId/upload';
/// Delete certificate by ID.
static String staffCertificateDelete(String certificateId) =>
'$baseUrl/staff/profile/certificates/$certificateId';
// ── Client Read ───────────────────────────────────────────────────────
/// Client session data.
static const String clientSession = '$baseUrl/client/session';
/// Client dashboard.
static const String clientDashboard = '$baseUrl/client/dashboard';
/// Client reorders.
static const String clientReorders = '$baseUrl/client/reorders';
/// Billing accounts.
static const String clientBillingAccounts = '$baseUrl/client/billing/accounts';
/// Pending invoices.
static const String clientBillingInvoicesPending =
'$baseUrl/client/billing/invoices/pending';
/// Invoice history.
static const String clientBillingInvoicesHistory =
'$baseUrl/client/billing/invoices/history';
/// Current bill.
static const String clientBillingCurrentBill =
'$baseUrl/client/billing/current-bill';
/// Savings data.
static const String clientBillingSavings = '$baseUrl/client/billing/savings';
/// Spend breakdown.
static const String clientBillingSpendBreakdown =
'$baseUrl/client/billing/spend-breakdown';
/// Coverage overview.
static const String clientCoverage = '$baseUrl/client/coverage';
/// Coverage stats.
static const String clientCoverageStats = '$baseUrl/client/coverage/stats';
/// Core team.
static const String clientCoverageCoreTeam =
'$baseUrl/client/coverage/core-team';
/// Hubs list.
static const String clientHubs = '$baseUrl/client/hubs';
/// Cost centers.
static const String clientCostCenters = '$baseUrl/client/cost-centers';
/// Vendors.
static const String clientVendors = '$baseUrl/client/vendors';
/// Vendor roles by ID.
static String clientVendorRoles(String vendorId) =>
'$baseUrl/client/vendors/$vendorId/roles';
/// Hub managers by ID.
static String clientHubManagers(String hubId) =>
'$baseUrl/client/hubs/$hubId/managers';
/// Team members.
static const String clientTeamMembers = '$baseUrl/client/team-members';
/// View orders.
static const String clientOrdersView = '$baseUrl/client/orders/view';
/// Order reorder preview.
static String clientOrderReorderPreview(String orderId) =>
'$baseUrl/client/orders/$orderId/reorder-preview';
/// Reports summary.
static const String clientReportsSummary = '$baseUrl/client/reports/summary';
/// Daily ops report.
static const String clientReportsDailyOps =
'$baseUrl/client/reports/daily-ops';
/// Spend report.
static const String clientReportsSpend = '$baseUrl/client/reports/spend';
/// Coverage report.
static const String clientReportsCoverage =
'$baseUrl/client/reports/coverage';
/// Forecast report.
static const String clientReportsForecast =
'$baseUrl/client/reports/forecast';
/// Performance report.
static const String clientReportsPerformance =
'$baseUrl/client/reports/performance';
/// No-show report.
static const String clientReportsNoShow = '$baseUrl/client/reports/no-show';
// ── Client Write ──────────────────────────────────────────────────────
/// Create one-time order.
static const String clientOrdersOneTime = '$baseUrl/client/orders/one-time';
/// Create recurring order.
static const String clientOrdersRecurring =
'$baseUrl/client/orders/recurring';
/// Create permanent order.
static const String clientOrdersPermanent =
'$baseUrl/client/orders/permanent';
/// Edit order by ID.
static String clientOrderEdit(String orderId) =>
'$baseUrl/client/orders/$orderId/edit';
/// Cancel order by ID.
static String clientOrderCancel(String orderId) =>
'$baseUrl/client/orders/$orderId/cancel';
/// Create hub.
static const String clientHubCreate = '$baseUrl/client/hubs';
/// Update hub by ID.
static String clientHubUpdate(String hubId) =>
'$baseUrl/client/hubs/$hubId';
/// Delete hub by ID.
static String clientHubDelete(String hubId) =>
'$baseUrl/client/hubs/$hubId';
/// Assign NFC to hub.
static String clientHubAssignNfc(String hubId) =>
'$baseUrl/client/hubs/$hubId/assign-nfc';
/// Assign managers to hub.
static String clientHubAssignManagers(String hubId) =>
'$baseUrl/client/hubs/$hubId/managers';
/// Approve invoice.
static String clientInvoiceApprove(String invoiceId) =>
'$baseUrl/client/billing/invoices/$invoiceId/approve';
/// Dispute invoice.
static String clientInvoiceDispute(String invoiceId) =>
'$baseUrl/client/billing/invoices/$invoiceId/dispute';
/// Submit coverage review.
static const String clientCoverageReviews =
'$baseUrl/client/coverage/reviews';
/// Cancel late worker assignment.
static String clientCoverageCancelLateWorker(String assignmentId) =>
'$baseUrl/client/coverage/late-workers/$assignmentId/cancel';
}

View File

@@ -1,8 +1,9 @@
import 'package:dio/dio.dart';
import 'package:krow_core/src/services/api_service/inspectors/auth_interceptor.dart';
import 'package:krow_core/src/services/api_service/inspectors/idempotency_interceptor.dart';
/// A custom Dio client for the KROW project that includes basic configuration
/// and an [AuthInterceptor].
/// A custom Dio client for the KROW project that includes basic configuration,
/// [AuthInterceptor], and [IdempotencyInterceptor].
class DioClient extends DioMixin implements Dio {
DioClient([BaseOptions? baseOptions]) {
options =
@@ -18,10 +19,11 @@ class DioClient extends DioMixin implements Dio {
// Add interceptors
interceptors.addAll(<Interceptor>[
AuthInterceptor(),
IdempotencyInterceptor(),
LogInterceptor(
requestBody: true,
responseBody: true,
), // Added for better debugging
),
]);
}
}

View File

@@ -0,0 +1,24 @@
import 'package:dio/dio.dart';
import 'package:uuid/uuid.dart';
/// A Dio interceptor that adds an `Idempotency-Key` header to write requests.
///
/// The V2 API requires an idempotency key for all POST, PUT, and DELETE
/// requests to prevent duplicate operations. A unique UUID v4 is generated
/// per request automatically.
class IdempotencyInterceptor extends Interceptor {
/// The UUID generator instance.
static const Uuid _uuid = Uuid();
@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
final String method = options.method.toUpperCase();
if (method == 'POST' || method == 'PUT' || method == 'DELETE') {
options.headers['Idempotency-Key'] = _uuid.v4();
}
handler.next(options);
}
}

View File

@@ -0,0 +1,135 @@
import 'dart:async';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
/// Mixin to handle API layer errors and map them to domain exceptions.
///
/// Use this in repository implementations to wrap [ApiService] calls.
/// It catches [DioException], [SocketException], etc., and throws
/// the appropriate [AppException] subclass.
mixin ApiErrorHandler {
/// Executes a Future and maps low-level exceptions to [AppException].
///
/// [timeout] defaults to 30 seconds.
Future<T> executeProtected<T>(
Future<T> Function() action, {
Duration timeout = const Duration(seconds: 30),
}) async {
try {
return await action().timeout(timeout);
} on TimeoutException {
debugPrint(
'ApiErrorHandler: Request timed out after ${timeout.inSeconds}s',
);
throw ServiceUnavailableException(
technicalMessage: 'Request timed out after ${timeout.inSeconds}s',
);
} on DioException catch (e) {
throw _mapDioException(e);
} on SocketException catch (e) {
throw NetworkException(
technicalMessage: 'SocketException: ${e.message}',
);
} catch (e) {
// If it's already an AppException, rethrow it.
if (e is AppException) rethrow;
final String errorStr = e.toString().toLowerCase();
if (_isNetworkRelated(errorStr)) {
debugPrint('ApiErrorHandler: Network-related error: $e');
throw NetworkException(technicalMessage: e.toString());
}
debugPrint('ApiErrorHandler: Unhandled exception caught: $e');
throw UnknownException(technicalMessage: e.toString());
}
}
/// Maps a [DioException] to the appropriate [AppException].
AppException _mapDioException(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
debugPrint('ApiErrorHandler: Dio timeout: ${e.type}');
return ServiceUnavailableException(
technicalMessage: 'Dio ${e.type}: ${e.message}',
);
case DioExceptionType.connectionError:
debugPrint('ApiErrorHandler: Connection error: ${e.message}');
return NetworkException(
technicalMessage: 'Connection error: ${e.message}',
);
case DioExceptionType.badResponse:
final int? statusCode = e.response?.statusCode;
final String body = e.response?.data?.toString() ?? '';
debugPrint(
'ApiErrorHandler: Bad response $statusCode: $body',
);
if (statusCode == 401 || statusCode == 403) {
return NotAuthenticatedException(
technicalMessage: 'HTTP $statusCode: $body',
);
}
if (statusCode == 404) {
return ServerException(
technicalMessage: 'HTTP 404: Not found — $body',
);
}
if (statusCode == 429) {
return ServiceUnavailableException(
technicalMessage: 'Rate limited (429): $body',
);
}
if (statusCode != null && statusCode >= 500) {
return ServiceUnavailableException(
technicalMessage: 'HTTP $statusCode: $body',
);
}
return ServerException(
technicalMessage: 'HTTP $statusCode: $body',
);
case DioExceptionType.cancel:
return UnknownException(
technicalMessage: 'Request cancelled',
);
case DioExceptionType.badCertificate:
return NetworkException(
technicalMessage: 'Bad certificate: ${e.message}',
);
case DioExceptionType.unknown:
if (e.error is SocketException) {
return NetworkException(
technicalMessage: 'Socket error: ${e.error}',
);
}
return UnknownException(
technicalMessage: 'Unknown Dio error: ${e.message}',
);
}
}
/// Checks if an error string is network-related.
bool _isNetworkRelated(String errorStr) {
return errorStr.contains('socketexception') ||
errorStr.contains('network') ||
errorStr.contains('offline') ||
errorStr.contains('connection failed') ||
errorStr.contains('unavailable') ||
errorStr.contains('handshake') ||
errorStr.contains('clientexception') ||
errorStr.contains('failed host lookup') ||
errorStr.contains('connection error') ||
errorStr.contains('terminated') ||
errorStr.contains('connectexception');
}
}

View File

@@ -0,0 +1,244 @@
import 'dart:async';
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
import 'package:flutter/cupertino.dart';
/// Enum representing the current session state.
enum SessionStateType { loading, authenticated, unauthenticated, error }
/// Data class for session state.
class SessionState {
/// Creates a [SessionState].
SessionState({required this.type, this.userId, this.errorMessage});
/// Creates a loading state.
factory SessionState.loading() =>
SessionState(type: SessionStateType.loading);
/// Creates an authenticated state.
factory SessionState.authenticated({required String userId}) =>
SessionState(type: SessionStateType.authenticated, userId: userId);
/// Creates an unauthenticated state.
factory SessionState.unauthenticated() =>
SessionState(type: SessionStateType.unauthenticated);
/// Creates an error state.
factory SessionState.error(String message) =>
SessionState(type: SessionStateType.error, errorMessage: message);
/// The type of session state.
final SessionStateType type;
/// The current user ID (if authenticated).
final String? userId;
/// Error message (if error occurred).
final String? errorMessage;
@override
String toString() =>
'SessionState(type: $type, userId: $userId, error: $errorMessage)';
}
/// Mixin for handling Firebase Auth session management, token refresh,
/// and state emissions.
///
/// Implementors must provide [auth] and [fetchUserRole]. The role fetch
/// should call `GET /auth/session` via [ApiService] instead of querying
/// Data Connect directly.
mixin SessionHandlerMixin {
/// Stream controller for session state changes.
final StreamController<SessionState> _sessionStateController =
StreamController<SessionState>.broadcast();
/// Last emitted session state (for late subscribers).
SessionState? _lastSessionState;
/// Public stream for listening to session state changes.
/// Late subscribers will immediately receive the last emitted state.
Stream<SessionState> get onSessionStateChanged {
return _createStreamWithLastState();
}
/// Creates a stream that emits the last state before subscribing to new events.
Stream<SessionState> _createStreamWithLastState() async* {
if (_lastSessionState != null) {
yield _lastSessionState!;
}
yield* _sessionStateController.stream;
}
/// Last token refresh timestamp to avoid excessive checks.
DateTime? _lastTokenRefreshTime;
/// Subscription to auth state changes.
StreamSubscription<firebase_auth.User?>? _authStateSubscription;
/// Minimum interval between token refresh checks.
static const Duration _minRefreshCheckInterval = Duration(seconds: 2);
/// Time before token expiry to trigger a refresh.
static const Duration _refreshThreshold = Duration(minutes: 5);
/// Firebase Auth instance (to be provided by implementing class).
firebase_auth.FirebaseAuth get auth;
/// List of allowed roles for this app (set during initialization).
List<String> _allowedRoles = <String>[];
/// Initialize the auth state listener (call once on app startup).
void initializeAuthListener({
List<String> allowedRoles = const <String>[],
}) {
_allowedRoles = allowedRoles;
_authStateSubscription?.cancel();
_authStateSubscription = auth.authStateChanges().listen(
(firebase_auth.User? user) async {
if (user == null) {
handleSignOut();
} else {
await _handleSignIn(user);
}
},
onError: (Object error) {
_emitSessionState(SessionState.error(error.toString()));
},
);
}
/// Validates if user has one of the allowed roles.
Future<bool> validateUserRole(
String userId,
List<String> allowedRoles,
) async {
try {
final String? userRole = await fetchUserRole(userId);
return userRole != null && allowedRoles.contains(userRole);
} catch (e) {
debugPrint('Failed to validate user role: $e');
return false;
}
}
/// Fetches user role from the backend.
///
/// Implementors should call `GET /auth/session` via [ApiService] and
/// extract the role from the response.
Future<String?> fetchUserRole(String userId);
/// Ensures the Firebase auth token is valid and refreshes if needed.
/// Retries up to 3 times with exponential backoff before emitting error.
Future<void> ensureSessionValid() async {
final firebase_auth.User? user = auth.currentUser;
if (user == null) return;
final DateTime now = DateTime.now();
if (_lastTokenRefreshTime != null) {
final Duration timeSinceLastCheck = now.difference(
_lastTokenRefreshTime!,
);
if (timeSinceLastCheck < _minRefreshCheckInterval) {
return;
}
}
const int maxRetries = 3;
int retryCount = 0;
while (retryCount < maxRetries) {
try {
final firebase_auth.IdTokenResult idToken =
await user.getIdTokenResult();
final DateTime? expiryTime = idToken.expirationTime;
if (expiryTime == null) return;
final Duration timeUntilExpiry = expiryTime.difference(now);
if (timeUntilExpiry <= _refreshThreshold) {
await user.getIdTokenResult();
}
_lastTokenRefreshTime = now;
return;
} catch (e) {
retryCount++;
debugPrint(
'Token validation error (attempt $retryCount/$maxRetries): $e',
);
if (retryCount >= maxRetries) {
_emitSessionState(
SessionState.error(
'Token validation failed after $maxRetries attempts: $e',
),
);
return;
}
final Duration backoffDuration = Duration(
seconds: 1 << (retryCount - 1),
);
debugPrint(
'Retrying token validation in ${backoffDuration.inSeconds}s',
);
await Future<void>.delayed(backoffDuration);
}
}
}
/// Handle user sign-in event.
Future<void> _handleSignIn(firebase_auth.User user) async {
try {
_emitSessionState(SessionState.loading());
if (_allowedRoles.isNotEmpty) {
final String? userRole = await fetchUserRole(user.uid);
if (userRole == null) {
_emitSessionState(SessionState.unauthenticated());
return;
}
if (!_allowedRoles.contains(userRole)) {
await auth.signOut();
_emitSessionState(SessionState.unauthenticated());
return;
}
}
final firebase_auth.IdTokenResult idToken =
await user.getIdTokenResult();
if (idToken.expirationTime != null &&
DateTime.now().difference(idToken.expirationTime!) <
const Duration(minutes: 5)) {
await user.getIdTokenResult();
}
_emitSessionState(SessionState.authenticated(userId: user.uid));
} catch (e) {
_emitSessionState(SessionState.error(e.toString()));
}
}
/// Handle user sign-out event.
void handleSignOut() {
_emitSessionState(SessionState.unauthenticated());
}
/// Emit session state update.
void _emitSessionState(SessionState state) {
_lastSessionState = state;
if (!_sessionStateController.isClosed) {
_sessionStateController.add(state);
}
}
/// Dispose session handler resources.
Future<void> disposeSessionHandler() async {
await _authStateSubscription?.cancel();
await _sessionStateController.close();
}
}

View File

@@ -32,3 +32,4 @@ dependencies:
flutter_local_notifications: ^21.0.0
shared_preferences: ^2.5.4
workmanager: ^0.9.0+3
uuid: ^4.5.1