diff --git a/FIREBASE_SETUP.md b/FIREBASE_SETUP.md new file mode 100644 index 0000000..4ef322a --- /dev/null +++ b/FIREBASE_SETUP.md @@ -0,0 +1,120 @@ +# Firebase Setup Guide for Sage + +## Step 1: Create Firebase Project + +1. Go to [Firebase Console](https://console.firebase.google.com/) +2. Click "Add project" +3. Enter project name: `sage-kitchen-management` +4. Disable Google Analytics (optional for this app) +5. Click "Create project" + +## Step 2: Add Android App + +1. In Firebase Console, click the Android icon to add an Android app +2. Enter package name: `com.example.sage` (must match AndroidManifest.xml) +3. App nickname: `Sage` (optional) +4. Debug signing certificate SHA-1: (optional, skip for now) +5. Click "Register app" + +## Step 3: Download Configuration File + +1. Download the `google-services.json` file +2. Place it in: `android/app/google-services.json` + +## Step 4: Set up Firestore Database + +1. In Firebase Console, go to "Build" → "Firestore Database" +2. Click "Create database" +3. Choose "Start in test mode" for development +4. Select a Firestore location (e.g., `us-central`) +5. Click "Enable" + +### Security Rules (update after testing) + +For development/testing, use test mode rules: +``` +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read, write: if request.time < timestamp.date(2025, 12, 31); + } + } +} +``` + +For production, update to: +``` +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + // Allow anyone to read household data by code + match /households/{householdId} { + allow read: if true; + allow create: if true; + allow update: if true; + allow delete: if request.auth != null; + + // Allow household members to manage items + match /items/{itemId} { + allow read: if true; + allow write: if true; + } + } + } +} +``` + +## Step 5: Update Android Build Files (Already Done) + +The following files need to be updated (will be done automatically): + +1. `android/build.gradle` - Add Google Services plugin +2. `android/app/build.gradle` - Apply Google Services plugin + +## Step 6: Initialize Firebase in App + +The app will automatically initialize Firebase on startup. + +## Firestore Data Structure + +``` +households (collection) + └── {householdCode} (document) + ├── id: string + ├── name: string + ├── ownerName: string + ├── createdAt: string (ISO 8601) + └── members: array + └── items (subcollection) + └── {itemKey} (document) + ├── name: string + ├── barcode: string? + ├── quantity: number + ├── unit: string? + ├── purchaseDate: string (ISO 8601) + ├── expirationDate: string (ISO 8601) + ├── locationIndex: number + ├── category: string? + ├── photoUrl: string? + ├── notes: string? + ├── userId: string? + ├── householdId: string + ├── lastModified: string (ISO 8601) + └── syncedToCloud: boolean +``` + +## Testing + +1. Create a household on Device A +2. Note the 6-character code +3. Join the household from Device B using the code +4. Add items on Device A → should appear on Device B +5. Add items on Device B → should appear on Device A + +## Troubleshooting + +- **"google-services.json not found"**: Make sure file is in `android/app/` directory +- **Build errors**: Run `flutter clean && flutter pub get` +- **Permission denied**: Check Firestore security rules in Firebase Console +- **Items not syncing**: Check internet connection and Firebase Console logs diff --git a/android/app/README_FIREBASE.md b/android/app/README_FIREBASE.md new file mode 100644 index 0000000..f8e14a8 --- /dev/null +++ b/android/app/README_FIREBASE.md @@ -0,0 +1,21 @@ +# Firebase Configuration Required + +## ⚠️ IMPORTANT: Replace google-services.json + +The current `google-services.json` file is a **PLACEHOLDER** and will **NOT** work. + +### Steps to get your real google-services.json: + +1. Follow the instructions in `/FIREBASE_SETUP.md` in the project root +2. Download the real `google-services.json` from Firebase Console +3. Replace the file in this directory: `android/app/google-services.json` + +### Quick Link: +[Firebase Console](https://console.firebase.google.com/) + +### Package Name (must match): +``` +com.sage.sage +``` + +Without the real Firebase configuration file, household sharing will not work across devices! diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index b1b0081..5b98b50 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -3,6 +3,7 @@ plugins { id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") + id("com.google.gms.google-services") } android { @@ -24,7 +25,7 @@ android { applicationId = "com.sage.sage" // You can update the following values to match your application needs. // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion + minSdk = flutter.minSdkVersion // Firebase requires minSdk 21 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..8813823 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,29 @@ +{ + "project_info": { + "project_number": "PLACEHOLDER", + "project_id": "sage-kitchen-management", + "storage_bucket": "sage-kitchen-management.appspot.com" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:PLACEHOLDER:android:PLACEHOLDER", + "android_client_info": { + "package_name": "com.sage.sage" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "PLACEHOLDER_API_KEY" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} diff --git a/android/build.gradle.kts b/android/build.gradle.kts index dbee657..5813b6d 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -1,3 +1,13 @@ +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.google.gms:google-services:4.4.2") + } +} + allprojects { repositories { google() diff --git a/lib/features/household/services/firebase_household_service.dart b/lib/features/household/services/firebase_household_service.dart new file mode 100644 index 0000000..ef6e084 --- /dev/null +++ b/lib/features/household/services/firebase_household_service.dart @@ -0,0 +1,199 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import '../../settings/models/household.dart'; +import '../../../features/inventory/models/food_item.dart'; + +/// Service for managing household data in Firestore +class FirebaseHouseholdService { + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + + /// Create a new household in Firestore + Future createHousehold(String name, String ownerName) async { + final household = Household( + id: Household.generateCode(), + name: name, + ownerName: ownerName, + createdAt: DateTime.now(), + members: [ownerName], + ); + + await _firestore.collection('households').doc(household.id).set({ + 'id': household.id, + 'name': household.name, + 'ownerName': household.ownerName, + 'createdAt': household.createdAt.toIso8601String(), + 'members': household.members, + }); + + return household; + } + + /// Get household by code from Firestore + Future getHousehold(String code) async { + try { + final doc = await _firestore.collection('households').doc(code).get(); + + if (!doc.exists) { + return null; + } + + final data = doc.data()!; + final household = Household( + id: data['id'] as String, + name: data['name'] as String, + ownerName: data['ownerName'] as String, + createdAt: DateTime.parse(data['createdAt'] as String), + members: List.from(data['members'] as List), + ); + + return household; + } catch (e) { + return null; + } + } + + /// Join a household (add member) + Future joinHousehold(String code, String memberName) async { + try { + final docRef = _firestore.collection('households').doc(code); + final doc = await docRef.get(); + + if (!doc.exists) { + return false; + } + + final members = List.from(doc.data()!['members'] as List); + if (!members.contains(memberName)) { + members.add(memberName); + await docRef.update({'members': members}); + } + + return true; + } catch (e) { + return false; + } + } + + /// Leave a household (remove member) + Future leaveHousehold(String code, String memberName) async { + final docRef = _firestore.collection('households').doc(code); + final doc = await docRef.get(); + + if (doc.exists) { + final members = List.from(doc.data()!['members'] as List); + members.remove(memberName); + + if (members.isEmpty) { + // Delete household if no members left + await docRef.delete(); + } else { + await docRef.update({'members': members}); + } + } + } + + /// Add food item to household in Firestore + Future addFoodItem(String householdId, FoodItem item, String itemKey) async { + await _firestore + .collection('households') + .doc(householdId) + .collection('items') + .doc(itemKey.toString()) + .set({ + 'name': item.name, + 'barcode': item.barcode, + 'quantity': item.quantity, + 'unit': item.unit, + 'purchaseDate': item.purchaseDate.toIso8601String(), + 'expirationDate': item.expirationDate.toIso8601String(), + 'locationIndex': item.locationIndex, + 'category': item.category, + 'photoUrl': item.photoUrl, + 'notes': item.notes, + 'userId': item.userId, + 'householdId': item.householdId, + 'lastModified': item.lastModified?.toIso8601String(), + 'syncedToCloud': true, + }); + } + + /// Update food item in Firestore + Future updateFoodItem(String householdId, FoodItem item, String itemKey) async { + await _firestore + .collection('households') + .doc(householdId) + .collection('items') + .doc(itemKey.toString()) + .update({ + 'name': item.name, + 'barcode': item.barcode, + 'quantity': item.quantity, + 'unit': item.unit, + 'purchaseDate': item.purchaseDate.toIso8601String(), + 'expirationDate': item.expirationDate.toIso8601String(), + 'locationIndex': item.locationIndex, + 'category': item.category, + 'photoUrl': item.photoUrl, + 'notes': item.notes, + 'lastModified': DateTime.now().toIso8601String(), + }); + } + + /// Delete food item from Firestore + Future deleteFoodItem(String householdId, String itemKey) async { + await _firestore + .collection('households') + .doc(householdId) + .collection('items') + .doc(itemKey.toString()) + .delete(); + } + + /// Stream household items from Firestore + Stream>> streamHouseholdItems(String householdId) { + return _firestore + .collection('households') + .doc(householdId) + .collection('items') + .snapshots() + .map((snapshot) { + return snapshot.docs.map((doc) { + final data = doc.data(); + data['firestoreId'] = doc.id; + return data; + }).toList(); + }); + } + + /// Sync local items to Firestore + Future syncItemsToFirestore(String householdId, List items) async { + final batch = _firestore.batch(); + final collection = _firestore + .collection('households') + .doc(householdId) + .collection('items'); + + for (final item in items) { + if (item.householdId == householdId && item.key != null) { + final docRef = collection.doc(item.key.toString()); + batch.set(docRef, { + 'name': item.name, + 'barcode': item.barcode, + 'quantity': item.quantity, + 'unit': item.unit, + 'purchaseDate': item.purchaseDate.toIso8601String(), + 'expirationDate': item.expirationDate.toIso8601String(), + 'locationIndex': item.locationIndex, + 'category': item.category, + 'photoUrl': item.photoUrl, + 'notes': item.notes, + 'userId': item.userId, + 'householdId': item.householdId, + 'lastModified': item.lastModified?.toIso8601String(), + 'syncedToCloud': true, + }); + } + } + + await batch.commit(); + } +} diff --git a/lib/features/settings/screens/household_screen.dart b/lib/features/settings/screens/household_screen.dart index 7e6be84..8330508 100644 --- a/lib/features/settings/screens/household_screen.dart +++ b/lib/features/settings/screens/household_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../../../core/constants/colors.dart'; import '../../../data/local/hive_database.dart'; +import '../../household/services/firebase_household_service.dart'; import '../models/app_settings.dart'; import '../models/household.dart'; @@ -13,6 +14,7 @@ class HouseholdScreen extends StatefulWidget { } class _HouseholdScreenState extends State { + final _firebaseService = FirebaseHouseholdService(); AppSettings? _settings; Household? _household; bool _isLoading = true; @@ -29,7 +31,8 @@ class _HouseholdScreenState extends State { if (settings.currentHouseholdId != null) { try { - household = await HiveDatabase.getHousehold(settings.currentHouseholdId!); + // Load from Firebase + household = await _firebaseService.getHousehold(settings.currentHouseholdId!); } catch (e) { // Household not found } @@ -81,13 +84,10 @@ class _HouseholdScreenState extends State { ); if (result != null && result.isNotEmpty) { - final household = Household( - id: Household.generateCode(), - name: result, - ownerName: _settings!.userName!, - members: [_settings!.userName!], - ); + // Create household in Firebase + final household = await _firebaseService.createHousehold(result, _settings!.userName!); + // Also save to local Hive for offline access await HiveDatabase.saveHousehold(household); _settings!.currentHouseholdId = household.id; @@ -150,26 +150,32 @@ class _HouseholdScreenState extends State { if (result != null && result.isNotEmpty) { try { - final household = await HiveDatabase.getHousehold(result.toUpperCase()); + final code = result.toUpperCase(); - if (household != null) { - if (!household.members.contains(_settings!.userName!)) { - household.members.add(_settings!.userName!); - await household.save(); - } + // Join household in Firebase + final success = await _firebaseService.joinHousehold(code, _settings!.userName!); - _settings!.currentHouseholdId = household.id; - await _settings!.save(); + if (success) { + // Load the household data + final household = await _firebaseService.getHousehold(code); - await _loadData(); + if (household != null) { + // Save to local Hive for offline access + await HiveDatabase.saveHousehold(household); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Joined ${household.name}!'), - backgroundColor: AppColors.success, - ), - ); + _settings!.currentHouseholdId = household.id; + await _settings!.save(); + + await _loadData(); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Joined ${household.name}!'), + backgroundColor: AppColors.success, + ), + ); + } } } else { if (mounted) { @@ -184,8 +190,8 @@ class _HouseholdScreenState extends State { } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Household not found. Check the code and try again.'), + SnackBar( + content: Text('Error joining household: $e'), backgroundColor: AppColors.error, ), ); @@ -261,8 +267,8 @@ class _HouseholdScreenState extends State { ); if (confirm == true && _household != null) { - _household!.members.remove(_settings!.userName); - await _household!.save(); + // Leave household in Firebase + await _firebaseService.leaveHousehold(_household!.id, _settings!.userName!); _settings!.currentHouseholdId = null; await _settings!.save(); diff --git a/lib/main.dart b/lib/main.dart index 62d2b02..fb08f31 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'core/constants/app_theme.dart'; import 'data/local/hive_database.dart'; import 'features/home/screens/home_screen.dart'; @@ -7,6 +8,9 @@ import 'features/home/screens/home_screen.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + // Initialize Firebase + await Firebase.initializeApp(); + // Initialize Hive database await HiveDatabase.init(); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 5a4adc0..6195f80 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,14 @@ import FlutterMacOS import Foundation +import cloud_firestore +import firebase_core import mobile_scanner import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 9134133..ec64ebb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "61.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: ff0a84a2734d9e1089f8aedd5c0af0061b82fb94e95260d943404e0ef2134b11 + url: "https://pub.dev" + source: hosted + version: "1.3.59" analyzer: dependency: transitive description: @@ -145,6 +153,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: "2d33da4465bdb81b6685c41b535895065adcb16261beb398f5f3bbc623979e9c" + url: "https://pub.dev" + source: hosted + version: "5.6.12" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: "413c4e01895cf9cb3de36fa5c219479e06cd4722876274ace5dfc9f13ab2e39b" + url: "https://pub.dev" + source: hosted + version: "6.6.12" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: c1e30fc4a0fcedb08723fb4b1f12ee4e56d937cbf9deae1bda43cbb6367bb4cf + url: "https://pub.dev" + source: hosted + version: "4.4.12" code_builder: dependency: transitive description: @@ -217,6 +249,30 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "7be63a3f841fc9663342f7f3a011a42aef6a61066943c90b1c434d79d5c995c5" + url: "https://pub.dev" + source: hosted + version: "3.15.2" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: "5873a370f0d232918e23a5a6137dbe4c2c47cf017301f4ea02d9d636e52f60f0" + url: "https://pub.dev" + source: hosted + version: "6.0.1" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "0ed0dc292e8f9ac50992e2394e9d336a0275b6ae400d64163fdf0a8a8b556c37" + url: "https://pub.dev" + source: hosted + version: "2.24.1" fixnum: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1975b1f..27a6853 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: sage description: "Smart Kitchen Management System" publish_to: 'none' -version: 1.0.0+1 +version: 1.1.0+2 environment: sdk: ^3.9.2 @@ -26,6 +26,10 @@ dependencies: mobile_scanner: ^5.2.3 # Barcode scanning http: ^1.2.2 # HTTP requests for Discord webhooks + # Cloud Backend + firebase_core: ^3.8.1 # Firebase initialization + cloud_firestore: ^5.6.0 # Firestore database + dev_dependencies: flutter_test: sdk: flutter diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..eeeeb11 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,12 @@ #include "generated_plugin_registrant.h" +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + CloudFirestorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..448a2c3 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST + cloud_firestore + firebase_core ) list(APPEND FLUTTER_FFI_PLUGIN_LIST