// 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 _statisticMarkQueue = []; static final List _itemMarkQueue = []; Future 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 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 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 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 _userinfoToAppShape(Map 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 _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 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 setProfilePhoto(dynamic file) async { // TODO(auth-followup): پیاده‌سازی آپلود به s3-service و سپس فراخوانی // AuthApiService.instance.setProfilePicture(id, key). appState = AppState.failed; return false; } Future 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 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 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 = { '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 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 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 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 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; } } }