From ae149e278cd0e9a1501444fff92b4c361253f00e Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Tue, 20 Jan 2026 20:03:30 -0300 Subject: [PATCH] Sync installed packages on navigation to updates --- lib/router.dart | 28 ++++- lib/screens/updates_screen.dart | 2 +- spec/features/FEAT-001-package-manager.md | 122 ---------------------- 3 files changed, 28 insertions(+), 124 deletions(-) delete mode 100644 spec/features/FEAT-001-package-manager.md diff --git a/lib/router.dart b/lib/router.dart index 27169c9..53e5e2f 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:models/models.dart'; @@ -8,6 +10,7 @@ import 'package:zapstore/screens/user_screen.dart'; import 'package:zapstore/screens/search_screen.dart'; import 'package:zapstore/screens/updates_screen.dart'; import 'package:zapstore/screens/profile_screen.dart'; +import 'package:zapstore/services/package_manager/package_manager.dart'; /// Root paths for each navigation branch (used for back navigation handling) const kBranchRoots = ['/search', '/updates', '/profile']; @@ -70,7 +73,9 @@ GoRoute _userRoute() { } final routerProvider = Provider((ref) { - return GoRouter( + String? previousPath; + + final router = GoRouter( initialLocation: '/search', routes: [ // Top-level route for market:// intents @@ -124,4 +129,25 @@ final routerProvider = Provider((ref) { ), ], ); + + // Listen for route changes to trigger actions + void onRouteChange() { + final currentPath = router.routerDelegate.currentConfiguration.uri.path; + final isUpdatesRoute = currentPath.startsWith('/updates'); + final wasUpdatesRoute = previousPath?.startsWith('/updates') ?? false; + + // Sync installed packages when navigating TO the updates branch + if (isUpdatesRoute && !wasUpdatesRoute) { + unawaited( + ref.read(packageManagerProvider.notifier).syncInstalledPackages(), + ); + } + + previousPath = currentPath; + } + + router.routerDelegate.addListener(onRouteChange); + ref.onDispose(() => router.routerDelegate.removeListener(onRouteChange)); + + return router; }); diff --git a/lib/screens/updates_screen.dart b/lib/screens/updates_screen.dart index 5eea8e7..1c8e3f6 100644 --- a/lib/screens/updates_screen.dart +++ b/lib/screens/updates_screen.dart @@ -61,7 +61,7 @@ class _ConnectionStatusIndicator extends ConsumerWidget { final statusColor = hasActiveConnection ? Colors.green : Colors.grey; final statusText = hasActiveConnection - ? 'Connected · Checking for updates' + ? 'Connected · Streaming updates' : 'Offline'; return Container( diff --git a/spec/features/FEAT-001-package-manager.md b/spec/features/FEAT-001-package-manager.md deleted file mode 100644 index 83b8b81..0000000 --- a/spec/features/FEAT-001-package-manager.md +++ /dev/null @@ -1,122 +0,0 @@ -# FEAT-001 — Package Manager - -## Goal - -Single source of truth for installed packages and active install operations. -Manages the complete lifecycle: download → verify → install, with pause/resume/cancel support. - -## Non-Goals - -- Managing non-APK file types -- Auto-updating without user awareness -- Installing from sources other than Nostr-published releases - -## User-Visible Behavior - -### Download Phase - -- User taps "Install" → download begins, progress shown -- User can pause/resume/cancel active downloads -- Multiple downloads queue automatically (max 3 concurrent) -- "Update All" queues all updates immediately with visual feedback - -### Verification Phase - -- After download completes, hash verification runs -- Verification state is visible (not hidden) -- Hash mismatch blocks install with clear error - -### Permission Phase - -- If "Install unknown apps" permission not granted, user is prompted -- Permission state is explicit in UI -- Once granted, all waiting installs advance automatically - -### Install Phase - -- Native Android install dialog shown -- One install dialog at a time (serialized) -- If user dismisses dialog, install shows "Tap to retry" state -- Success updates installed list immediately (no stale UI) - -### Failure States - -- Download failed → clear error, can retry -- Hash mismatch → error, cannot proceed -- Certificate mismatch → offer "Uninstall and reinstall" option -- Permission denied → guidance to enable in Settings - -## State Machine - -Operations follow this sealed class hierarchy (`install_operation.dart`): - -``` -DownloadQueued → Downloading ↔ DownloadPaused - ↓ - Verifying - ↓ - AwaitingPermission (if needed) - ↓ - ReadyToInstall - ↓ - Installing → AwaitingUserAction (if dismissed) - ↓ - [cleared] or OperationFailed -``` - -State transitions are unidirectional except Downloading ↔ DownloadPaused. - -## Edge Cases - -- Network drops mid-download → download pauses or fails gracefully, can retry -- App backgrounded during install → install completes, UI updates on return -- 404 from origin server → automatic CDN fallback before failing -- Stale operations (>7 days) → garbage collected on app restart -- Android package DB race condition → state updated from target metadata, not sync - -## Invariants - -These are non-negotiable. Violations mean the implementation is broken. - -1. **UI never blocks** — `install()` returns immediately; events drive state via EventChannel -2. **One install dialog at a time** — Android PackageInstaller limitation, enforced by serialization -3. **Hash verification before install** — Native side verifies before install session opens -4. **Permission flow is explicit** — `AwaitingPermission` state exists for UI feedback -5. **Downloaded files are cleaned up** — Deleted after success or dismissal -6. **No polling** — All state changes via callbacks/events, never periodic checks - -## Integration Boundaries - -``` -┌─────────────────────────────────────────────────────────────┐ -│ PackageManager (Dart) │ -│ - State machine owner │ -│ - Download management (background_downloader) │ -│ - Orchestrates flow │ -└─────────────────────────┬───────────────────────────────────┘ - │ MethodChannel / EventChannel -┌─────────────────────────▼───────────────────────────────────┐ -│ AndroidPackageManagerPlugin (Kotlin) │ -│ - Hash verification │ -│ - PackageInstaller session │ -│ - Permission checks │ -│ - Emits: verifying/started/success/failed/cancelled │ -└─────────────────────────────────────────────────────────────┘ -``` - -## Acceptance Criteria - -- [ ] User can download, pause, resume, cancel downloads -- [ ] User can install apps with proper verification -- [ ] Multiple downloads queue correctly (max 3 concurrent) -- [ ] Install failures show actionable error messages -- [ ] Certificate mismatch offers force-update option -- [ ] UI remains responsive throughout all operations -- [ ] No operations block the UI thread - -## Files - -- `lib/services/package_manager/package_manager.dart` — Base class, state machine -- `lib/services/package_manager/install_operation.dart` — State definitions -- `lib/services/package_manager/android_package_manager.dart` — Android implementation -- `android/.../AndroidPackageManagerPlugin.kt` — Native side