mirror of
https://github.com/zapstore/zapstore.git
synced 2026-10-05 20:48: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: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,
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
- 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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user