import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import '../../core/config/sync_config.dart'; import 'app_database.dart'; import 'catalogue_dao.dart'; /// Persists how this terminal reaches the back office. /// /// Split deliberately across two stores. Which broker, on which port, over TLS /// — that is configuration, and it goes in the database where it can be read /// during support. The username, password and API key are credentials, and go /// to the platform keystore: Keychain on macOS, Credential Manager on Windows, /// the Android Keystore on a tablet. /// /// Writing them into SQLite would put them in the same file as the bills, on a /// machine behind a shop counter, readable by anything that can open it. class SyncConfigStore { SyncConfigStore(this._catalogue, {FlutterSecureStorage? secureStorage}) : _secure = secureStorage ?? const FlutterSecureStorage(); final CatalogueDao _catalogue; final FlutterSecureStorage _secure; static const _kUsername = 'sync.username'; static const _kPassword = 'sync.password'; static const _kApiKey = 'sync.api_key'; /// Reads the stored configuration, falling back to [fallback] per field. /// /// The fallback carries the terminal's own store and terminal ids, which are /// never overwritten from here — they belong to the device's identity. Future load(SyncConfig fallback) async { final transport = await _catalogue.meta(MetaKeys.syncTransport); final host = await _catalogue.meta(MetaKeys.syncBrokerHost); final port = await _catalogue.meta(MetaKeys.syncBrokerPort); final tls = await _catalogue.meta(MetaKeys.syncUseTls); final httpUrl = await _catalogue.meta(MetaKeys.syncHttpBaseUrl); final credentials = await _readCredentials(); return fallback.copyWith( transport: TransportKind.values .where((k) => k.name == transport) .firstOrNull ?? fallback.transport, brokerHost: host ?? fallback.brokerHost, brokerPort: int.tryParse(port ?? '') ?? fallback.brokerPort, useTls: tls == null ? fallback.useTls : tls == '1', httpBaseUrl: httpUrl ?? fallback.httpBaseUrl, username: credentials.username, password: credentials.password, apiKey: credentials.apiKey, ); } Future save(SyncConfig config) async { await _catalogue.setMeta(MetaKeys.syncTransport, config.transport.name); await _catalogue.setMeta(MetaKeys.syncBrokerHost, config.brokerHost); await _catalogue.setMeta(MetaKeys.syncBrokerPort, '${config.brokerPort}'); await _catalogue.setMeta(MetaKeys.syncUseTls, config.useTls ? '1' : '0'); await _catalogue.setMeta(MetaKeys.syncHttpBaseUrl, config.httpBaseUrl); await _writeSecret(_kUsername, config.username); await _writeSecret(_kPassword, config.password); await _writeSecret(_kApiKey, config.apiKey); } Future<({String? username, String? password, String? apiKey})> _readCredentials() async { try { return ( username: await _secure.read(key: _kUsername), password: await _secure.read(key: _kPassword), apiKey: await _secure.read(key: _kApiKey), ); } on Object catch (e) { // No keystore — a headless test host, or a Linux box with no secret // service. The terminal still runs; it just cannot authenticate until // someone re-enters the credentials, which is the safe way to fail. debugPrint('Secure storage unavailable, credentials not loaded: $e'); return (username: null, password: null, apiKey: null); } } Future _writeSecret(String key, String? value) async { try { if (value == null || value.isEmpty) { await _secure.delete(key: key); } else { await _secure.write(key: key, value: value); } } on Object catch (e) { debugPrint('Could not persist $key to secure storage: $e'); } } /// Wipes stored credentials. Used when a terminal is handed on or re-pointed /// at a different back office. Future clearCredentials() async { for (final key in [_kUsername, _kPassword, _kApiKey]) { await _writeSecret(key, null); } } }