diff --git a/lib/screens/updates_screen.dart b/lib/screens/updates_screen.dart index cc11c25..b9118e2 100644 --- a/lib/screens/updates_screen.dart +++ b/lib/screens/updates_screen.dart @@ -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, diff --git a/lib/services/device_backup_service.dart b/lib/services/device_backup_service.dart index 8c467f3..1994307 100644 --- a/lib/services/device_backup_service.dart +++ b/lib/services/device_backup_service.dart @@ -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( String _deviceIdentifierFor(String identifier) { return switch (identifier) { _kLegacyInstalledAppsBackupIdentifier => kInstalledAppsIdentifier, - _kLegacyIgnoredAppsIdentifier => kUnmanagedAppsIdentifier, + _kLegacyUnmanagedAppsIdentifier => kUnmanagedAppsIdentifier, _ => identifier, }; } diff --git a/lib/services/ignored_apps_service.dart b/lib/services/unmanaged_apps_service.dart similarity index 100% rename from lib/services/ignored_apps_service.dart rename to lib/services/unmanaged_apps_service.dart diff --git a/lib/services/updates_service.dart b/lib/services/updates_service.dart index ca8e17f..f08e41e 100644 --- a/lib/services/updates_service.dart +++ b/lib/services/updates_service.dart @@ -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'; diff --git a/spec/features/FEAT-003-updates-screen.md b/spec/features/FEAT-003-updates-screen.md index 7db6e6e..8f59301 100644 --- a/spec/features/FEAT-003-updates-screen.md +++ b/spec/features/FEAT-003-updates-screen.md @@ -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 diff --git a/spec/features/FEAT-006-device-key.md b/spec/features/FEAT-006-device-key.md index 1c611a8..f5e4cac 100644 --- a/spec/features/FEAT-006-device-key.md +++ b/spec/features/FEAT-006-device-key.md @@ -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 diff --git a/spec/work/WORK-010-ignored-apps.md b/spec/work/WORK-010-ignored-apps.md deleted file mode 100644 index e17fd2d..0000000 --- a/spec/work/WORK-010-ignored-apps.md +++ /dev/null @@ -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), 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 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 diff --git a/spec/work/WORK-010-unmanaged-apps.md b/spec/work/WORK-010-unmanaged-apps.md new file mode 100644 index 0000000..bb9eba1 --- /dev/null +++ b/spec/work/WORK-010-unmanaged-apps.md @@ -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), 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 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. diff --git a/spec/work/WORK-011-device-key.md b/spec/work/WORK-011-device-key.md index 82dad0a..de97a01 100644 --- a/spec/work/WORK-011-device-key.md +++ b/spec/work/WORK-011-device-key.md @@ -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>. @@ -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