mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
Remove broken installed app backup feature
This commit is contained in:
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<List<String>> fetchLegacyInstalledAppIds({
|
||||
required Ref ref,
|
||||
required Signer amberSigner,
|
||||
}) async {
|
||||
final results = await ref
|
||||
.read(storageNotifierProvider.notifier)
|
||||
.query(
|
||||
RequestFilter<AppStack>(
|
||||
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<String>()
|
||||
.where((id) => id.startsWith('32267:'))
|
||||
.toList()
|
||||
: const [];
|
||||
} catch (_) {
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final deviceBackupServiceProvider = Provider<DeviceBackupService>(
|
||||
@@ -257,20 +217,6 @@ Future<void> 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<void>(
|
||||
context: context,
|
||||
builder: (_) =>
|
||||
LegacyInstalledAppsDialog(appIds: legacyInstalledApps),
|
||||
);
|
||||
}
|
||||
}
|
||||
await settings.saveTemp(temp.copyWith(restoreOnboardingComplete: true));
|
||||
}
|
||||
await service.backupDeviceKey(ref: ref, amberSigner: amberSigner);
|
||||
|
||||
@@ -12,20 +12,16 @@ const _storage = FlutterSecureStorage(
|
||||
|
||||
/// Portable preferences, mirrored by DeviceStateService.
|
||||
class PortableSettings {
|
||||
final bool installedAppsBackupEnabled;
|
||||
final bool backgroundAutoUpdatesEnabled;
|
||||
final Set<String> trustedSigners;
|
||||
|
||||
const PortableSettings({
|
||||
this.installedAppsBackupEnabled = false,
|
||||
this.backgroundAutoUpdatesEnabled = false,
|
||||
this.trustedSigners = const {},
|
||||
});
|
||||
|
||||
factory PortableSettings.fromJson(Map<String, dynamic> json) {
|
||||
return PortableSettings(
|
||||
installedAppsBackupEnabled:
|
||||
json['installedAppsBackupEnabled'] as bool? ?? false,
|
||||
backgroundAutoUpdatesEnabled:
|
||||
json['backgroundAutoUpdatesEnabled'] as bool? ?? false,
|
||||
trustedSigners:
|
||||
@@ -35,18 +31,14 @@ class PortableSettings {
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'installedAppsBackupEnabled': installedAppsBackupEnabled,
|
||||
'backgroundAutoUpdatesEnabled': backgroundAutoUpdatesEnabled,
|
||||
'trustedSigners': trustedSigners.toList()..sort(),
|
||||
};
|
||||
|
||||
PortableSettings copyWith({
|
||||
bool? installedAppsBackupEnabled,
|
||||
bool? backgroundAutoUpdatesEnabled,
|
||||
Set<String>? 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<String> get trustedSigners => portable.trustedSigners;
|
||||
@@ -131,7 +122,6 @@ class LocalSettings {
|
||||
DateTime? lastAppOpened,
|
||||
DateTime? seenUntil,
|
||||
DateTime? deletionSyncedUntil,
|
||||
bool? installedAppsBackupEnabled,
|
||||
bool? backgroundAutoUpdatesEnabled,
|
||||
Set<String>? trustedSigners,
|
||||
LogLevel? logLevel,
|
||||
@@ -142,7 +132,6 @@ class LocalSettings {
|
||||
? null
|
||||
: (nwcConnectionString ?? this.nwcConnectionString),
|
||||
portable: portable.copyWith(
|
||||
installedAppsBackupEnabled: installedAppsBackupEnabled,
|
||||
backgroundAutoUpdatesEnabled: backgroundAutoUpdatesEnabled,
|
||||
trustedSigners: trustedSigners,
|
||||
),
|
||||
|
||||
@@ -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<UpdatePollerState> {
|
||||
|
||||
final Ref ref;
|
||||
Timer? _pollTimer;
|
||||
List<String> _lastBackedUpIds = [];
|
||||
|
||||
void _init() {
|
||||
// Hydrate from local storage as soon as SQLite is ready — no network.
|
||||
@@ -173,7 +170,6 @@ class UpdatePollerNotifier extends StateNotifier<UpdatePollerState> {
|
||||
clearError: true,
|
||||
hasHydrated: true,
|
||||
);
|
||||
unawaited(_backupInstalledApps());
|
||||
} catch (e, st) {
|
||||
LogService.I.warn(
|
||||
'update check failed',
|
||||
@@ -280,70 +276,6 @@ class UpdatePollerNotifier extends StateNotifier<UpdatePollerState> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<void> _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<App>(
|
||||
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<String> a, List<String> 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();
|
||||
|
||||
@@ -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<String> appIds;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final (:authors, :identifiers) = decomposeAddressableIds(appIds);
|
||||
final appsState = ref.watch(
|
||||
query<App>(
|
||||
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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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'],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user