Files
zapstore/lib/services/secure_storage_service.dart
T
Henrique Velloso 84906164bb feat: auto-backup installed apps on install/uninstall
Back up the list of installed apps to Nostr (encrypted AppStack) when
batch installs complete or apps are uninstalled. Toggle in profile, off
by default. Restore screen fetches backup and lets users reinstall apps.

Changes by file:

- installed_apps_backup_service: Listener for batch completion and
  installed-list changes; debounced backup (3s); restore-only batches
  ignored; timer cancelled on provider dispose; logs reduced to errors
  and completion only; null-aware assignment for baseline keys.

- secure_storage_service: Persistence for backup-enabled toggle (off
  by default).

- profile_screen: Toggle to enable/disable backup; entry point for
  restore flow.

- restore_installed_apps_screen: Restore UI with shared AppBar; lists
  apps to install and already installed; cards navigate to app detail.

- main_scaffold: Watches backup listener provider so it stays active.

- package_manager: Removed unused installSourceProvider; install source
  handling for restore operations.

- install_operation, android_package_manager, install_button,
  batch_progress_banner: InstallSource.restore support for restore flow.

- app_card: Factory constructors (forRestore, forDiscovery, etc.) to
  simplify restore and other screens.

- router, installed_packages_snapshot: Supporting wiring for the feature.
2026-02-26 18:27:41 -03:00

184 lines
6.1 KiB
Dart

import 'dart:convert';
import 'package:amber_signer/amber_signer.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
/// Service for securely storing sensitive data (NWC connection strings,
/// app catalog relays) using platform-native secure storage
/// (Keychain on iOS, KeyStore on Android).
///
/// This does NOT require user authentication - data is encrypted at rest
/// by the platform's secure storage mechanism.
class SecureStorageService {
SecureStorageService();
// Use explicit options for reliability across platforms
static final _storage = FlutterSecureStorage(
aOptions: const AndroidOptions(encryptedSharedPreferences: true),
iOptions: const IOSOptions(
accessibility: KeychainAccessibility.first_unlock,
),
);
static const _nwcKey = 'nwc_connection_string';
static const _appCatalogRelaysKey = 'app_catalog_relays';
/// Get the stored NWC connection string
Future<String?> getNWCString() async {
final value = await _storage.read(key: _nwcKey);
return (value?.isNotEmpty == true) ? value : null;
}
/// Store an NWC connection string
Future<void> setNWCString(String connectionString) async {
await _storage.write(key: _nwcKey, value: connectionString);
}
/// Clear the stored NWC connection string
Future<void> clearNWCString() async {
await _storage.delete(key: _nwcKey);
}
/// Check if an NWC connection string is stored
Future<bool> hasNWCString() async {
final value = await _storage.read(key: _nwcKey);
return value?.isNotEmpty == true;
}
// =========================================================================
// App Open Tracking (for background notification throttling)
// =========================================================================
static const _lastAppOpenedKey = 'last_app_opened';
/// Get the last time the user opened the app.
Future<DateTime?> getLastAppOpenedTime() async {
final value = await _storage.read(key: _lastAppOpenedKey);
if (int.tryParse(value ?? '') case final ms?) {
return DateTime.fromMillisecondsSinceEpoch(ms);
}
return null;
}
/// Store the last app opened time.
Future<void> setLastAppOpenedTime(DateTime time) async {
await _storage.write(
key: _lastAppOpenedKey,
value: '${time.millisecondsSinceEpoch}',
);
}
// =========================================================================
// Seen Until Timestamp (for background notification deduplication)
// =========================================================================
static const _seenUntilKey = 'seen_until';
/// Get the "seen until" timestamp.
/// Updates with release.createdAt <= this timestamp have already been notified.
Future<DateTime?> getSeenUntil() async {
final value = await _storage.read(key: _seenUntilKey);
if (int.tryParse(value ?? '') case final ms?) {
return DateTime.fromMillisecondsSinceEpoch(ms);
}
return null;
}
/// Store the "seen until" timestamp.
/// Called when a notification is shown, set to now() so future checks
/// only notify about releases created after this time.
Future<void> setSeenUntil(DateTime time) async {
await _storage.write(
key: _seenUntilKey,
value: '${time.millisecondsSinceEpoch}',
);
}
// =========================================================================
// App Catalog Relays
// =========================================================================
/// Get the stored app catalog relay URLs.
///
/// Returns null if no relays have been stored (use defaults).
/// Returns empty set if user explicitly cleared all relays (invalid state,
/// but handled gracefully).
Future<Set<String>?> getAppCatalogRelays() async {
final json = await _storage.read(key: _appCatalogRelaysKey);
if (json == null || json.isEmpty) return null;
try {
final list = jsonDecode(json) as List;
return Set<String>.from(list.cast<String>());
} catch (e) {
// Corrupted data - treat as unset
return null;
}
}
/// Store app catalog relay URLs.
///
/// This is the local source of truth for relay configuration,
/// used to initialize the app before sign-in.
Future<void> setAppCatalogRelays(Set<String> relays) async {
await _storage.write(
key: _appCatalogRelaysKey,
value: jsonEncode(relays.toList()),
);
}
// =========================================================================
// Installed Apps Backup
// =========================================================================
static const _backupEnabledKey = 'installed_apps_backup_enabled';
/// Whether installed apps backup is enabled (off by default).
Future<bool> getBackupEnabled() async {
final value = await _storage.read(key: _backupEnabledKey);
return value == 'true';
}
/// Store the installed apps backup toggle state.
Future<void> setBackupEnabled(bool enabled) async {
await _storage.write(
key: _backupEnabledKey,
value: enabled.toString(),
);
}
}
/// Persists the AmberSigner pubkey in flutter_secure_storage.
/// This survives app data clears (database deletion) and is encrypted.
class SecureStoragePubkeyPersistence implements AmberPubkeyPersistence {
static const _key = 'amber_pubkey';
@override
Future<void> persistPubkey(String pubkey) async {
await SecureStorageService._storage.write(key: _key, value: pubkey);
}
@override
Future<String?> loadPubkey() async {
return SecureStorageService._storage.read(key: _key);
}
@override
Future<void> clearPubkey() async {
await SecureStorageService._storage.delete(key: _key);
}
}
final secureStorageServiceProvider = Provider<SecureStorageService>(
(ref) => SecureStorageService(),
);
/// Whether an NWC connection string is currently stored.
///
/// Use `ref.invalidate(hasNwcStringProvider)` after updating or clearing the
/// stored string to refresh UI.
final hasNwcStringProvider = FutureProvider.autoDispose<bool>((ref) async {
final secureStorage = ref.watch(secureStorageServiceProvider);
return secureStorage.hasNWCString();
});