import 'dart:convert'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/installation.dart'; /// Persistance des installations enregistrées, **indexée par Server UUID**. /// /// Séparation des supports (décision verrouillée) : /// • **métadonnées** (`uuid/name/host/port/lastSeen`) → SharedPreferences ; /// • **token** (credential) → `flutter_secure_storage` (Keychain/Keystore), /// clé `etm_token_`. /// /// Aucune connexion ici : c'est le magasin que pilotera le `ConnectionManager` /// (Étape 2). Le piège mono→multi (réutiliser le token d'une autre box) est /// évité par construction : le token est lu/écrit **par UUID**. class InstallationStore { static const _listKey = 'etm_installations_v1'; static const _tokenPrefix = 'etm_token_'; final FlutterSecureStorage _secure; InstallationStore({FlutterSecureStorage? secureStorage}) : _secure = secureStorage ?? const FlutterSecureStorage(); final Map _byUuid = {}; /// Installations connues, les plus récemment vues d'abord. List get installations { final list = _byUuid.values.toList(); list.sort((a, b) { final ad = a.lastSeen, bd = b.lastSeen; if (ad == null && bd == null) return a.name.compareTo(b.name); if (ad == null) return 1; if (bd == null) return -1; return bd.compareTo(ad); }); return list; } Installation? byUuid(String uuid) => _byUuid[uuid]; /// Charge les métadonnées (prefs) puis hydrate chaque token (secure storage). Future load() async { final prefs = await SharedPreferences.getInstance(); _byUuid.clear(); final raw = prefs.getString(_listKey); if (raw == null) return; final decoded = jsonDecode(raw); if (decoded is! List) return; for (final entry in decoded) { if (entry is! Map) continue; final meta = Installation.fromJson(Map.from(entry)); // Le token est un bonus : son indispo (web, plateforme sans secure // storage) ne doit jamais empêcher le chargement des métadonnées. final token = await _readToken(meta.uuid); _byUuid[meta.uuid] = token == null ? meta : meta.copyWith(token: token); } } /// Crée ou met à jour une installation (métadonnées + token si présent). Future upsert(Installation inst) async { _byUuid[inst.uuid] = inst; await _persistMeta(); if (inst.hasToken) { await _writeToken(inst.uuid, inst.token!); } } /// Retire une installation et **efface son token** du secure storage. Future remove(String uuid) async { _byUuid.remove(uuid); await _persistMeta(); await _deleteToken(uuid); } /// Token stocké pour cet UUID (lecture directe secure storage, tolérante). Future tokenFor(String uuid) => _readToken(uuid); // ── Accès secure storage tolérants aux pannes (async + faillible) ────────── Future _readToken(String uuid) async { try { return await _secure.read(key: _tokenPrefix + uuid); } catch (_) { return null; // dégradation propre : pas de token, pas de crash } } Future _writeToken(String uuid, String token) async { try { await _secure.write(key: _tokenPrefix + uuid, value: token); } catch (_) { // best-effort : la session reste valide en mémoire pour cette exécution } } Future _deleteToken(String uuid) async { try { await _secure.delete(key: _tokenPrefix + uuid); } catch (_) {/* idempotent */} } Future _persistMeta() async { final prefs = await SharedPreferences.getInstance(); final list = _byUuid.values.map((i) => i.toJson()).toList(); await prefs.setString(_listKey, jsonEncode(list)); } }