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:hooks_riverpod/hooks_riverpod.dart';
import 'package:models/models.dart'; import 'package:models/models.dart';
import 'package:zapstore/services/device_key_service.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/package_manager/package_manager.dart';
import 'package:zapstore/services/updates_service.dart'; import 'package:zapstore/services/updates_service.dart';
import 'package:zapstore/theme.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 { class _AppSection extends ConsumerWidget {
const _AppSection({ const _AppSection({
required this.icon, required this.icon,
+2 -2
View File
@@ -15,7 +15,7 @@ import 'package:zapstore/widgets/device_backup_dialog.dart';
const kSettingsIdentifier = 'zapstore-settings'; const kSettingsIdentifier = 'zapstore-settings';
const _kDeviceBackupsKey = 'deviceBackups'; const _kDeviceBackupsKey = 'deviceBackups';
const _kLegacyInstalledAppsBackupIdentifier = 'zapstore-installed-backup'; 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. /// Manages device key backup/restore via the Amber-signed settings event.
/// ///
@@ -236,7 +236,7 @@ final deviceBackupServiceProvider = Provider<DeviceBackupService>(
String _deviceIdentifierFor(String identifier) { String _deviceIdentifierFor(String identifier) {
return switch (identifier) { return switch (identifier) {
_kLegacyInstalledAppsBackupIdentifier => kInstalledAppsIdentifier, _kLegacyInstalledAppsBackupIdentifier => kInstalledAppsIdentifier,
_kLegacyIgnoredAppsIdentifier => kUnmanagedAppsIdentifier, _kLegacyUnmanagedAppsIdentifier => kUnmanagedAppsIdentifier,
_ => identifier, _ => 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/deletion_processor.dart';
import 'package:zapstore/services/device_key_service.dart'; import 'package:zapstore/services/device_key_service.dart';
import 'package:zapstore/services/package_manager/package_manager.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/services/settings_service.dart';
import 'package:zapstore/utils/extensions.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 ### 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 - "Update All" button at top updates all apps with automatic update capability
- "Last checked" timestamp shows when updates were last fetched - "Last checked" timestamp shows when updates were last fetched
- Pull-to-refresh triggers immediate update check (with throttling) - 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 ### 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 - Network offline during poll → fails silently, retries on next interval
- App backgrounded → polling pauses, resumes when app returns to foreground - App backgrounded → polling pauses, resumes when app returns to foreground
- Rapid pull-to-refresh → throttled to prevent server spam - 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 ## 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") - [ ] "Last checked" timestamp displays relative time (e.g., "2 minutes ago")
- [ ] Skeleton shown only on cold start until first installed app matches - [ ] Skeleton shown only on cold start until first installed app matches
- [ ] Subsequent refreshes show spinner, not skeleton - [ ] 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 ## 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 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 encrypted events. Amber becomes purely the identity layer for public actions
(sharing stacks, zaps, web of trust). (sharing stacks, zaps, web of trust).
@@ -17,7 +17,7 @@ encrypted events. Amber becomes purely the identity layer for public actions
## User-Visible Behavior ## User-Visible Behavior
- On first launch, a device key is silently generated and stored in secure storage - 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 - Profile screen shows device key section with ability to copy nsec
- On first Amber sign-in, a dialog offers to: - On first Amber sign-in, a dialog offers to:
- Back up this device (stores device nsec inside encrypted `zapstore-settings`) - 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) - Device nsec: secure storage only (key: device_nsec)
- NWC string: secure storage only (existing key) - NWC string: secure storage only (existing key)
- Bookmarks: encrypted AppStack (30267), d=zapstore-bookmarks, signed by device 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 - Installed backup: encrypted AppStack (30267), d=zapstore-installed-backup, signed by device key
- App settings: encrypted CustomData (30078), d=zapstore-settings - App settings: encrypted CustomData (30078), d=zapstore-settings
- Device backup: entries inside encrypted `zapstore-settings`, signed by Amber key - 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 generated on first launch and persisted in secure storage
- [ ] Device key survives SQLite clear / app restart - [ ] Device key survives SQLite clear / app restart
- [ ] Bookmarks work without Amber sign-in - [ ] 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 - [ ] User can copy device nsec from profile screen
- [ ] First Amber sign-in triggers backup/restore dialog - [ ] First Amber sign-in triggers backup/restore dialog
- [ ] Backup encrypts device nsec inside `zapstore-settings` to Amber key - [ ] 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 ## Phases
- A: Device key generation + service + registration at boot + copy nsec UI - 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 - C: Amber backup/restore dialog + CustomData events
- D: Remove appStackEventFilter, clean up sign-in gating in UI - 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 - Changed from FutureProvider to synchronous Provider
- No manual nip44Decrypt calls (EncryptableModel auto-decrypts) - No manual nip44Decrypt calls (EncryptableModel auto-decrypts)
- No sign-in gate; always available - No sign-in gate; always available
- [x] 5. Rewrite ignored apps to use device key - [x] 5. Rewrite unmanaged apps to use device key
- Files: `lib/services/ignored_apps_service.dart`, `lib/services/updates_service.dart` - Files: `lib/services/unmanaged_apps_service.dart`, `lib/services/updates_service.dart`
- Changed from FutureProvider to synchronous Provider - Changed from FutureProvider to synchronous Provider
- No sign-in gate - No sign-in gate
- [x] 6. Remove sign-in gate from SaveAppDialog - [x] 6. Remove sign-in gate from SaveAppDialog
@@ -37,7 +37,7 @@
- Offers restore before migration so the final device key is chosen first - Offers restore before migration so the final device key is chosen first
- Queries Amber-authored encrypted AppStacks after Amber connection - Queries Amber-authored encrypted AppStacks after Amber connection
- Merges them into device-authored encrypted stacks using the device signer - 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 - Marks migration complete per Amber pubkey + device pubkey; empty results retry
- [x] 9. Store device key backups in encrypted settings - [x] 9. Store device key backups in encrypted settings
- Files: `lib/services/device_backup_service.dart` - Files: `lib/services/device_backup_service.dart`
@@ -52,9 +52,9 @@
**Context:** Users may have bookmarks encrypted to their Amber key. **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. **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. **Context:** Previously FutureProvider because of manual decrypt. Now EncryptableModel auto-decrypts.
**Decision:** Changed to synchronous Provider<Set<String>>. **Decision:** Changed to synchronous Provider<Set<String>>.
@@ -74,7 +74,7 @@
## Spec Issues ## 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 ## Progress Notes