234 lines
7.1 KiB
Dart
234 lines
7.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:cloud_firestore/cloud_firestore.dart';
|
|
import 'package:firebase_auth/firebase_auth.dart';
|
|
import 'package:firebase_storage/firebase_storage.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'dart:io';
|
|
|
|
import 'package:kzeduca_app/models/app_models.dart' as app_models;
|
|
import 'package:kzeduca_app/models/transaction.dart' as txn;
|
|
import 'package:kzeduca_app/services/hive_service.dart';
|
|
|
|
enum UserStateStatus {
|
|
idle,
|
|
loading,
|
|
loaded,
|
|
error,
|
|
}
|
|
|
|
class UserStateService extends ChangeNotifier {
|
|
final FirebaseFirestore _db = FirebaseFirestore.instance;
|
|
final FirebaseAuth _auth = FirebaseAuth.instance;
|
|
final FirebaseStorage _storage = FirebaseStorage.instance;
|
|
final String _appId = 'default-kzeduca-app';
|
|
|
|
app_models.AppUser? _currentUser;
|
|
UserStateStatus _status = UserStateStatus.idle;
|
|
String? _errorMessage;
|
|
|
|
app_models.AppUser? get currentUser => _currentUser;
|
|
UserStateStatus get status => _status;
|
|
String? get errorMessage => _errorMessage;
|
|
FirebaseAuth get auth => _auth;
|
|
|
|
UserStateService() {
|
|
_auth.authStateChanges().listen((user) {
|
|
if (user == null) {
|
|
_currentUser = null;
|
|
_status = UserStateStatus.idle;
|
|
// NOTA: A notificação aqui é síncrona, o que é geralmente seguro no construtor
|
|
notifyListeners();
|
|
} else {
|
|
// Inicia a busca de dados quando o estado de autenticação muda
|
|
fetchUserData(user.uid);
|
|
}
|
|
});
|
|
}
|
|
|
|
CollectionReference get _usersCollection {
|
|
return _db.collection('artifacts').doc(_appId).collection('users');
|
|
}
|
|
|
|
// MÉTODO AUXILIAR PARA NOTIFICAÇÃO SEGURA (CORREÇÃO DO BUG setState during build)
|
|
void _safeNotifyListeners() {
|
|
// Garante que a notificação é adiada para a próxima "micropassagem"
|
|
// do loop de eventos, evitando o erro durante a fase de build.
|
|
if (hasListeners) {
|
|
Future.microtask(notifyListeners);
|
|
}
|
|
}
|
|
|
|
// ----------------------
|
|
// MÉTODO: fetchUserData
|
|
// ----------------------
|
|
Future<void> fetchUserData(String userId) async {
|
|
_status = UserStateStatus.loading;
|
|
_safeNotifyListeners(); // Notificação inicial
|
|
|
|
try {
|
|
final userDoc = await _usersCollection.doc(userId).get();
|
|
if (userDoc.exists) {
|
|
final data = userDoc.data();
|
|
if (data != null) {
|
|
_currentUser = app_models.AppUser.fromMap(data as Map<String, dynamic>);
|
|
_status = UserStateStatus.loaded;
|
|
} else {
|
|
_status = UserStateStatus.error;
|
|
_errorMessage = 'User data is empty.';
|
|
}
|
|
} else {
|
|
// Lida com a criação e atribui o utilizador imediatamente
|
|
await createUserDocument(_auth.currentUser!, 'Utilizador');
|
|
_status = UserStateStatus.loaded;
|
|
}
|
|
} catch (e) {
|
|
_status = UserStateStatus.error;
|
|
_errorMessage = 'Failed to fetch user data: $e';
|
|
} finally {
|
|
// Notificação final (agora segura)
|
|
_safeNotifyListeners();
|
|
}
|
|
}
|
|
|
|
// ----------------------
|
|
// MÉTODO: updateBalance
|
|
// ----------------------
|
|
Future<void> updateBalance(txn.Transaction transaction) async {
|
|
if (_currentUser == null) {
|
|
_errorMessage = 'No user logged in.';
|
|
_status = UserStateStatus.error;
|
|
_safeNotifyListeners();
|
|
return;
|
|
}
|
|
|
|
final newBalance = transaction.type == txn.TransactionType.income
|
|
? _currentUser!.currentBalance + transaction.amount
|
|
: _currentUser!.currentBalance - transaction.amount;
|
|
|
|
try {
|
|
await _usersCollection.doc(_currentUser!.uid).update({
|
|
'currentBalance': newBalance,
|
|
});
|
|
|
|
_currentUser = _currentUser!.copyWith(currentBalance: newBalance);
|
|
_status = UserStateStatus.loaded;
|
|
} catch (e) {
|
|
_status = UserStateStatus.error;
|
|
_errorMessage = 'Failed to update user balance: $e';
|
|
} finally {
|
|
_safeNotifyListeners();
|
|
}
|
|
}
|
|
|
|
// ----------------------
|
|
// MÉTODO: createUserDocument
|
|
// ----------------------
|
|
Future<void> createUserDocument(User firebaseUser, String username) async {
|
|
final userDocRef = _usersCollection.doc(firebaseUser.uid);
|
|
final userDoc = await userDocRef.get();
|
|
|
|
if (!userDoc.exists) {
|
|
final newUser = app_models.AppUser(
|
|
uid: firebaseUser.uid,
|
|
username: username,
|
|
email: firebaseUser.email,
|
|
currentBalance: 0.0,
|
|
);
|
|
|
|
await userDocRef.set(newUser.toMap());
|
|
_currentUser = newUser;
|
|
_status = UserStateStatus.loaded;
|
|
_safeNotifyListeners(); // Notificação segura
|
|
}
|
|
}
|
|
|
|
// ----------------------
|
|
// MÉTODO: recalculateBalanceFromHive
|
|
// ----------------------
|
|
Future<void> recalculateBalanceFromHive() async {
|
|
final hiveService = HiveService();
|
|
final transactions = await hiveService.getTransactions();
|
|
double balance = 0.0;
|
|
for (final t in transactions) {
|
|
if (t.type == txn.TransactionType.income) {
|
|
balance += t.amount;
|
|
} else {
|
|
balance -= t.amount;
|
|
}
|
|
}
|
|
if (_currentUser != null) {
|
|
_currentUser = _currentUser!.copyWith(currentBalance: balance);
|
|
_safeNotifyListeners(); // Notificação segura
|
|
}
|
|
}
|
|
|
|
// ----------------------
|
|
// MÉTODO: updateUserProfile
|
|
// ----------------------
|
|
Future<void> updateUserProfile({
|
|
String? username,
|
|
String? email,
|
|
XFile? profileImage,
|
|
}) async {
|
|
if (_currentUser == null) return;
|
|
|
|
_status = UserStateStatus.loading;
|
|
_safeNotifyListeners();
|
|
|
|
try {
|
|
final updatedData = <String, dynamic>{};
|
|
|
|
if (username != null) {
|
|
updatedData['username'] = username;
|
|
}
|
|
if (email != null) {
|
|
updatedData['email'] = email;
|
|
}
|
|
|
|
// Lógica para fazer o upload da imagem de perfil
|
|
if (profileImage != null) {
|
|
final storageRef = _storage
|
|
.ref('artifacts')
|
|
.child(_appId)
|
|
.child('users')
|
|
.child(_currentUser!.uid)
|
|
.child('profileImage.jpg');
|
|
|
|
await storageRef.putFile(File(profileImage.path));
|
|
final imageUrl = await storageRef.getDownloadURL();
|
|
updatedData['profileImage'] = imageUrl;
|
|
}
|
|
|
|
if (updatedData.isNotEmpty) {
|
|
await _usersCollection.doc(_currentUser!.uid).update(updatedData);
|
|
}
|
|
|
|
// Atualiza o objeto localmente para refletir as mudanças
|
|
_currentUser = _currentUser!.copyWith(
|
|
username: username ?? _currentUser!.username,
|
|
email: email ?? _currentUser!.email,
|
|
profileImage: updatedData['profileImage'] ?? _currentUser!.profileImage,
|
|
);
|
|
|
|
_status = UserStateStatus.loaded;
|
|
} catch (e) {
|
|
_errorMessage = 'Erro ao atualizar perfil: $e';
|
|
_status = UserStateStatus.error;
|
|
} finally {
|
|
_safeNotifyListeners();
|
|
}
|
|
}
|
|
|
|
// ----------------------
|
|
// MÉTODO: logout
|
|
// ----------------------
|
|
void logout() {
|
|
_auth.signOut();
|
|
_currentUser = null;
|
|
_status = UserStateStatus.idle;
|
|
notifyListeners(); // A notificação síncrona é segura no logout
|
|
}
|
|
|
|
// O método loadUser não precisa de lógica, pois fetchUserData já é chamado pelo listener.
|
|
Future<void> loadUser() async {}
|
|
} |