Fix unmanaged apps feature finally, closes #315

This commit is contained in:
franzap
2026-06-16 12:48:00 -03:00
parent c91934b551
commit 3c38f3f63d
9 changed files with 73 additions and 46 deletions
+2 -2
View File
@@ -6,7 +6,7 @@ import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:models/models.dart';
import 'package:zapstore/services/device_key_service.dart';
import 'package:zapstore/services/ignored_apps_service.dart';
import 'package:zapstore/services/unmanaged_apps_service.dart';
import 'package:zapstore/services/package_manager/package_manager.dart';
import 'package:zapstore/services/updates_service.dart';
import 'package:zapstore/theme.dart';
@@ -367,7 +367,7 @@ class _UpdatesList extends ConsumerWidget {
}
}
/// A section: header + list of AppCards with swipe-to-ignore, rendered as a single sliver.
/// A section: header + list of AppCards with swipe-to-unmanage, rendered as a single sliver.
class _AppSection extends ConsumerWidget {
const _AppSection({
required this.icon,
+2 -2
View File
@@ -15,7 +15,7 @@ import 'package:zapstore/widgets/device_backup_dialog.dart';
const kSettingsIdentifier = 'zapstore-settings';
const _kDeviceBackupsKey = 'deviceBackups';
const _kLegacyInstalledAppsBackupIdentifier = 'zapstore-installed-backup';
const _kLegacyIgnoredAppsIdentifier = 'zapstore-ignored-apps';
const _kLegacyUnmanagedAppsIdentifier = 'zapstore-ignored-apps';
/// Manages device key backup/restore via the Amber-signed settings event.
///
@@ -236,7 +236,7 @@ final deviceBackupServiceProvider = Provider<DeviceBackupService>(
String _deviceIdentifierFor(String identifier) {
return switch (identifier) {
_kLegacyInstalledAppsBackupIdentifier => kInstalledAppsIdentifier,
_kLegacyIgnoredAppsIdentifier => kUnmanagedAppsIdentifier,
_kLegacyUnmanagedAppsIdentifier => kUnmanagedAppsIdentifier,
_ => identifier,
};
}
+1 -1
View File
@@ -9,7 +9,7 @@ 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/package_manager/package_manager.dart';
import 'package:zapstore/services/ignored_apps_service.dart';
import 'package:zapstore/services/unmanaged_apps_service.dart';
import 'package:zapstore/services/settings_service.dart';
import 'package:zapstore/utils/extensions.dart';
+12 -1
View File
@@ -14,10 +14,14 @@ Show users their installed apps with available updates, allow manual refresh, an
### Updates Screen
- Shows categorized lists: Installing, Updates (automatic), Manual Updates, Up to Date, Other Installed
- Shows categorized lists: Installing, Updates (automatic), Manual Updates, Up to Date, Other Installed, Unmanaged Apps
- "Update All" button at top updates all apps with automatic update capability
- "Last checked" timestamp shows when updates were last fetched
- Pull-to-refresh triggers immediate update check (with throttling)
- Apps marked unmanaged are excluded from update counts and "Update All"
- Cataloged unmanaged apps keep Zapstore metadata such as icon, publisher, description, and version data; uncataloged unmanaged apps fall back to installed package metadata
- Apps installed by another app store may default to unmanaged; apps installed manually, through browser/file-manager/package-installer flows, or by Zapstore stay managed by default
- Users can override either default with explicit Unmanage/Manage actions, and explicit choices persist across restarts
### Cold Start Behavior
@@ -50,6 +54,8 @@ Show users their installed apps with available updates, allow manual refresh, an
- Network offline during poll → fails silently, retries on next interval
- App backgrounded → polling pauses, resumes when app returns to foreground
- Rapid pull-to-refresh → throttled to prevent server spam
- Installer source unavailable or ambiguous → app remains managed by default
- Explicit Manage must not be undone by automatic installer-source detection on the next package scan
## Acceptance Criteria
@@ -60,3 +66,8 @@ Show users their installed apps with available updates, allow manual refresh, an
- [ ] "Last checked" timestamp displays relative time (e.g., "2 minutes ago")
- [ ] Skeleton shown only on cold start until first installed app matches
- [ ] Subsequent refreshes show spinner, not skeleton
- [ ] Unmanaged apps excluded from update count and "Update All"
- [ ] Cataloged unmanaged apps retain Zapstore metadata in the Unmanaged Apps section
- [ ] Other-store installs default unmanaged when installer source is known
- [ ] Browser/manual/package-installer installs default managed
- [ ] Explicit Manage/Unmanage choices override installer-source defaults and persist
+5 -5
View File
@@ -2,7 +2,7 @@
## Goal
Decouple private data (bookmarks, ignored apps, installed backup, settings) from
Decouple private data (bookmarks, unmanaged apps, installed backup, settings) from
Amber sign-in by generating a local device key (nsec) that owns all private
encrypted events. Amber becomes purely the identity layer for public actions
(sharing stacks, zaps, web of trust).
@@ -17,7 +17,7 @@ encrypted events. Amber becomes purely the identity layer for public actions
## User-Visible Behavior
- On first launch, a device key is silently generated and stored in secure storage
- Bookmarks, ignored apps, and settings work immediately without sign-in
- Bookmarks, unmanaged apps, and settings work immediately without sign-in
- Profile screen shows device key section with ability to copy nsec
- On first Amber sign-in, a dialog offers to:
- Back up this device (stores device nsec inside encrypted `zapstore-settings`)
@@ -29,7 +29,7 @@ encrypted events. Amber becomes purely the identity layer for public actions
- Device nsec: secure storage only (key: device_nsec)
- NWC string: secure storage only (existing key)
- Bookmarks: encrypted AppStack (30267), d=zapstore-bookmarks, signed by device key
- Ignored apps: encrypted AppStack (30267), d=zapstore-ignored-apps, signed by device key
- Unmanaged apps: encrypted AppStack (30267), d=zapstore-unmanaged-apps, signed by device key
- Installed backup: encrypted AppStack (30267), d=zapstore-installed-backup, signed by device key
- App settings: encrypted CustomData (30078), d=zapstore-settings
- Device backup: entries inside encrypted `zapstore-settings`, signed by Amber key
@@ -60,7 +60,7 @@ encrypted events. Amber becomes purely the identity layer for public actions
- [ ] Device key generated on first launch and persisted in secure storage
- [ ] Device key survives SQLite clear / app restart
- [ ] Bookmarks work without Amber sign-in
- [ ] Ignored apps work without Amber sign-in
- [ ] Unmanaged apps work without Amber sign-in
- [ ] User can copy device nsec from profile screen
- [ ] First Amber sign-in triggers backup/restore dialog
- [ ] Backup encrypts device nsec inside `zapstore-settings` to Amber key
@@ -71,6 +71,6 @@ encrypted events. Amber becomes purely the identity layer for public actions
## Phases
- A: Device key generation + service + registration at boot + copy nsec UI
- B: Migrate bookmarks/ignored/backup to device key (drop Amber requirement)
- B: Migrate bookmarks/unmanaged/backup to device key (drop Amber requirement)
- C: Amber backup/restore dialog + CustomData events
- D: Remove appStackEventFilter, clean up sign-in gating in UI
-29
View File
@@ -1,29 +0,0 @@
# WORK-010: Ignored Apps
## Goal
Allow users to mark installed apps as "Ignored" so they are excluded from the
Updates screen and the "Update All" batch operation.
## Approach
Store the ignored set as an encrypted AppStack (kind 30267) with identifier
`zapstore-ignored-apps`, using the same pattern as the bookmarks stack. This
gives reactivity (SQLite + query<AppStack>), NIP-44 encryption, and
cross-device sync for free.
## Tasks
- [x] Add kIgnoredAppsIdentifier constant
- [x] Update appStackEventFilter to reject the ignored-apps stack by d tag
- [x] Add flutter_slidable dependency
- [x] Create ignoredAppsProvider (mirrors bookmarksProvider)
- [x] Create ignored_apps_service.dart with toggleIgnoredApp write helper
- [x] Filter ignored apps out of all lists in categorizedUpdatesProvider
- [x] Add ignoredApps: List<PackageInfo> to CategorizedUpdates
- [x] Wrap app cards in Slidable on updates screen (swipe-left -> Ignore)
- [x] Add "Ignored" section at bottom with swipe-left -> "Manage again" action
## Decisions
- Encrypted stack (not CustomData) reuses existing infrastructure and encryption
- Ignored app IDs stored as bare package IDs since uncataloged apps have no kind:pubkey prefix
- For cataloged apps the app.identifier (package ID) is used consistently
- Graceful degradation: unsigned-out users see no swipe actions
- Ignored apps excluded from all lists: automaticUpdates, manualUpdates, upToDateApps, uncatalogedApps
+45
View File
@@ -0,0 +1,45 @@
# WORK-010: Unmanaged Apps
## Goal
Allow users to mark installed apps as "Unmanaged" so they are excluded from the
Updates screen and the "Update All" batch operation.
## Approach
Store the unmanaged set as an encrypted AppStack (kind 30267) with identifier
`zapstore-unmanaged-apps`, using the same pattern as the bookmarks stack. This
gives reactivity (SQLite + query<AppStack>), NIP-44 encryption, and
cross-device sync.
## Tasks
- [x] Add kUnmanagedAppsIdentifier constant
- [x] Update appStackEventFilter to reject the unmanaged-apps stack by d tag
- [x] Add flutter_slidable dependency
- [x] Create unmanagedAppsProvider (mirrors bookmarksProvider)
- [x] Create unmanaged_apps_service.dart with toggleUnmanagedApp write helper
- [x] Filter unmanaged apps out of all lists in categorizedUpdatesProvider
- [x] Add unmanagedApps: List<PackageInfo> to CategorizedUpdates
- [x] Wrap app cards in Slidable on updates screen (swipe-left -> Unmanage)
- [x] Add "Unmanaged Apps" section at bottom with swipe-left -> "Manage" action
- [ ] Preserve catalog metadata for cataloged unmanaged apps in the Unmanaged Apps section
- [ ] Extend native package scan with Android installer-source metadata
- [ ] Default apps installed by known third-party app stores to unmanaged
- [ ] Keep browser/manual/package-installer installs managed by default
- [ ] Add explicit override state so user Manage/Unmanage choices win over installer-source defaults
- [ ] Cover defaulting, override, restart, and unavailable-installer-source cases in tests
## Decisions
- Encrypted stack (not CustomData) reuses existing infrastructure and encryption
- Unmanaged app IDs stored as bare package IDs since uncataloged apps have no kind:pubkey prefix
- For cataloged apps the app.identifier (package ID) is used consistently
- Graceful degradation: unsigned-out users see no swipe actions
- Unmanaged apps are excluded from automaticUpdates, manualUpdates, upToDateApps, and uncatalogedApps
- Installer-source detection should be conservative: unknown or ambiguous source means managed
- Third-party app stores should default unmanaged; Zapstore, package installer, file manager, browser, shell, and unknown/manual flows should default managed
- Automatic defaults need a persistent "user has overridden this package/install" signal; otherwise tapping Manage would be undone by the next package scan
- Cataloged unmanaged apps should render from App metadata when local catalog data exists, and fall back to PackageInfo only when uncataloged
## Implementation Notes
- Android can expose source through `PackageManager.getInstallSourceInfo(packageName)` on API 30+ and `getInstallerPackageName(packageName)` on older APIs.
- Add installer-source fields to `PackageInfo`, `AndroidPackageManager.syncInstalledPackages`, `BackgroundPackageManager.syncInstalledPackages`, and `InstalledPackagesSnapshot`.
- Store policy separately from the unmanaged AppStack or extend the private data model carefully: the unmanaged stack alone cannot represent both "auto-unmanaged by source" and "user explicitly managed this package".
- The updates categorizer currently removes unmanaged app IDs before querying App metadata. To preserve metadata, it needs a separate local query for unmanaged cataloged IDs with `latestAsset` / `latestRelease.latestMetadata`, then render those with `AppCard` and a Manage action.
+6 -6
View File
@@ -21,8 +21,8 @@
- Changed from FutureProvider to synchronous Provider
- No manual nip44Decrypt calls (EncryptableModel auto-decrypts)
- No sign-in gate; always available
- [x] 5. Rewrite ignored apps to use device key
- Files: `lib/services/ignored_apps_service.dart`, `lib/services/updates_service.dart`
- [x] 5. Rewrite unmanaged apps to use device key
- Files: `lib/services/unmanaged_apps_service.dart`, `lib/services/updates_service.dart`
- Changed from FutureProvider to synchronous Provider
- No sign-in gate
- [x] 6. Remove sign-in gate from SaveAppDialog
@@ -37,7 +37,7 @@
- Offers restore before migration so the final device key is chosen first
- Queries Amber-authored encrypted AppStacks after Amber connection
- Merges them into device-authored encrypted stacks using the device signer
- Normalizes legacy installed/ignored d-tags to current identifiers
- Normalizes legacy installed/unmanaged d-tags to current identifiers
- Marks migration complete per Amber pubkey + device pubkey; empty results retry
- [x] 9. Store device key backups in encrypted settings
- Files: `lib/services/device_backup_service.dart`
@@ -52,9 +52,9 @@
**Context:** Users may have bookmarks encrypted to their Amber key.
**Decision:** On Amber connection, migrate encrypted AppStacks authored by the Amber pubkey to equivalent device-key stacks.
**Rationale:** Private data should follow the new device-key ownership model without losing existing saved apps, installed-app backups, or ignored/unmanaged app state.
**Rationale:** Private data should follow the new device-key ownership model without losing existing saved apps, installed-app backups, or unmanaged app state.
### 2026-05-07 - Synchronous providers for bookmarks/ignored
### 2026-05-07 - Synchronous providers for bookmarks/unmanaged apps
**Context:** Previously FutureProvider because of manual decrypt. Now EncryptableModel auto-decrypts.
**Decision:** Changed to synchronous Provider<Set<String>>.
@@ -74,7 +74,7 @@
## Spec Issues
- `spec/features/FEAT-006-device-key.md` still lists migration as a non-goal and uses legacy d-tags for installed/ignored apps. Implementation now follows the product direction from this work session: migrate private stacks to the device pubkey on Amber connection.
- `spec/features/FEAT-006-device-key.md` still lists migration as a non-goal and uses legacy d-tags for installed/unmanaged apps. Implementation now follows the product direction from this work session: migrate private stacks to the device pubkey on Amber connection.
## Progress Notes