From 4d1b6b265625ffdf1938c16500c9d47ed3223770 Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Wed, 1 Apr 2026 20:09:43 -0300 Subject: [PATCH] Fix system dialog dismissed UX --- .gitignore | 4 +++- .../plugins/AndroidPackageManagerPlugin.kt | 23 ++++++++++++++++--- .../android_package_manager.dart | 22 ++---------------- lib/widgets/install_button.dart | 2 +- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 637d64a..1146f8f 100644 --- a/.gitignore +++ b/.gitignore @@ -55,4 +55,6 @@ app.*.map.json reference/* # FVM Version Cache -.fvm/ \ No newline at end of file +.fvm/ + +bugs/ \ No newline at end of file diff --git a/android/app/src/main/kotlin/dev/zapstore/app/plugins/AndroidPackageManagerPlugin.kt b/android/app/src/main/kotlin/dev/zapstore/app/plugins/AndroidPackageManagerPlugin.kt index dadb4b1..31d8150 100644 --- a/android/app/src/main/kotlin/dev/zapstore/app/plugins/AndroidPackageManagerPlugin.kt +++ b/android/app/src/main/kotlin/dev/zapstore/app/plugins/AndroidPackageManagerPlugin.kt @@ -353,10 +353,27 @@ class AndroidPackageManagerPlugin : Log.d(TAG, "App foregrounded") if (!wasForeground) { - // Re-launch any pending install dialogs for ((packageName, intent) in pendingUserActionIntents.toMap()) { - Log.d(TAG, "Re-launching pending dialog for $packageName") - launchConfirmDialog(packageName, intent) + val sessionActive = findExistingSession(packageName) != null + if (sessionActive) { + // Session still alive — user was in another app, dialog is still pending. + // Re-show it so the user can act on it without hunting for it. + Log.d(TAG, "Re-launching pending dialog for $packageName (session active)") + launchConfirmDialog(packageName, intent) + } else { + // Session is gone — dialog was dismissed without confirming. + // Emit CANCELLED so Dart transitions to InstallCancelled immediately. + Log.d(TAG, "Session gone for $packageName on foreground — emitting cancelled") + pendingUserActionIntents.remove(packageName) + val sessionId = sessionToPackage.entries.find { it.value == packageName }?.key ?: -1 + clearWatchdog(packageName) + onInstallResult( + sessionId = sessionId, + status = InstallStatus.CANCELLED, + packageName = packageName, + message = "Install dialog dismissed" + ) + } } } } diff --git a/lib/services/package_manager/android_package_manager.dart b/lib/services/package_manager/android_package_manager.dart index 0d8cc85..b6f08bc 100644 --- a/lib/services/package_manager/android_package_manager.dart +++ b/lib/services/package_manager/android_package_manager.dart @@ -71,8 +71,6 @@ final class AndroidPackageManager extends PackageManager { static const _methodChannel = MethodChannel('android_package_manager'); static const _eventChannel = EventChannel('android_package_manager/events'); - static const _pendingUserActionTimeout = Duration(seconds: 10); - bool _supportsSilentInstall = false; int _syncGeneration = 0; StreamSubscription? _eventSubscription; @@ -230,29 +228,13 @@ final class AndroidPackageManager extends PackageManager { break; case InstallStatus.pendingUserAction: - // If pending user action lingers too long, treat it like dismiss/cancel - // and surface "Install (retry)" instead of staying stuck requesting. - if (existingOp is Installing && - !existingOp.isSilent && - DateTime.now().difference(existingOp.startedAt) >= - _pendingUserActionTimeout) { - if (filePath != null) { - setOperation( - appId, - InstallCancelled(target: target, filePath: filePath), - ); - } else { - clearOperation(appId); - } - clearInstallSlot(appId); - break; - } - // User action required. Ensure we don't get stuck in Verifying if the // STARTED event was missed; show Installing state. // Skip if already in Installing or SystemProcessing to preserve the // original startedAt timestamp — otherwise the watchdog timeout resets // on every pendingUserAction/systemProcessing bounce and never fires. + // Dialog dismiss is detected on the Kotlin side via onStart + + // findExistingSession, which emits CANCELLED when the session is gone. if (filePath != null && existingOp is! Installing && existingOp is! SystemProcessing) { diff --git a/lib/widgets/install_button.dart b/lib/widgets/install_button.dart index eb74c42..7c6af5b 100644 --- a/lib/widgets/install_button.dart +++ b/lib/widgets/install_button.dart @@ -175,7 +175,7 @@ class InstallButton extends ConsumerWidget { isSilent ? (isInstalled ? 'Updating...' : 'Installing...') : (isInstalled ? 'Requesting update' : 'Requesting installation'), - null, // Not tappable - auto-transitions to retry after 10s if no response + null, // Not tappable - transitions to retry when Kotlin detects dialog dismissed fontSize: fontSize, showSpinner: true, ),