From 7c6ea8c8dd8c6097526da964cb5397f10c993343 Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Wed, 15 Jul 2026 13:53:44 -0300 Subject: [PATCH] Remove broken installed app backup feature --- lib/constants/app_constants.dart | 3 - lib/screens/profile_screen.dart | 56 ++------------- lib/services/device_backup_service.dart | 54 --------------- lib/services/settings_service.dart | 11 --- lib/services/updates_service.dart | 68 ------------------- lib/widgets/legacy_installed_apps_dialog.dart | 61 ----------------- .../WORK-025-remove-installed-apps-backup.md | 51 ++++++++++++++ test/services/settings_service_test.dart | 6 +- 8 files changed, 58 insertions(+), 252 deletions(-) delete mode 100644 lib/widgets/legacy_installed_apps_dialog.dart create mode 100644 spec/work/WORK-025-remove-installed-apps-backup.md diff --git a/lib/constants/app_constants.dart b/lib/constants/app_constants.dart index e23b4b7..95f6a21 100644 --- a/lib/constants/app_constants.dart +++ b/lib/constants/app_constants.dart @@ -22,9 +22,6 @@ const kZapstoreCommunityPubkey = /// Identifier for storing user saved apps const kAppBookmarksIdentifier = 'zapstore-bookmarks'; -/// Identifier for the encrypted stack of installed apps -const kInstalledAppsIdentifier = 'zapstore-installed-apps'; - /// Identifier for the encrypted stack of apps the user chose as unmanaged const kUnmanagedAppsIdentifier = 'zapstore-unmanaged-apps'; diff --git a/lib/screens/profile_screen.dart b/lib/screens/profile_screen.dart index 2ca87bf..fc62f60 100644 --- a/lib/screens/profile_screen.dart +++ b/lib/screens/profile_screen.dart @@ -16,7 +16,6 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:purplebase/purplebase.dart'; import 'package:zapstore/main.dart'; import 'package:zapstore/services/device_key_service.dart'; -import 'package:zapstore/services/device_private_event_service.dart'; import 'package:zapstore/services/device_state_service.dart'; import 'package:zapstore/services/log_service.dart' as app_logs; import 'package:zapstore/services/package_manager/package_manager.dart'; @@ -46,7 +45,7 @@ class ProfileScreen extends ConsumerWidget { const SizedBox(height: 24), - // User Stacks Section (Saved Apps + Installed Apps) + // User Stacks Section const _UserStacksSection(), const SizedBox(height: 24), @@ -1655,46 +1654,6 @@ class _BackgroundAutoUpdatesToggle extends ConsumerWidget { } } -class _InstalledAppsBackupToggle extends ConsumerWidget { - @override - Widget build(BuildContext context, WidgetRef ref) { - final devicePubkey = ref.watch(devicePubkeyProvider); - if (devicePubkey == null) return const SizedBox.shrink(); - - final settingsAsync = ref.watch(localSettingsProvider); - final ready = ref.watch(deviceStateProvider).isReady; - final enabled = - settingsAsync.valueOrNull?.installedAppsBackupEnabled ?? false; - - return SwitchListTile( - secondary: CircleAvatar( - radius: 18, - backgroundColor: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.12), - child: Icon( - Icons.cloud_upload, - color: Theme.of(context).colorScheme.primary, - size: 20, - ), - ), - title: const Text('Back up installed apps'), - value: enabled, - contentPadding: EdgeInsets.zero, - onChanged: !ready - ? null - : (value) async { - await ref - .read(deviceStateProvider.notifier) - .updatePortable( - (s) => s.copyWith(installedAppsBackupEnabled: value), - ); - ref.invalidate(localSettingsProvider); - }, - ); - } -} - class _DataManagementSection extends ConsumerWidget { const _DataManagementSection(); @@ -1713,8 +1672,6 @@ class _DataManagementSection extends ConsumerWidget { const SizedBox(height: 16), const _DeviceKeyCard(), const SizedBox(height: 8), - _InstalledAppsBackupToggle(), - const SizedBox(height: 8), const _BackgroundAutoUpdatesToggle(), const SizedBox(height: 8), ListTile( @@ -1864,7 +1821,10 @@ class _UserStacksSection extends ConsumerWidget { // Filter to only encrypted (private) stacks final privateStacks = stacksState.models - .where((s) => s.content.isNotEmpty) + .where( + (s) => + s.content.isNotEmpty && s.identifier != 'zapstore-installed-apps', + ) .toList(); // Don't show section if no private stacks exist @@ -1872,12 +1832,10 @@ class _UserStacksSection extends ConsumerWidget { return const SizedBox.shrink(); } - // Sort: Saved Apps first, then Installed Apps, then others alphabetically + // Sort: Saved Apps first, then Unmanaged Apps, then others alphabetically privateStacks.sort((a, b) { if (a.identifier == kAppBookmarksIdentifier) return -1; if (b.identifier == kAppBookmarksIdentifier) return 1; - if (a.identifier == kInstalledAppsIdentifier) return -1; - if (b.identifier == kInstalledAppsIdentifier) return 1; if (a.identifier == kUnmanagedAppsIdentifier) return -1; if (b.identifier == kUnmanagedAppsIdentifier) return 1; return (a.name ?? a.identifier).compareTo(b.name ?? b.identifier); @@ -1899,8 +1857,6 @@ class _UserStacksSection extends ConsumerWidget { final stack = entry.value; final displayName = stack.identifier == kAppBookmarksIdentifier ? 'Saved Apps' - : stack.identifier == kInstalledAppsIdentifier - ? 'Installed Apps' : stack.identifier == kUnmanagedAppsIdentifier ? 'Unmanaged Apps' : null; diff --git a/lib/services/device_backup_service.dart b/lib/services/device_backup_service.dart index 54e6e58..57e0b5f 100644 --- a/lib/services/device_backup_service.dart +++ b/lib/services/device_backup_service.dart @@ -16,7 +16,6 @@ import 'package:zapstore/services/log_service.dart'; import 'package:zapstore/services/settings_service.dart'; import 'package:zapstore/widgets/device_backup_dialog.dart'; import 'package:zapstore/widgets/device_restore_dialog.dart'; -import 'package:zapstore/widgets/legacy_installed_apps_dialog.dart'; /// Backs up the device key to the active Amber identity and restores it again. /// @@ -127,45 +126,6 @@ class DeviceBackupService { await signer.signIn(setAsActive: false); ref.read(devicePubkeyProvider.notifier).state = pubkey; } - - Future> fetchLegacyInstalledAppIds({ - required Ref ref, - required Signer amberSigner, - }) async { - final results = await ref - .read(storageNotifierProvider.notifier) - .query( - RequestFilter( - authors: {amberSigner.pubkey}, - tags: { - '#d': {kInstalledAppsIdentifier}, - }, - limit: 1, - ).toRequest(), - source: const LocalAndRemoteSource( - relays: 'AppCatalog', - stream: false, - ), - subscriptionPrefix: 'app-legacy-installed-recovery', - ); - final stack = results.firstOrNull; - if (stack == null || !verifySignedEvent(ref, stack.event)) return const []; - try { - final plaintext = await amberSigner.nip44Decrypt( - stack.content, - amberSigner.pubkey, - ); - final decoded = jsonDecode(plaintext); - return decoded is List - ? decoded - .whereType() - .where((id) => id.startsWith('32267:')) - .toList() - : const []; - } catch (_) { - return const []; - } - } } final deviceBackupServiceProvider = Provider( @@ -257,20 +217,6 @@ Future maybeOfferDeviceBackup(Ref ref) async { } } } - final legacyInstalledApps = await service.fetchLegacyInstalledAppIds( - ref: ref, - amberSigner: amberSigner, - ); - if (legacyInstalledApps.isNotEmpty) { - final context = rootNavigatorKey.currentState?.overlay?.context; - if (context != null && context.mounted) { - await showDialog( - context: context, - builder: (_) => - LegacyInstalledAppsDialog(appIds: legacyInstalledApps), - ); - } - } await settings.saveTemp(temp.copyWith(restoreOnboardingComplete: true)); } await service.backupDeviceKey(ref: ref, amberSigner: amberSigner); diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 6c0dd53..be510e2 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -12,20 +12,16 @@ const _storage = FlutterSecureStorage( /// Portable preferences, mirrored by DeviceStateService. class PortableSettings { - final bool installedAppsBackupEnabled; final bool backgroundAutoUpdatesEnabled; final Set trustedSigners; const PortableSettings({ - this.installedAppsBackupEnabled = false, this.backgroundAutoUpdatesEnabled = false, this.trustedSigners = const {}, }); factory PortableSettings.fromJson(Map json) { return PortableSettings( - installedAppsBackupEnabled: - json['installedAppsBackupEnabled'] as bool? ?? false, backgroundAutoUpdatesEnabled: json['backgroundAutoUpdatesEnabled'] as bool? ?? false, trustedSigners: @@ -35,18 +31,14 @@ class PortableSettings { } Map toJson() => { - 'installedAppsBackupEnabled': installedAppsBackupEnabled, 'backgroundAutoUpdatesEnabled': backgroundAutoUpdatesEnabled, 'trustedSigners': trustedSigners.toList()..sort(), }; PortableSettings copyWith({ - bool? installedAppsBackupEnabled, bool? backgroundAutoUpdatesEnabled, Set? trustedSigners, }) => PortableSettings( - installedAppsBackupEnabled: - installedAppsBackupEnabled ?? this.installedAppsBackupEnabled, backgroundAutoUpdatesEnabled: backgroundAutoUpdatesEnabled ?? this.backgroundAutoUpdatesEnabled, trustedSigners: trustedSigners ?? this.trustedSigners, @@ -120,7 +112,6 @@ class LocalSettings { DateTime? get seenUntil => temp.seenUntil; DateTime? get deletionSyncedUntil => temp.deletionSyncedUntil; LogLevel get logLevel => temp.logLevel; - bool get installedAppsBackupEnabled => portable.installedAppsBackupEnabled; bool get backgroundAutoUpdatesEnabled => portable.backgroundAutoUpdatesEnabled; Set get trustedSigners => portable.trustedSigners; @@ -131,7 +122,6 @@ class LocalSettings { DateTime? lastAppOpened, DateTime? seenUntil, DateTime? deletionSyncedUntil, - bool? installedAppsBackupEnabled, bool? backgroundAutoUpdatesEnabled, Set? trustedSigners, LogLevel? logLevel, @@ -142,7 +132,6 @@ class LocalSettings { ? null : (nwcConnectionString ?? this.nwcConnectionString), portable: portable.copyWith( - installedAppsBackupEnabled: installedAppsBackupEnabled, backgroundAutoUpdatesEnabled: backgroundAutoUpdatesEnabled, trustedSigners: trustedSigners, ), diff --git a/lib/services/updates_service.dart b/lib/services/updates_service.dart index bd06a63..eb71989 100644 --- a/lib/services/updates_service.dart +++ b/lib/services/updates_service.dart @@ -7,8 +7,6 @@ import 'package:purplebase/purplebase.dart'; import 'package:zapstore/services/catalog_fetcher.dart'; import 'package:zapstore/services/log_service.dart'; import 'package:zapstore/services/deletion_processor.dart'; -import 'package:zapstore/services/device_key_service.dart'; -import 'package:zapstore/services/device_private_event_service.dart'; import 'package:zapstore/services/package_manager/package_manager.dart'; import 'package:zapstore/services/unmanaged_apps_service.dart'; import 'package:zapstore/services/settings_service.dart'; @@ -109,7 +107,6 @@ class UpdatePollerNotifier extends StateNotifier { final Ref ref; Timer? _pollTimer; - List _lastBackedUpIds = []; void _init() { // Hydrate from local storage as soon as SQLite is ready — no network. @@ -173,7 +170,6 @@ class UpdatePollerNotifier extends StateNotifier { clearError: true, hasHydrated: true, ); - unawaited(_backupInstalledApps()); } catch (e, st) { LogService.I.warn( 'update check failed', @@ -280,70 +276,6 @@ class UpdatePollerNotifier extends StateNotifier { } } - /// Best-effort backup of installed apps as an encrypted private stack. - /// Runs after each successful update check. Only publishes when the set - /// of cataloged installed apps has changed since the last backup. - Future _backupInstalledApps() async { - final devicePubkey = ref.read(devicePubkeyProvider); - if (devicePubkey == null) return; - - final settings = await ref.read(settingsServiceProvider).load(); - if (!settings.installedAppsBackupEnabled) return; - - final pmNotifier = ref.read(packageManagerProvider.notifier); - final installed = ref.read(packageManagerProvider).installed; - final platform = pmNotifier.platform; - final storage = ref.read(storageNotifierProvider.notifier); - - try { - final apps = await storage.query( - RequestFilter( - tags: { - '#d': installed.keys.toSet(), - '#f': {platform}, - }, - ).toRequest(), - source: const LocalSource(), - subscriptionPrefix: 'app-backup-resolve', - ); - - final appIds = - apps - .map((a) => '${a.event.kind}:${a.event.pubkey}:${a.identifier}') - .toList() - ..sort(); - - if (_listEquals(appIds, _lastBackedUpIds)) return; - - final partialStack = PartialAppStack.withEncryptedApps( - name: 'Installed Apps', - identifier: kInstalledAppsIdentifier, - apps: appIds, - platform: platform, - ); - - await ref - .read(devicePrivateEventServiceProvider) - .saveDraftAndQueue(partialStack); - _lastBackedUpIds = appIds; - } catch (e, st) { - LogService.I.warn( - 'installed apps backup failed', - tag: 'updates', - err: e, - stack: st, - ); - } - } - - static bool _listEquals(List a, List b) { - if (a.length != b.length) return false; - for (var i = 0; i < a.length; i++) { - if (a[i] != b[i]) return false; - } - return true; - } - @override void dispose() { _pollTimer?.cancel(); diff --git a/lib/widgets/legacy_installed_apps_dialog.dart b/lib/widgets/legacy_installed_apps_dialog.dart deleted file mode 100644 index 0816bef..0000000 --- a/lib/widgets/legacy_installed_apps_dialog.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:models/models.dart'; -import 'package:zapstore/widgets/app_stack_container.dart'; -import 'package:zapstore/widgets/common/base_dialog.dart'; -import 'package:zapstore/widgets/install_button.dart'; - -/// Lets a restored device install apps from an Amber-era installed-app backup. -class LegacyInstalledAppsDialog extends ConsumerWidget { - const LegacyInstalledAppsDialog({super.key, required this.appIds}); - - final List appIds; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final (:authors, :identifiers) = decomposeAddressableIds(appIds); - final appsState = ref.watch( - query( - authors: authors, - tags: {'#d': identifiers}, - source: const LocalAndRemoteSource(relays: 'AppCatalog', stream: false), - subscriptionPrefix: 'app-legacy-installed-recovery', - ), - ); - final apps = appsState.models.toList(); - - return BaseDialog( - title: const BaseDialogTitle('Restore apps'), - titleIcon: const Icon(Icons.restore, size: 20), - content: BaseDialogContent( - children: [ - Text( - 'Found ${appIds.length} apps from your previous device. ' - 'Choose which ones to install.', - ), - const SizedBox(height: 12), - if (appsState is StorageLoading && apps.isEmpty) - const Center(child: CircularProgressIndicator()) - else if (apps.isEmpty) - const Text( - 'App details are unavailable right now. Try again later.', - ) - else - ...apps.map( - (app) => ListTile( - contentPadding: EdgeInsets.zero, - title: Text(app.name ?? app.identifier), - trailing: InstallButton(app: app, compact: true), - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Done'), - ), - ], - ); - } -} diff --git a/spec/work/WORK-025-remove-installed-apps-backup.md b/spec/work/WORK-025-remove-installed-apps-backup.md new file mode 100644 index 0000000..55fdcdc --- /dev/null +++ b/spec/work/WORK-025-remove-installed-apps-backup.md @@ -0,0 +1,51 @@ +# WORK-025 — Remove Installed Apps Backup + +**Feature:** FEAT-006-device-key.md +**Status:** In Progress + +## Tasks + +- [x] 1. Remove the Installed Apps private-stack and backup setting UI. + - Files: `lib/screens/profile_screen.dart` +- [x] 2. Remove Installed Apps backup persistence and periodic publishing. + - Files: `lib/services/settings_service.dart`, `lib/services/updates_service.dart` +- [x] 3. Remove legacy Amber Installed Apps recovery. + - Files: `lib/services/device_backup_service.dart`, `lib/widgets/legacy_installed_apps_dialog.dart` +- [x] 4. Remove obsolete identifiers and update tests. + - Files: `lib/constants/app_constants.dart`, `test/services/settings_service_test.dart` +- [ ] 5. Self-review against the local-first, lifecycle, and data-robustness invariants. + +## Test Coverage + +| Scenario | Expected | Status | +|----------|----------|--------| +| Profile with old Installed Apps stack | Legacy stack is hidden | [x] | +| Portable settings round-trip | Remaining settings serialize and deserialize | [x] | +| Update refresh | Updates continue without publishing an Installed Apps stack | [x] | +| Legacy device restore | Device-key restore completes without Installed Apps recovery UI | [x] | + +## Decisions + +### 2026-07-15 — Preserve installation and update management + +**Context:** Installed Apps backup is separate from package scanning, installation, and the Updates screen. +**Decision:** Remove only the private-stack backup, profile presentation, and legacy recovery paths. +**Rationale:** This preserves core app distribution behavior while eliminating the deprecated backup feature. + +### 2026-07-15 — Hide persisted legacy stacks + +**Context:** Existing devices may still contain `zapstore-installed-apps` events. +**Decision:** Filter the legacy identifier from the private-stack profile list after removing new writes. +**Rationale:** Old data should not reappear as an unnamed or unsupported feature. + +## Spec Issues + +_None_ + +## Progress Notes + +**2026-07-15:** Removed the backup UI, setting, update publisher, and legacy Amber recovery path. Existing legacy stacks are hidden from the profile. `fvm flutter analyze` and `HOME=/tmp fvm flutter test` pass. + +## On Merge + +Delete this work packet. Promote any non-obvious decision to `spec/knowledge/` if needed. diff --git a/test/services/settings_service_test.dart b/test/services/settings_service_test.dart index 32c1ffc..8d4652d 100644 --- a/test/services/settings_service_test.dart +++ b/test/services/settings_service_test.dart @@ -22,13 +22,9 @@ void main() { }); test('uses lower camel case portable JSON keys', () { - const settings = PortableSettings( - installedAppsBackupEnabled: true, - trustedSigners: {'a'}, - ); + const settings = PortableSettings(trustedSigners: {'a'}); expect(settings.toJson(), { - 'installedAppsBackupEnabled': true, 'backgroundAutoUpdatesEnabled': false, 'trustedSigners': ['a'], });