390 lines
13 KiB
Dart
Executable File
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);
|
|
}
|
|
} |