From e4a2634a44711724f63f95804592d0dd11be0e17 Mon Sep 17 00:00:00 2001 From: franzap <_@franzap.com> Date: Thu, 25 Dec 2025 20:35:00 -0300 Subject: [PATCH] Send errors with description from native side, only show app zaps --- .../plugins/AndroidPackageManagerPlugin.kt | 24 +++++++++++---- lib/screens/user_screen.dart | 30 ++++++------------- .../android_package_manager.dart | 4 ++- .../package_manager/install_operation.dart | 2 ++ lib/widgets/app_detail_widgets.dart | 28 ++--------------- lib/widgets/install_button.dart | 9 +++++- 6 files changed, 43 insertions(+), 54 deletions(-) diff --git a/android/app/src/main/kotlin/dev/zapstore/alpha/plugins/AndroidPackageManagerPlugin.kt b/android/app/src/main/kotlin/dev/zapstore/alpha/plugins/AndroidPackageManagerPlugin.kt index f99f89c..8196927 100644 --- a/android/app/src/main/kotlin/dev/zapstore/alpha/plugins/AndroidPackageManagerPlugin.kt +++ b/android/app/src/main/kotlin/dev/zapstore/alpha/plugins/AndroidPackageManagerPlugin.kt @@ -287,7 +287,8 @@ class AndroidPackageManagerPlugin : FlutterPlugin, MethodCallHandler, appId: String, status: String, message: String? = null, - errorCode: String? = null + errorCode: String? = null, + description: String? = null ) { val emitNow = emit@{ // Update watchdog regardless of whether Dart is listening. @@ -310,6 +311,9 @@ class AndroidPackageManagerPlugin : FlutterPlugin, MethodCallHandler, if (errorCode != null) { event["errorCode"] = errorCode } + if (description != null) { + event["description"] = description + } Log.d(TAG, "Emitting to Dart: $event") sink.success(event) } @@ -496,8 +500,9 @@ class AndroidPackageManagerPlugin : FlutterPlugin, MethodCallHandler, emitInstallStatus( packageName, InstallStatus.FAILED, - verification.errorMessage, - verification.errorCode + verification.errorTitle, + verification.errorCode, + verification.errorDescription ) } verificationThreads.remove(packageName) @@ -648,14 +653,20 @@ class AndroidPackageManagerPlugin : FlutterPlugin, MethodCallHandler, private data class VerificationResult( val isSuccess: Boolean, - val errorMessage: String = "", + val errorTitle: String = "", + val errorDescription: String? = null, val errorCode: String? = null ) private fun verifyApk(file: File, expectedHash: String, expectedSize: Long): VerificationResult { if (!isValidApkFormat(file)) { file.delete() - return VerificationResult(false, "Invalid APK file format", ErrorCode.INVALID_FILE) + return VerificationResult( + false, + "Invalid APK file", + "The downloaded file is not a valid APK format.", + ErrorCode.INVALID_FILE + ) } val digest = MessageDigest.getInstance("SHA-256") @@ -671,7 +682,8 @@ class AndroidPackageManagerPlugin : FlutterPlugin, MethodCallHandler, if (actualHash.lowercase() != expectedHash.lowercase()) { return VerificationResult( false, - "Hash verification failed. Expected: $expectedHash, Actual: $actualHash", + "Hash verification failed", + "The downloaded file hash does not match.\n\nExpected: $expectedHash\nActual: $actualHash", ErrorCode.HASH_MISMATCH ) } diff --git a/lib/screens/user_screen.dart b/lib/screens/user_screen.dart index 3dc6b2e..154a035 100644 --- a/lib/screens/user_screen.dart +++ b/lib/screens/user_screen.dart @@ -205,24 +205,22 @@ class _UserZapsList extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - // Collect addressable tags for apps and metadata IDs separately - final allAppTags = >{}; - final metadataIds = {}; + // Don't query zaps if user has no apps + if (apps.isEmpty) { + return const SizedBox.shrink(); + } + // Collect addressable tags for apps + final allAppTags = >{}; for (final app in apps) { final appTags = app.event.addressableIdTagMap; for (final entry in appTags.entries) { allAppTags[entry.key] = {...?allAppTags[entry.key], ...entry.value}; } - - final metadata = app.latestFileMetadata; - if (metadata != null) { - metadataIds.add(metadata.id); - } } - // Query zaps on apps (via #a tag) - separate from metadata zaps - final appZapsState = ref.watch( + // Query zaps on apps (via #a tag) + final zapsState = ref.watch( query( tags: allAppTags, source: const LocalAndRemoteSource(relays: 'social'), @@ -230,17 +228,7 @@ class _UserZapsList extends HookConsumerWidget { ), ); - // Query zaps on metadata (via #e tag) - separate query - final metadataZapsState = ref.watch( - query( - tags: {'#e': metadataIds}, - source: const LocalAndRemoteSource(relays: 'social'), - subscriptionPrefix: 'user-metadata-zaps', - ), - ); - - // Combine zaps from both queries - final allZaps = {...appZapsState.models, ...metadataZapsState.models}; + final allZaps = zapsState.models; if (allZaps.isEmpty) { return const SizedBox.shrink(); diff --git a/lib/services/package_manager/android_package_manager.dart b/lib/services/package_manager/android_package_manager.dart index ff76376..7c60d88 100644 --- a/lib/services/package_manager/android_package_manager.dart +++ b/lib/services/package_manager/android_package_manager.dart @@ -99,10 +99,11 @@ final class AndroidPackageManager extends PackageManager { final statusRaw = event['status'] as String?; final message = event['message'] as String?; final errorCode = event['errorCode'] as String?; + final description = event['description'] as String?; final status = InstallStatusX.tryParse(statusRaw); debugPrint( - '[PackageManager] Received event: appId=$appId, status=$status, msg=$message, errorCode=$errorCode', + '[PackageManager] Received event: appId=$appId, status=$status, msg=$message, errorCode=$errorCode, desc=$description', ); if (appId == null || statusRaw == null) { @@ -196,6 +197,7 @@ final class AndroidPackageManager extends PackageManager { target: target, type: _errorCodeToFailureType(errorCode, message), message: message ?? 'Installation failed', + description: description, filePath: filePath, ), ); diff --git a/lib/services/package_manager/install_operation.dart b/lib/services/package_manager/install_operation.dart index 4b024df..817a884 100644 --- a/lib/services/package_manager/install_operation.dart +++ b/lib/services/package_manager/install_operation.dart @@ -132,12 +132,14 @@ class Uninstalling extends InstallOperation { class OperationFailed extends InstallOperation { final FailureType type; final String message; + final String? description; final String? filePath; const OperationFailed({ required super.target, required this.type, required this.message, + this.description, this.filePath, }); diff --git a/lib/widgets/app_detail_widgets.dart b/lib/widgets/app_detail_widgets.dart index 4ab9542..8ea4de1 100644 --- a/lib/widgets/app_detail_widgets.dart +++ b/lib/widgets/app_detail_widgets.dart @@ -275,24 +275,14 @@ class _ZappersListSection extends StatelessWidget { @override Widget build(BuildContext context) { - final latestMetadata = app.latestFileMetadata; - if (latestMetadata == null) return const SizedBox.shrink(); - - return _ZappersListSectionWithMetadata( - app: app, - metadataId: latestMetadata.id, - ); + return _ZappersListSectionWithMetadata(app: app); } } class _ZappersListSectionWithMetadata extends ConsumerWidget { - const _ZappersListSectionWithMetadata({ - required this.app, - required this.metadataId, - }); + const _ZappersListSectionWithMetadata({required this.app}); final App app; - final String metadataId; @override Widget build(BuildContext context, WidgetRef ref) { @@ -305,19 +295,7 @@ class _ZappersListSectionWithMetadata extends ConsumerWidget { ), ); - // Query zaps on metadata (via #e tag) - final zapsOnMetadataState = ref.watch( - query( - tags: { - '#e': {metadataId}, - }, - source: const LocalAndRemoteSource(relays: 'social'), - subscriptionPrefix: 'metadata-zaps', - ), - ); - - // Combine zaps from both queries - final allZaps = {...zapsState.models, ...zapsOnMetadataState.models}; + final allZaps = zapsState.models; if (allZaps.isEmpty) return const SizedBox.shrink(); diff --git a/lib/widgets/install_button.dart b/lib/widgets/install_button.dart index 50b4727..0f8a3c1 100644 --- a/lib/widgets/install_button.dart +++ b/lib/widgets/install_button.dart @@ -604,7 +604,11 @@ class InstallButton extends ConsumerWidget { final operation = ref.read(installOperationProvider(app.identifier)); if (operation is! OperationFailed) return; - context.showError(operation.message, actions: const []); + context.showError( + operation.message, + description: operation.description, + actions: const [], + ); // Always clear error after showing it (reckless mode removed). final pm = ref.read(packageManagerProvider.notifier); @@ -617,6 +621,9 @@ class InstallButton extends ConsumerWidget { 'Certificate mismatch', description: 'The app signature does not match. Force update required.', ); + } else if (operation.description != null) { + // Errors with descriptions (from Kotlin) are shown as toasts + context.showError(operation.message, description: operation.description); } // Other errors are shown when user taps the error button }