amoriai/lib/services/firebase_service.dart

550 lines
16 KiB
Dart

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import '../models/user_model.dart';
import '../models/recommendation_model.dart';
import '../core/constants/app_constants.dart';
import 'google_signin_wrapper.dart';
class FirebaseService {
static final FirebaseService _instance = FirebaseService._internal();
factory FirebaseService() => _instance;
// Firebase instances
final FirebaseAuth _auth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final FirebaseMessaging _messaging = FirebaseMessaging.instance;
// Initialize Firebase Service
FirebaseService._internal();
// Current user stream
Stream<User?> get authStateChanges => _auth.authStateChanges();
User? get currentUser => _auth.currentUser;
bool get isSignedIn => currentUser != null;
// Initialize Firebase
static Future<void> initialize() async {
try {
await Firebase.initializeApp();
debugPrint('Firebase initialized successfully');
} catch (e) {
debugPrint('Firebase initialization error: $e');
rethrow;
}
}
// Authentication Methods
Future<UserCredential?> signInWithEmail(String email, String password) async {
try {
final credential = await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
return credential;
} on FirebaseAuthException catch (e) {
debugPrint('Sign in error: ${e.message}');
throw _handleAuthException(e);
}
}
Future<UserCredential?> registerWithEmail(
String email,
String password,
) async {
try {
final credential = await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
// Create user document in Firestore
if (credential.user != null) {
await createUserDocument(credential.user!);
}
return credential;
} on FirebaseAuthException catch (e) {
debugPrint('Registration error: ${e.message}');
throw _handleAuthException(e);
}
}
Future<UserCredential?> signInWithGoogle() async {
try {
debugPrint('🔄 Starting safe Google Sign-In process...');
// Use the crash-safe wrapper
final googleSignInWrapper = GoogleSignInWrapper.instance;
// Check if Google Sign-In is available
bool isAvailable = await googleSignInWrapper.isAvailable();
if (!isAvailable) {
throw Exception('Google Sign-In is not available on this device');
}
// Attempt sign-in using the wrapper
UserCredential? userCredential = await googleSignInWrapper.signIn();
if (userCredential == null) {
debugPrint('👤 User cancelled Google Sign-In');
return null;
}
return userCredential;
} catch (e) {
debugPrint('❌ Google Sign-In failed: $e');
throw Exception('Google Sign-In failed: $e');
}
}
Future<void> signOut() async {
try {
await Future.wait([
_auth.signOut(),
GoogleSignInWrapper.instance.signOut(),
]);
} catch (e) {
debugPrint('Sign out error: $e');
throw Exception('Sign out failed: $e');
}
}
Future<void> resetPassword(String email) async {
try {
await _auth.sendPasswordResetEmail(email: email);
} on FirebaseAuthException catch (e) {
debugPrint('Password reset error: ${e.message}');
throw _handleAuthException(e);
}
}
/// Check if email exists in Firestore (registered user)
Future<bool> checkEmailExists(String email) async {
try {
final querySnapshot = await _firestore
.collection(AppConstants.usersCollection)
.where('email', isEqualTo: email)
.limit(1)
.get();
return querySnapshot.docs.isNotEmpty;
} catch (e) {
debugPrint('Check email exists error: $e');
return false;
}
}
// Email Verification Methods
Future<UserCredential?> createUserForVerification(
String email,
String tempPassword,
) async {
try {
final credential = await _auth.createUserWithEmailAndPassword(
email: email,
password: tempPassword,
);
return credential;
} on FirebaseAuthException catch (e) {
debugPrint('Create user for verification error: ${e.message}');
throw _handleAuthException(e);
}
}
Future<void> sendEmailVerification() async {
try {
final user = _auth.currentUser;
if (user != null && !user.emailVerified) {
await user.sendEmailVerification();
debugPrint('📧 Verification email sent to ${user.email}');
}
} catch (e) {
debugPrint('Send email verification error: $e');
throw Exception('Gagal mengirim email verifikasi: $e');
}
}
Future<bool> checkEmailVerified() async {
try {
await _auth.currentUser?.reload();
return _auth.currentUser?.emailVerified ?? false;
} catch (e) {
debugPrint('Check email verified error: $e');
return false;
}
}
Future<void> updatePassword(String newPassword) async {
try {
final user = _auth.currentUser;
if (user != null) {
await user.updatePassword(newPassword);
debugPrint('🔑 Password updated successfully');
}
} catch (e) {
debugPrint('Update password error: $e');
throw Exception('Gagal memperbarui password: $e');
}
}
/// Link email/password to existing account (for Google users)
Future<UserCredential?> linkEmailPassword(
String email,
String password,
) async {
try {
final user = _auth.currentUser;
if (user == null) {
throw Exception('User tidak ditemukan');
}
// Create email/password credential
final credential = EmailAuthProvider.credential(
email: email,
password: password,
);
// Link the credential to the current user
final result = await user.linkWithCredential(credential);
debugPrint('✅ Email/password linked successfully to ${user.email}');
return result;
} on FirebaseAuthException catch (e) {
debugPrint('Link email/password error: ${e.code} - ${e.message}');
if (e.code == 'provider-already-linked') {
debugPrint('⚠️ Provider already linked, updating password to match new input...');
try {
await _auth.currentUser?.updatePassword(password);
return null;
} catch (err) {
debugPrint('Failed to update password on already linked account: $err');
}
} else if (e.code == 'credential-already-in-use') {
throw Exception('Email ini sudah digunakan akun lain');
}
throw _handleAuthException(e);
} catch (e) {
debugPrint('Link email/password error: $e');
throw Exception('Gagal menghubungkan email/password: $e');
}
}
// User Document Methods
Future<UserModel> createUserDocument(
User user, {
String? name,
String? phone,
}) async {
try {
final userDoc = _firestore
.collection(AppConstants.usersCollection)
.doc(user.uid);
// Force create/merge user document to ensure it exists
// This is crucial for self-healing if data was deleted
final userModel = UserModel(
id: user.uid,
email: user.email!,
displayName: name ?? user.displayName,
phoneNumber: phone,
photoUrl: user.photoURL,
preferences: UserPreferences(),
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
isSetupComplete: false,
);
// Use SetOptions(merge: true) to safe-write
await userDoc.set(userModel.toJson(), SetOptions(merge: true));
debugPrint('✅ User document created/merged for ${user.uid}');
return userModel;
} catch (e) {
debugPrint('Create user document error: $e');
throw Exception('Failed to create user document: $e');
}
}
Future<bool> isUserRegistered(String uid) async {
try {
final doc = await _firestore
.collection(AppConstants.usersCollection)
.doc(uid)
.get();
return doc.exists;
} catch (e) {
return false;
}
}
Future<UserModel?> getUserData(String userId) async {
try {
final doc = await _firestore
.collection(AppConstants.usersCollection)
.doc(userId)
.get();
if (doc.exists && doc.data() != null) {
return UserModel.fromJson(doc.data()!);
}
return null;
} catch (e) {
debugPrint('Get user data error: $e');
throw Exception('Failed to get user data: $e');
}
}
/// Get user data by email address
Future<UserModel?> getUserDataByEmail(String email) async {
try {
final querySnapshot = await _firestore
.collection(AppConstants.usersCollection)
.where('email', isEqualTo: email)
.limit(1)
.get();
if (querySnapshot.docs.isNotEmpty) {
return UserModel.fromJson(querySnapshot.docs.first.data());
}
return null;
} catch (e) {
debugPrint('Get user data by email error: $e');
return null;
}
}
Future<void> updateUserData(UserModel user) async {
try {
await _firestore
.collection(AppConstants.usersCollection)
.doc(user.id)
.update(user.copyWith(updatedAt: DateTime.now()).toJson());
} catch (e) {
debugPrint('Update user data error: $e');
throw Exception('Failed to update user data: $e');
}
}
Future<void> updateUserPreferences(
String userId,
UserPreferences preferences,
) async {
try {
final doc = await _firestore
.collection(AppConstants.usersCollection)
.doc(userId)
.get();
if (!doc.exists) throw Exception('User not found');
final currentData = doc.data() as Map<String, dynamic>;
final dailyPreferences = Map<String, dynamic>.from(
currentData['dailyPreferences'] ?? {});
// Sinkronisasi Balik: Profil -> Daily Check-in (keinginan)
bool hasFood = preferences.favoriteCategories.any((c) =>
c.toLowerCase().contains('makanan') ||
c.toLowerCase().contains('snack') ||
c.toLowerCase().contains('dessert') ||
c.toLowerCase().contains('pastry') ||
c.toLowerCase().contains('sarapan') ||
c.toLowerCase().contains('berkuah'));
bool hasDrink = preferences.favoriteCategories.any((c) =>
c.toLowerCase().contains('minuman'));
if (hasFood && hasDrink) {
dailyPreferences['keinginan'] = 'keduanya';
} else if (hasDrink && !hasFood) {
dailyPreferences['keinginan'] = 'minuman';
} else if (hasFood && !hasDrink) {
dailyPreferences['keinginan'] = 'makanan';
}
// Sinkronkan juga budget dari profil ke daily checkin hari ini
dailyPreferences['budget'] = preferences.budgetRange;
final data = preferences.toJson();
// 🔍 DEBUG — hapus setelah debugging selesai
debugPrint('=== SAVE PREFERENCES DEBUG ===');
debugPrint('userId: $userId');
debugPrint('favoriteCategories: ${preferences.favoriteCategories}');
debugPrint('new keinginan: ${dailyPreferences['keinginan']}');
debugPrint('==============================');
await _firestore
.collection(AppConstants.usersCollection)
.doc(userId)
.update({
'preferences': data,
'dailyPreferences': dailyPreferences,
'updatedAt': DateTime.now().toIso8601String(),
});
debugPrint('✅ Preferences saved successfully (and synced to dailyPreferences)');
} catch (e) {
debugPrint('Update user preferences error: $e');
throw Exception('Failed to update user preferences: $e');
}
}
Future<void> setSetupComplete(String userId) async {
try {
await _firestore
.collection(AppConstants.usersCollection)
.doc(userId)
.update({
'isSetupComplete': true,
'updatedAt': DateTime.now().toIso8601String(),
});
} catch (e) {
debugPrint('Set setup complete error: $e');
throw Exception('Failed to set setup complete: $e');
}
}
// Recommendations Methods
Future<void> saveRecommendation(RecommendationModel recommendation) async {
try {
await _firestore
.collection(AppConstants.recommendationsCollection)
.doc(recommendation.id)
.set(recommendation.toJson());
} catch (e) {
debugPrint('Save recommendation error: $e');
throw Exception('Failed to save recommendation: $e');
}
}
Future<List<RecommendationModel>> getUserRecommendations(
String userId, {
int limit = 20,
}) async {
try {
// Simplified query to avoid index issues - just get by userId first
final query = await _firestore
.collection(AppConstants.recommendationsCollection)
.where('userId', isEqualTo: userId)
.limit(limit)
.get();
// Sort in memory to avoid composite index requirement
final recommendations = query.docs
.map((doc) => RecommendationModel.fromJson(doc.data()))
.toList();
// Sort by createdAt in descending order
recommendations.sort((a, b) => b.createdAt.compareTo(a.createdAt));
return recommendations;
} catch (e) {
debugPrint('Get user recommendations error: $e');
// Return empty list instead of throwing exception for better UX
return [];
}
}
Future<void> toggleRecommendationFavorite(
String recommendationId,
bool isFavorite,
) async {
try {
await _firestore
.collection(AppConstants.recommendationsCollection)
.doc(recommendationId)
.update({'isFavorite': isFavorite});
} catch (e) {
debugPrint('Toggle recommendation favorite error: $e');
throw Exception('Failed to update recommendation: $e');
}
}
Future<void> deleteRecommendation(String recommendationId) async {
try {
await _firestore
.collection(AppConstants.recommendationsCollection)
.doc(recommendationId)
.delete();
} catch (e) {
debugPrint('Delete recommendation error: $e');
throw Exception('Failed to delete recommendation: $e');
}
}
// Push Notifications
Future<void> initializeNotifications() async {
try {
// Request permission
NotificationSettings settings = await _messaging.requestPermission(
alert: true,
announcement: false,
badge: true,
carPlay: false,
criticalAlert: false,
provisional: false,
sound: true,
);
if (settings.authorizationStatus == AuthorizationStatus.authorized) {
debugPrint('User granted permission for notifications');
// Get FCM token
String? token = await _messaging.getToken();
if (token != null && currentUser != null) {
await _saveFCMToken(currentUser!.uid, token);
}
// Listen for token refresh
_messaging.onTokenRefresh.listen((token) {
if (currentUser != null) {
_saveFCMToken(currentUser!.uid, token);
}
});
}
} catch (e) {
debugPrint('Initialize notifications error: $e');
}
}
Future<void> _saveFCMToken(String userId, String token) async {
try {
await _firestore
.collection(AppConstants.usersCollection)
.doc(userId)
.update({
'fcmToken': token,
'updatedAt': DateTime.now().toIso8601String(),
});
} catch (e) {
debugPrint('Save FCM token error: $e');
}
}
// Error handling
String _handleAuthException(FirebaseAuthException e) {
switch (e.code) {
case 'invalid-credential':
return 'Email atau Password salah';
case 'user-not-found':
return 'Pengguna tidak ditemukan';
case 'wrong-password':
return 'Password salah';
case 'email-already-in-use':
return 'Email sudah digunakan';
case 'weak-password':
return 'Password terlalu lemah';
case 'invalid-email':
return 'Format email tidak valid';
case 'user-disabled':
return 'Akun telah dinonaktifkan';
case 'too-many-requests':
return 'Terlalu banyak percobaan, coba lagi nanti';
case 'operation-not-allowed':
return 'Operasi tidak diizinkan';
default:
return e.message ?? 'Terjadi kesalahan';
}
}
}