You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

349 lines
11 KiB

import 'dart:io';
import 'package:firebase_core/firebase_core.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_storage/firebase_storage.dart';
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import '../../core/exceptions/firebase_exception.dart' show FirebaseServiceException;
import '../local/local_storage_service.dart';
import '../local/models/item.dart';
import 'models/firebase_config.dart';
/// Service for Firebase integration (optional cloud sync, storage, auth, notifications, analytics).
///
/// This service provides:
/// - Cloud Firestore for optional metadata sync and backup
/// - Firebase Storage for optional media storage
/// - Firebase Authentication for user login/logout
/// - Firebase Cloud Messaging for push notifications
/// - Firebase Analytics for optional analytics
///
/// The service is modular and optional - can be enabled/disabled without affecting other modules.
/// When disabled, all methods return safely without throwing errors.
///
/// The service maintains offline-first behavior by syncing with local storage
/// and only using Firebase as an optional cloud backup/sync layer.
class FirebaseService {
/// Firebase configuration (determines which services are enabled).
final FirebaseConfig config;
/// Local storage service for offline-first behavior.
final LocalStorageService localStorage;
/// Firestore instance (null if not enabled).
FirebaseFirestore? _firestore;
/// Firebase Storage instance (null if not enabled).
FirebaseStorage? _storage;
/// Firebase Auth instance (null if not enabled).
firebase_auth.FirebaseAuth? _auth;
/// Firebase Messaging instance (null if not enabled).
FirebaseMessaging? _messaging;
/// Firebase Analytics instance (null if not enabled).
FirebaseAnalytics? _analytics;
/// Whether Firebase has been initialized.
bool _initialized = false;
/// Current user from Firebase Auth (if enabled).
firebase_auth.User? _firebaseUser;
/// Creates a [FirebaseService] instance.
///
/// [config] - Firebase configuration (determines which services are enabled).
/// [localStorage] - Local storage service for offline-first behavior.
FirebaseService({
required this.config,
required this.localStorage,
});
/// Gets the current Firebase Auth user (null if not logged in or auth disabled).
firebase_auth.User? get currentFirebaseUser => _firebaseUser;
/// Checks if Firebase is enabled and initialized.
bool get isEnabled => config.enabled && _initialized;
/// Checks if a user is logged in via Firebase Auth.
bool get isLoggedIn => _auth != null && _firebaseUser != null;
/// Initializes Firebase services based on configuration.
///
/// Must be called before using any Firebase services.
/// If Firebase is disabled, this method does nothing.
///
/// Throws [FirebaseServiceException] if initialization fails.
Future<void> initialize() async {
if (!config.enabled) {
return; // Firebase disabled, nothing to initialize
}
try {
// Initialize Firebase Core (required for all services)
await Firebase.initializeApp();
// Initialize enabled services
if (config.firestoreEnabled) {
_firestore = FirebaseFirestore.instance;
// Enable offline persistence for Firestore
_firestore!.settings = const Settings(
persistenceEnabled: true,
cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,
);
}
if (config.storageEnabled) {
_storage = FirebaseStorage.instance;
}
if (config.authEnabled) {
_auth = firebase_auth.FirebaseAuth.instance;
// Listen for auth state changes
_auth!.authStateChanges().listen((firebase_auth.User? user) {
_firebaseUser = user;
});
_firebaseUser = _auth!.currentUser;
}
if (config.messagingEnabled) {
_messaging = FirebaseMessaging.instance;
// Request notification permissions
await _messaging!.requestPermission(
alert: true,
badge: true,
sound: true,
);
}
if (config.analyticsEnabled) {
_analytics = FirebaseAnalytics.instance;
}
_initialized = true;
} catch (e) {
throw FirebaseServiceException('Failed to initialize Firebase: $e');
}
}
/// Logs in a user with email and password.
///
/// [email] - User email address.
/// [password] - User password.
///
/// Returns the Firebase Auth user.
///
/// Throws [FirebaseServiceException] if auth is disabled or login fails.
Future<firebase_auth.User> loginWithEmailPassword({
required String email,
required String password,
}) async {
if (!config.enabled || !config.authEnabled) {
throw FirebaseServiceException('Firebase Auth is not enabled');
}
if (!_initialized || _auth == null) {
throw FirebaseServiceException(
'Firebase not initialized. Call initialize() first.');
}
try {
final credential = await _auth!.signInWithEmailAndPassword(
email: email,
password: password,
);
_firebaseUser = credential.user;
return _firebaseUser!;
} catch (e) {
throw FirebaseServiceException('Failed to login: $e');
}
}
/// Logs out the current user.
///
/// Throws [FirebaseServiceException] if auth is disabled or logout fails.
Future<void> logout() async {
if (!config.enabled || !config.authEnabled) {
throw FirebaseServiceException('Firebase Auth is not enabled');
}
if (!_initialized || _auth == null) {
throw FirebaseServiceException(
'Firebase not initialized. Call initialize() first.');
}
try {
await _auth!.signOut();
_firebaseUser = null;
} catch (e) {
throw FirebaseServiceException('Failed to logout: $e');
}
}
/// Syncs local items to Firestore (cloud backup).
///
/// [userId] - User ID to associate items with (for multi-user support).
///
/// Throws [FirebaseServiceException] if Firestore is disabled or sync fails.
Future<void> syncItemsToFirestore(String userId) async {
if (!config.enabled || !config.firestoreEnabled) {
throw FirebaseServiceException('Firestore is not enabled');
}
if (!_initialized || _firestore == null) {
throw FirebaseServiceException(
'Firestore not initialized. Call initialize() first.');
}
try {
// Get all local items
final items = await localStorage.getAllItems();
// Batch write to Firestore
final batch = _firestore!.batch();
final collection =
_firestore!.collection('users').doc(userId).collection('items');
for (final item in items) {
final docRef = collection.doc(item.id);
batch.set(docRef, {
'id': item.id,
'data': item.data,
'created_at': item.createdAt,
'updated_at': item.updatedAt,
});
}
await batch.commit();
} catch (e) {
throw FirebaseServiceException('Failed to sync items to Firestore: $e');
}
}
/// Syncs items from Firestore to local storage.
///
/// [userId] - User ID to fetch items for.
///
/// Throws [FirebaseServiceException] if Firestore is disabled or sync fails.
Future<void> syncItemsFromFirestore(String userId) async {
if (!config.enabled || !config.firestoreEnabled) {
throw FirebaseServiceException('Firestore is not enabled');
}
if (!_initialized || _firestore == null) {
throw FirebaseServiceException(
'Firestore not initialized. Call initialize() first.');
}
try {
final snapshot = await _firestore!
.collection('users')
.doc(userId)
.collection('items')
.get();
for (final doc in snapshot.docs) {
final data = doc.data();
final item = Item(
id: data['id'] as String,
data: data['data'] as Map<String, dynamic>,
createdAt: data['created_at'] as int,
updatedAt: data['updated_at'] as int,
);
// Only insert if not already in local storage (avoid duplicates)
final existing = await localStorage.getItem(item.id);
if (existing == null) {
await localStorage.insertItem(item);
}
}
} catch (e) {
throw FirebaseServiceException('Failed to sync items from Firestore: $e');
}
}
/// Uploads a file to Firebase Storage.
///
/// [file] - File to upload.
/// [path] - Storage path (e.g., 'users/userId/media/image.jpg').
///
/// Returns the download URL.
///
/// Throws [FirebaseServiceException] if Storage is disabled or upload fails.
Future<String> uploadFile(File file, String path) async {
if (!config.enabled || !config.storageEnabled) {
throw FirebaseServiceException('Firebase Storage is not enabled');
}
if (!_initialized || _storage == null) {
throw FirebaseServiceException(
'Firebase Storage not initialized. Call initialize() first.');
}
try {
final ref = _storage!.ref().child(path);
await ref.putFile(file);
return await ref.getDownloadURL();
} catch (e) {
throw FirebaseServiceException('Failed to upload file: $e');
}
}
/// Gets the FCM token for push notifications.
///
/// Returns the FCM token, or null if messaging is disabled.
Future<String?> getFcmToken() async {
if (!config.enabled || !config.messagingEnabled || _messaging == null) {
return null;
}
try {
return await _messaging!.getToken();
} catch (e) {
return null;
}
}
/// Logs an event to Firebase Analytics.
///
/// [eventName] - Name of the event.
/// [parameters] - Optional event parameters.
///
/// Does nothing if Analytics is disabled.
Future<void> logEvent(String eventName,
{Map<String, dynamic>? parameters}) async {
if (!config.enabled || !config.analyticsEnabled || _analytics == null) {
return;
}
try {
// Convert Map<String, dynamic> to Map<String, Object> for Firebase Analytics
Map<String, Object>? analyticsParams;
if (parameters != null) {
analyticsParams =
parameters.map((key, value) => MapEntry(key, value as Object));
}
await _analytics!.logEvent(
name: eventName,
parameters: analyticsParams,
);
} catch (e) {
// Silently fail - analytics failures shouldn't break the app
}
}
/// Disposes of Firebase resources.
///
/// Should be called when the service is no longer needed.
Future<void> dispose() async {
if (_auth != null) {
await _auth!.signOut();
}
_firebaseUser = null;
_initialized = false;
}
}

Powered by TurnKey Linux.