mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 12:38:24 +00:00
Fix unmanaged apps feature finally, closes #315
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user