kz_educa/lib/services/firebase_service.dart

390 lines
13 KiB
Dart
Executable File

// lib/services/firebase_service.dart
import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:kzeduca_app/services/user_state_service.dart';
import 'package:kzeduca_app/models/app_models.dart' as app_models;
import 'package:flutter/foundation.dart';
import '../models/budget.dart';
import '../models/transaction.dart' as app;
import 'package:google_sign_in/google_sign_in.dart'; // <--- 🌟 NOVO: Importação do Google Sign-In
class FirebaseService {
final FirebaseAuth _auth = FirebaseAuth.instance;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
final UserStateService _userStateService;
// 🌟 NOVO: Inicializa o GoogleSignIn
final GoogleSignIn _googleSignIn = GoogleSignIn();
final String _appId;
FirebaseService(this._userStateService, {String? appId}) : _appId = appId ?? 'default-kzeduca-app';
FirebaseAuth get auth => _auth;
FirebaseFirestore get firestore => _firestore;
// --- Métodos de Autenticação ---
Future<void> initializeAuth(String? initialAuthToken) async {
if (_auth.currentUser != null) {
if (kDebugMode) {
print('Usuário já autenticado, ignorando a inicialização.');
}
return;
}
try {
if (initialAuthToken != null) {
// ✅ NOVO: Usar o token personalizado para autenticação se ele existir
await _auth.signInWithCustomToken(initialAuthToken);
if (kDebugMode) {
print('Autenticado com token personalizado.');
}
} else {
// ✅ FALLBACK: Se o token não existir, autentica anonimamente
await _auth.signInAnonymously();
if (kDebugMode) {
print('Autenticado anonimamente.');
}
}
} on FirebaseAuthException catch (e) {
if (e.code == 'operation-not-allowed' || e.code == 'admin-restricted-operation') {
if (kDebugMode) {
print("Login anônimo desabilitado, o usuário não será autenticado anonimamente.");
}
return;
}
if (kDebugMode) {
print('Erro de autenticação inicial: ${e.code}');
}
rethrow;
} catch (e) {
if (kDebugMode) {
print('Erro inesperado durante a autenticação inicial: $e');
}
rethrow;
}
}
// -----------------------------------------------------------
// 🌟 NOVO MÉTODO: LOGIN COM GOOGLE
// -----------------------------------------------------------
Future<void> signInWithGoogle() async {
try {
// 1. Inicia o fluxo de autenticação do Google
final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
if (googleUser == null) {
// O usuário cancelou o processo
throw FirebaseAuthException(
code: 'google-login-cancelled',
message: 'O login com o Google foi cancelado pelo usuário.',
);
}
// 2. Obtém a autenticação (token)
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
// 3. Cria a credencial do Firebase
final AuthCredential credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
// 4. Faz login no Firebase
final userCredential = await _auth.signInWithCredential(credential);
final firebaseUser = userCredential.user;
if (firebaseUser != null) {
// 5. Garante a existência/atualização do perfil no Firestore
// Usa o nome do Google como nome de utilizador padrão.
final username = googleUser.displayName ?? 'Utilizador Google';
final newUserProfileData = {
'uid': firebaseUser.uid,
'username': username,
'email': firebaseUser.email,
'lastSignInAt': FieldValue.serverTimestamp(),
// Se for um novo utilizador, estes campos serão criados (merge: true)
'currentBalance': 0.0,
'points': 0,
'badges': [],
'createdAt': FieldValue.serverTimestamp(),
};
// Usa updateUserProfile com merge: true para criar ou atualizar de forma segura
await updateUserProfile(
uid: firebaseUser.uid,
data: newUserProfileData,
);
await _userStateService.fetchUserData(firebaseUser.uid);
}
} on FirebaseAuthException catch (e) {
if (kDebugMode) {
print('Erro de autenticação do Google: ${e.code}');
}
rethrow;
} catch (e) {
if (kDebugMode) {
print('Erro inesperado no Google Sign-In: $e');
}
rethrow;
}
}
// -----------------------------------------------------------
Future<void> registerWithEmailAndPassword(String email, String password, String username) async {
try {
final userCredential = await _auth.createUserWithEmailAndPassword(
email: email,
password: password,
);
final firebaseUser = userCredential.user;
if (firebaseUser != null) {
final newUserProfileData = {
'uid': firebaseUser.uid,
'username': username,
'email': firebaseUser.email,
'currentBalance': 0.0,
'points': 0,
'badges': [],
'createdAt': FieldValue.serverTimestamp(),
};
await _firestore
.collection('artifacts')
.doc(_appId)
.collection('users')
.doc(firebaseUser.uid)
.set(newUserProfileData);
await _userStateService.fetchUserData(firebaseUser.uid);
}
} on FirebaseAuthException catch (e) {
if (kDebugMode) {
print('Erro de autenticação no registro: ${e.code}');
}
rethrow;
}
}
Future<void> signInWithEmailAndPassword(String email, String password) async {
try {
final userCredential = await _auth.signInWithEmailAndPassword(
email: email,
password: password,
);
final firebaseUser = userCredential.user;
if (firebaseUser != null) {
final userProfileRef = _firestore.collection('artifacts').doc(_appId).collection('users').doc(firebaseUser.uid);
final userProfileSnapshot = await userProfileRef.get();
if (!userProfileSnapshot.exists) {
if (kDebugMode) {
print('Documento de perfil não encontrado. Criando um novo.');
}
// Usa updateUserProfile para garantir que a lógica de criação/atualização seja consistente
await updateUserProfile(
uid: firebaseUser.uid,
data: {
'uid': firebaseUser.uid,
'email': firebaseUser.email,
'username': 'Utilizador', // Nome padrão
'currentBalance': 0.0,
'points': 0,
'badges': [],
'createdAt': FieldValue.serverTimestamp(),
},
);
}
await _userStateService.fetchUserData(firebaseUser.uid);
}
} on FirebaseAuthException catch (e) {
if (kDebugMode) {
print('Erro de autenticação no login: ${e.code}');
}
rethrow;
}
}
Future<void> signOut() async {
await _auth.signOut();
// 🌟 NOVO: Desconecta também do Google Sign-In para evitar login automático na próxima vez
await _googleSignIn.signOut();
}
String? get uid => _auth.currentUser?.uid;
CollectionReference _getUserCollection() {
return _firestore.collection('artifacts').doc(_appId).collection('users');
}
CollectionReference get _transactionsCollection {
if (uid == null) {
throw Exception('User not logged in');
}
return _getUserCollection().doc(uid).collection('transactions');
}
CollectionReference get _budgetsCollection {
if (uid == null) {
throw Exception('User not logged in');
}
return _getUserCollection().doc(uid).collection('budgets');
}
// ✅ NOVO MÉTODO: Adicionado para resolver o erro 'update' de documento inexistente.
// Utiliza 'set' com 'merge: true' para criar ou atualizar o perfil.
Future<void> updateUserProfile({
required String uid,
required Map<String, dynamic> data,
}) async {
try {
await _firestore
.collection('artifacts')
.doc(_appId)
.collection('users')
.doc(uid)
.set(
data,
SetOptions(merge: true),
);
if (kDebugMode) {
print('Perfil do utilizador $uid atualizado com sucesso.');
}
} on FirebaseException catch (e) {
if (kDebugMode) {
print('Erro no Firebase ao atualizar o perfil: ${e.message}');
}
rethrow; // Rejeita o erro para ser capturado no try-catch da UI.
} catch (e) {
if (kDebugMode) {
print('Erro desconhecido ao atualizar o perfil: $e');
}
rethrow;
}
}
Future<DocumentSnapshot> getUserProfile(String uid) async {
return await _firestore.collection('artifacts').doc(_appId).collection('users').doc(uid).get();
}
// NOTE: Este método é redundante com `updateUserProfile` e não é usado acima, mas mantido por integridade.
Future<void> createUserProfile(String uid, String email, String username) async {
await _firestore
.collection('artifacts')
.doc(_appId)
.collection('users')
.doc(uid)
.set({
'uid': uid,
'email': email,
'username': username,
'createdAt': FieldValue.serverTimestamp(),
'points': 0,
'badges': [],
});
}
Stream<DocumentSnapshot> streamUserProfile(String uid) {
return _firestore.collection('artifacts').doc(_appId).collection('users').doc(uid).snapshots();
}
// --- Métodos de Transações e Orçamentos ---
Future<List<app.Transaction>> getTransactions() async {
final snapshot = await _transactionsCollection.get();
return snapshot.docs.map((doc) => app.Transaction.fromMap(doc.data() as Map<String, dynamic>)).toList();
}
Future<void> addTransaction(app.Transaction transaction) async {
await _transactionsCollection.add(transaction.toMap());
}
Future<void> deleteTransaction(String transactionId) async {
await _transactionsCollection.doc(transactionId).delete();
}
Future<List<Budget>> getBudgets() async {
final snapshot = await _budgetsCollection.get();
return snapshot.docs.map((doc) => Budget.fromFirestore(doc)).toList();
}
Future<void> addBudget(Budget budget) async {
await _budgetsCollection.add(budget.toMap());
}
Future<void> deleteBudget(String budgetId) async {
await _budgetsCollection.doc(budgetId).delete();
}
// --- Métodos de Simulações de Poupança ---
Future<void> saveSimulation(String userId, Map<String, dynamic> simulationData) async {
await _getUserCollection().doc(userId).collection('simulations').add(simulationData);
}
Stream<QuerySnapshot> streamSavedSimulations(String userId) {
return _getUserCollection().doc(userId).collection('simulations').snapshots();
}
// --- Métodos de Desafios ---
// Obtém todos os desafios públicos para exibição
Future<List<app_models.FinancialChallenge>> getAllChallenges() async {
final snapshot = await _firestore.collection('artifacts').doc(_appId).collection('public').doc('data').collection('challenges').get();
return snapshot.docs.map((doc) => app_models.FinancialChallenge.fromFirestore(doc)).toList();
}
// Fluxo em tempo real para desafios públicos
Stream<QuerySnapshot> streamPublicChallenges() {
return _firestore.collection('artifacts').doc(_appId).collection('public').doc('data').collection('challenges').snapshots();
}
// Fluxo em tempo real para os desafios aceites pelo usuário
Stream<QuerySnapshot> streamUserChallenges(String userId) {
return _getUserCollection().doc(userId).collection('userChallenges').snapshots();
}
Future<void> acceptChallenge(String userId, Map<String, dynamic> challengeData) async {
await _getUserCollection().doc(userId).collection('userChallenges').add(challengeData);
}
Future<void> updateChallengeProgress(String userId, String challengeId, int progress) async {
await _getUserCollection().doc(userId).collection('userChallenges').doc(challengeId).update({
'progresso': progress,
'lastUpdated': FieldValue.serverTimestamp(),
});
}
// --- Métodos de Aulas ---
Stream<QuerySnapshot> streamPublicLessons() {
return _firestore.collection('artifacts').doc(_appId).collection('public').doc('data').collection('lessons').snapshots();
}
Stream<QuerySnapshot> streamUserCompletedLessons(String userId) {
return _getUserCollection().doc(userId).collection('userLessons').snapshots();
}
Future<void> markLessonAsComplete(String userId, String lessonId) async {
await _getUserCollection().doc(userId).collection('userLessons').doc(lessonId).set({
'lessonId': lessonId,
'completedAt': FieldValue.serverTimestamp(),
});
}
// --- Métodos de Quiz ---
Stream<QuerySnapshot> streamQuizQuestions() {
return _firestore.collection('artifacts').doc(_appId).collection('public').doc('data').collection('quizQuestions').snapshots();
}
Stream<QuerySnapshot> streamUserQuizResults(String userId) {
return _getUserCollection().doc(userId).collection('userQuizResults').snapshots();
}
Future<void> saveQuizResult(String userId, Map<String, dynamic> quizResultData) async {
await _getUserCollection().doc(userId).collection('userQuizResults').add(quizResultData);
}
}