didvan-app/lib/providers/user.dart

436 lines
13 KiB
Dart

// ignore: depend_on_referenced_packages
// ignore_for_file: avoid_print
import 'package:collection/collection.dart';
import 'package:didvan/config/auth_config.dart';
import 'package:didvan/main.dart';
import 'package:didvan/models/enums.dart';
import 'package:didvan/models/user.dart';
import 'package:didvan/models/view/alert_data.dart';
import 'package:didvan/providers/core.dart';
import 'package:didvan/services/auth/auth_api_service.dart';
import 'package:didvan/services/auth/keycloak_auth_service.dart';
import 'package:didvan/services/auth/token_storage.dart';
import 'package:didvan/services/network/request.dart';
import 'package:didvan/services/network/request_helper.dart';
import 'package:didvan/services/notification/firebase_api.dart';
import 'package:didvan/services/notification/notification_service.dart';
import 'package:didvan/services/storage/storage.dart';
import 'package:didvan/utils/action_sheet.dart';
class UserProvider extends CoreProvier {
late User user;
bool isAuthenticated = false;
int _unreadMessageCount = 0;
String? _welcomeMessage;
bool _isLoadingWelcome = true;
String? get welcomeMessage => _welcomeMessage;
bool get isLoadingWelcome => _isLoadingWelcome;
set unreadMessageCount(int value) {
if (value < 0) {
return;
}
_unreadMessageCount = value;
notifyListeners();
}
int get unreadMessageCount => _unreadMessageCount;
static final List<MapEntry> _statisticMarkQueue = [];
static final List<Map> _itemMarkQueue = [];
Future<void> fetchWelcomeMessage() async {
if (!_isLoadingWelcome) {
_isLoadingWelcome = true;
notifyListeners();
}
if (!AuthConfig.useLegacyApi) {
final hour = DateTime.now().hour;
String period;
if (hour < 12) {
period = 'صبح';
} else if (hour < 17) {
period = 'ظهر';
} else if (hour < 20) {
period = 'عصر';
} else {
period = 'شب';
}
String name = '';
try {
name = user.fullName;
} catch (_) {}
_welcomeMessage =
name.isEmpty ? '$period بخیر 👋' : '$period بخیر $name 👋';
_isLoadingWelcome = false;
notifyListeners();
return;
}
try {
const String url = 'https://api.didvan.app/ai/aiwellcom';
if (RequestService.token == null) {
print("UserProvider: fetchWelcomeMessage skipped, token is null.");
_isLoadingWelcome = false;
notifyListeners();
return;
}
print("UserProvider: Fetching welcome message...");
final service = RequestService(url, useAutherization: true);
await service.post();
if (service.isSuccess) {
print("UserProvider: Welcome message API success.");
final period = service.data('period');
final userData = service.data('user');
if (period != null &&
userData is Map &&
userData.containsKey('fullName')) {
final fullName = userData['fullName'];
_welcomeMessage = '$period بخیر $fullName 👋';
print("UserProvider: Welcome message set: $_welcomeMessage");
} else {
print(
"UserProvider: Welcome message API success but data format unexpected.");
_welcomeMessage = null;
}
} else {
print(
"UserProvider: Welcome message API failed. Status: ${service.statusCode}, Error: ${service.errorMessage}");
_welcomeMessage = null;
}
} catch (e) {
print("UserProvider: Exception fetching welcome message: $e");
_welcomeMessage = null;
} finally {
_isLoadingWelcome = false;
print(
"UserProvider: fetchWelcomeMessage finished. isLoadingWelcome: $_isLoadingWelcome");
notifyListeners();
}
}
Future<String?> setAndGetToken({String? newToken}) async {
try {
if (newToken == null) {
final cached = TokenStorage.accessToken;
if (cached != null && cached.isNotEmpty) {
RequestService.token = cached;
return cached;
}
final token = await StorageService.getValue(key: 'token');
if (token != null) RequestService.token = token;
return token;
}
RequestService.token = newToken;
await StorageService.setValue(key: 'token', value: newToken);
return null;
} catch (e) {
return null;
}
}
Future<bool> getUserInfo() async {
isAuthenticated = true;
print("UserProvider: Getting user info...");
final keycloakId = TokenStorage.userId;
if (keycloakId == null || keycloakId.isEmpty) {
print("UserProvider: getUserInfo failed - no user id in token.");
isAuthenticated = false;
return false;
}
final userinfo = await KeycloakAuthService.instance.fetchUserInfo();
if (userinfo == null) {
print("UserProvider: userinfo failed (likely 401).");
isAuthenticated = false;
return false;
}
final result = await AuthApiService.instance.getUserInfo(keycloakId);
Map<String, dynamic> mergedData;
if (result.isSuccess && result.data != null) {
mergedData = {..._userinfoToAppShape(userinfo), ...result.data!};
print("UserProvider: User info merged from userinfo + auth-service.");
} else {
print("UserProvider: auth-service ${result.statusCode ?? 'n/a'} -> "
"fallback to userinfo only.");
mergedData = _userinfoToAppShape(userinfo);
}
try {
user = User.fromJson(mergedData);
if (mergedData['start'] != null) {
await StorageService.setValue(
key: 'notificationTimeRangeStart',
value: mergedData['start'],
);
}
if (mergedData['end'] != null) {
await StorageService.setValue(
key: 'notificationTimeRangeEnd',
value: mergedData['end'],
);
}
// ignore: discarded_futures
_registerFirebaseToken();
notifyListeners();
// ignore: discarded_futures
fetchWelcomeMessage();
// If the user reached us via a notification tap while logged out,
// splash stashed the payload. Now that we're authenticated, replay
// it so they land on the right detail screen instead of staying on
// the home page.
// ignore: discarded_futures
NotificationService.consumePendingNotificationTap();
return true;
} catch (e) {
print("UserProvider: Exception processing user info: $e");
isAuthenticated = false;
return false;
}
}
Map<String, dynamic> _userinfoToAppShape(Map<String, dynamic> uinfo) {
return {
'id': uinfo['sub'],
'keycloakId': uinfo['sub'],
'username': uinfo['preferred_username'],
'firstName': uinfo['given_name'],
'lastName': uinfo['family_name'],
'fullName': uinfo['name'],
'email': uinfo['email'],
// attribute هایی که فقط در صورت تنظیم Protocol Mapper در client می‌آیند:
'phone': uinfo['phone'] ?? uinfo['phone_number'],
'avatar': uinfo['avatar'] ?? uinfo['picture'],
};
}
Future<void> _registerFirebaseToken() async {
if (FirebaseApi.fcmToken == null) return;
final id = TokenStorage.userId ?? user.keycloakId;
if (id == null || id.isEmpty) return;
// کلید DTO سمت backend `firebaseToken` (camelCase) است — backend خودش
// در users.service.ts آن را به `firebase_token` در Keycloak attribute
// تبدیل می‌کند (پس از fix snake_case در update path).
await AuthApiService.instance.updateUser(
id: id,
data: {'firebaseToken': FirebaseApi.fcmToken},
);
}
Future<void> logout() async {
print('UserProvider: logout() called - clearing local state first');
final refreshToken = TokenStorage.refreshToken;
RequestService.token = null;
await TokenStorage.clear();
await StorageService.delete(key: 'token');
isAuthenticated = false;
notifyListeners();
print('UserProvider: local state cleared, '
'accessToken=${TokenStorage.accessToken}');
if (refreshToken != null) {
// ignore: discarded_futures
KeycloakAuthService.instance.revokeRefreshToken(refreshToken);
}
}
Future<bool> setProfilePhoto(dynamic file) async {
// TODO(auth-followup): پیاده‌سازی آپلود به s3-service و سپس فراخوانی
// AuthApiService.instance.setProfilePicture(id, key).
appState = AppState.failed;
return false;
}
Future<bool> deleteProfilePhoto() async {
appState = AppState.isolatedBusy;
final id = TokenStorage.userId ?? user.keycloakId;
if (id == null || id.isEmpty) {
appState = AppState.idle;
return false;
}
final result = await AuthApiService.instance.updateUser(
id: id,
data: {'avatar': null},
);
if (result.isSuccess) {
user = user.copyWith(photo: null);
appState = AppState.idle;
return true;
}
appState = AppState.idle;
return false;
}
Future<bool?> checkUsername(String username) async {
if (user.username == username) return true;
final result = await AuthApiService.instance.getRequiredActions(username);
if (result.statusCode == 404) return true; // موجود نیست => قابل استفاده
if (result.isSuccess) return false; // پیدا شد => تکراری
return null;
}
Future<void> editProfile(
String fullName,
String? username,
String? email,
) async {
appState = AppState.isolatedBusy;
final id = TokenStorage.userId ?? user.keycloakId;
if (id == null || id.isEmpty) {
appState = AppState.idle;
ActionSheetUtils(navigatorKey.currentContext!).showAlert(
AlertData(message: 'شناسه کاربر یافت نشد.'),
);
return;
}
final parts = fullName.trim().split(RegExp(r'\s+'));
final firstName = parts.isNotEmpty ? parts.first : '';
final lastName = parts.length > 1 ? parts.sublist(1).join(' ') : '';
final data = <String, dynamic>{
'firstName': firstName,
'lastName': lastName,
if (email != null) 'email': email,
};
final result = await AuthApiService.instance.updateUser(id: id, data: data);
if (result.isSuccess) {
user = user.copyWith(
fullName: fullName,
firstName: firstName,
lastName: lastName,
email: email,
username: username,
photo: user.photo,
);
appState = AppState.idle;
ActionSheetUtils(navigatorKey.currentContext!).pop();
ActionSheetUtils(navigatorKey.currentContext!).showAlert(
AlertData(
message: 'پروفایل با موفقیت ویرایش شد',
aLertType: ALertType.success,
),
);
return;
}
appState = AppState.idle;
ActionSheetUtils(navigatorKey.currentContext!).showAlert(
AlertData(
message: result.errorMessage ?? 'خطا در ویرایش پروفایل.',
),
);
}
static Future<void> changeItemMark(String type, int id, bool? value,
{String? description}) async {
_itemMarkQueue.add({
'type': type,
'id': id,
'value': value,
'description': description,
});
Future.delayed(const Duration(milliseconds: 500), () async {
final lastChange =
_itemMarkQueue.lastWhereOrNull((item) => item['id'] == id);
if (lastChange == null) return;
final service = RequestService(
RequestHelper.editItemBookmark(type, id),
body: {'description': lastChange['description']},
);
if (lastChange['value'] == true) {
await service.post();
} else if (lastChange['value'] == false) {
await service.delete();
} else {
service.put();
}
_itemMarkQueue.removeWhere((element) => element['id'] == id);
});
}
static Future<void> changeItemLiked(
String type,
int id,
bool value,
) async {
if (type == 'infography') {
type = 'banner';
}
final service = RequestService(
RequestHelper.editItemLike(type, id),
);
if (value) {
await service.post();
} else {
await service.delete();
}
}
static Future<void> changeStatisticMark(int id, bool value) async {
_statisticMarkQueue.add(MapEntry(id, value));
Future.delayed(const Duration(milliseconds: 500), () async {
final MapEntry? lastChange =
_statisticMarkQueue.lastWhereOrNull((item) => item.key == id);
if (lastChange == null) return;
final service = RequestService(RequestHelper.mark(id, 'statistic'));
if (lastChange.value) {
await service.post();
} else {
await service.delete();
}
_statisticMarkQueue.removeWhere((element) => element.key == id);
});
}
Future<bool> changePassword({
required String currentPassword,
required String newPassword,
}) async {
try {
final username = user.username;
if (username == null || username.isEmpty) {
return false;
}
final loginResult = await KeycloakAuthService.instance.login(
username: username,
password: currentPassword,
);
if (!loginResult.success) {
return false;
}
final id = TokenStorage.userId ?? user.keycloakId;
if (id == null || id.isEmpty) return false;
final result = await AuthApiService.instance.setOwnPassword(
id: id,
newPassword: newPassword,
);
return result.isSuccess;
} catch (e) {
print('Error changing password: $e');
return false;
}
}
}