import 'dart:convert'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import '../presentation/claims_overview/claims_overview_cache.dart'; import 'package:nhancepolicy/logger.dart'; class TokenStorageService { static final TokenStorageService _instance = TokenStorageService._internal(); factory TokenStorageService() => _instance; TokenStorageService._internal(); // 🔐 Secure storage instance static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(); // Storage keys static const String _preEnrollmentKey = 'pre_enrollment_data'; static const String _postEnrollmentKey = 'post_enrollment_data'; static const String _selectedBranchKey = 'selected_branch'; static const String _decodedTokenKey = 'decoded_token'; static const String _branchNameKey = 'branch_name'; // In-memory cache List? _preEnrollmentData; List? _postEnrollmentData; Map? _selectedBranch; Map? _decodedToken; // 🔄 Initialize from secure storage Future initialize() async { final preData = await _secureStorage.read(key: _preEnrollmentKey); if (preData != null) { _preEnrollmentData = json.decode(preData); } final postData = await _secureStorage.read(key: _postEnrollmentKey); if (postData != null) { _postEnrollmentData = json.decode(postData); } final branchData = await _secureStorage.read(key: _selectedBranchKey); if (branchData != null) { _selectedBranch = json.decode(branchData); } final tokenData = await _secureStorage.read(key: _decodedTokenKey); if (tokenData != null) { _decodedToken = json.decode(tokenData); } } // 💾 Save enrollment data Future saveEnrollmentData( List preData, List postData, ) async { _preEnrollmentData = preData; _postEnrollmentData = postData; await _secureStorage.write( key: _preEnrollmentKey, value: json.encode(preData)); await _secureStorage.write( key: _postEnrollmentKey, value: json.encode(postData)); } List> getCombinedBranches() { List> combined = []; Set seenTokens = {}; Set seenIds = {}; // Helper: check valid token bool hasValidToken(dynamic token) { return token != null && token.toString().trim().isNotEmpty; } // Add pre-enrollment data if (_preEnrollmentData != null) { for (var item in _preEnrollmentData!) { final token = item['token']; // ❌ SKIP if token is empty or null if (!hasValidToken(token)) continue; String uniqueId = '${item['id']}_${item['client_id']}_${item['client_branch_id']}'; if (seenIds.contains(uniqueId)) continue; if (token.isNotEmpty && seenTokens.contains(token)) continue; if (token.isNotEmpty) seenTokens.add(token); seenIds.add(uniqueId); combined.add({...item, 'enrollment_type': 'pre'}); } } // Add post-enrollment data if (_postEnrollmentData != null) { for (var item in _postEnrollmentData!) { final token = item['token']; // ❌ SKIP if token is empty or null if (!hasValidToken(token)) continue; String uniqueId = '${item['id']}_${item['client_id']}_${item['client_branch_id']}'; if (seenIds.contains(uniqueId)) continue; if (token.isNotEmpty && seenTokens.contains(token)) continue; if (token.isNotEmpty) seenTokens.add(token); seenIds.add(uniqueId); combined.add({...item, 'enrollment_type': 'post'}); } } return combined; } // 🌿 Save selected branch + decode JWT Future saveSelectedBranch(Map branch) async { _selectedBranch = branch; String token = branch['token']?.toString() ?? ''; _decodedToken = token.isNotEmpty ? _decodeJWT(token) : null; await _secureStorage.write( key: _selectedBranchKey, value: json.encode(branch)); if (_decodedToken != null) { await _secureStorage.write( key: _decodedTokenKey, value: json.encode(_decodedToken)); } await _secureStorage.write( key: _branchNameKey, value: branch['branch_name']?.toString() ?? '', ); // Prefer claims_sub_menu from branch payload when available. final claimsSubMenu = branch['claims_sub_menu']; if (claimsSubMenu is List) { await _secureStorage.write( key: 'claims_sub_menu', value: jsonEncode(claimsSubMenu), ); } } // 🔓 Decode JWT Map? _decodeJWT(String token) { try { final parts = token.split('.'); if (parts.length != 3) return null; final payload = base64Url.normalize(parts[1]); final decoded = utf8.decode(base64Url.decode(payload)); return json.decode(decoded); } catch (e) { logDebug('JWT decode error: $e'); return null; } } // Getters Map? getSelectedBranch() => _selectedBranch; Map? getDecodedToken() => _decodedToken; String? getCurrentToken() => _selectedBranch?['token']; bool isLoggedIn() => _selectedBranch != null && _selectedBranch!['token'] != null && _selectedBranch!['token'].toString().isNotEmpty; // 🚪 Logout (clear everything) Future clearAll() async { _preEnrollmentData = null; _postEnrollmentData = null; _selectedBranch = null; _decodedToken = null; await _secureStorage.deleteAll(); } Future saveDecodedSessionData( Map decodedToken, String token, ) async { // Handle allowed_modules safely dynamic allowedModules = decodedToken['allowed_modules']; if (allowedModules is String) { allowedModules = jsonDecode(allowedModules); } await _secureStorage.write( key: 'empClientBranchId', value: decodedToken['post_branch_id']?.toString()); await _secureStorage.write( key: 'empPrimaryId', value: decodedToken['post_hr_id']?.toString()); await _secureStorage.write( key: 'empClientId', value: decodedToken['post_client_id']?.toString()); await _secureStorage.write( key: 'empHrId', value: decodedToken['post_hr_id']?.toString()); await _secureStorage.write( key: 'empAllowed_modules', value: jsonEncode(allowedModules?['post'] ?? [])); // ================= PRE (Enrollment) ================= await _secureStorage.write( key: 'enrollmentEmpClientBranchId', value: decodedToken['pre_branch_id']?.toString()); await _secureStorage.write( key: 'enrollmentEmpPrimaryId', value: decodedToken['pre_hr_id']?.toString()); await _secureStorage.write( key: 'enrollmentClient_id', value: decodedToken['pre_client_id']?.toString()); await _secureStorage.write( key: 'enrollmentHrId', value: decodedToken['pre_hr_id']?.toString()); await _secureStorage.write( key: 'enrollmentAllowed_modules', value: jsonEncode(allowedModules?['pre'] ?? [])); // ================= TOKEN ================= await _secureStorage.write(key: 'token', value: token); // ================= CLAIMS SUB MENU ================= if (decodedToken.containsKey('claims_sub_menu')) { final claimsSubMenu = decodedToken['claims_sub_menu']; await _secureStorage.write( key: 'claims_sub_menu', value: jsonEncode(claimsSubMenu is List ? claimsSubMenu : []), ); } } Future readValue(String key) async { return await _secureStorage.read(key: key); } Future writeValue(String key, String value) async { await _secureStorage.write(key: key, value: value); } Future removeValue(String key) async { await _secureStorage.delete(key: key); } Future clearBranchSession() async { final keysToRemove = [ 'selected_branch', 'decoded_token', 'clientLogo', 'clientName', 'empAllowed_modules', 'empClientBranchId', 'empClientId', 'empEmail', 'empHrId', 'empPrimaryId', 'enrollmentAllowed_modules', 'enrollmentClient_id', 'enrollmentEmpClientBranchId', 'enrollmentEmpPrimaryId', 'enrollmentHrId', 'token', 'claims_sub_menu', ]; for (final key in keysToRemove) { await _secureStorage.delete(key: key); } } Future resetSessionAndSwitchBranch( Map newBranch, ) async { // 1️⃣ Clear ONLY branch/session related keys await clearBranchSession(); // 2️⃣ Clear claims overview cache so the dashboard fetches fresh data await ClaimsOverviewCache.clearAll(); // 3️⃣ Save selected branch await saveSelectedBranch(newBranch); // 3️⃣ Rebuild decoded session data from token final token = newBranch['token']?.toString(); if (token != null && token.isNotEmpty) { final decoded = _decodeJWT(token); if (decoded != null) { await saveDecodedSessionData(decoded, token); } } // 4️⃣ Update in-memory cache _selectedBranch = newBranch; } }