mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
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.
184 lines
6.1 KiB
Dart
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();
|
|
});
|