Send errors with description from native side, only show app zaps

This commit is contained in:
franzap
2025-12-25 20:35:00 -03:00
parent fc566a4345
commit e4a2634a44
6 changed files with 43 additions and 54 deletions
@@ -287,7 +287,8 @@ class AndroidPackageManagerPlugin : FlutterPlugin, MethodCallHandler,
appId: String, appId: String,
status: String, status: String,
message: String? = null, message: String? = null,
errorCode: String? = null errorCode: String? = null,
description: String? = null
) { ) {
val emitNow = emit@{ val emitNow = emit@{
// Update watchdog regardless of whether Dart is listening. // Update watchdog regardless of whether Dart is listening.
@@ -310,6 +311,9 @@ class AndroidPackageManagerPlugin : FlutterPlugin, MethodCallHandler,
if (errorCode != null) { if (errorCode != null) {
event["errorCode"] = errorCode event["errorCode"] = errorCode
} }
if (description != null) {
event["description"] = description
}
Log.d(TAG, "Emitting to Dart: $event") Log.d(TAG, "Emitting to Dart: $event")
sink.success(event) sink.success(event)
} }
@@ -496,8 +500,9 @@ class AndroidPackageManagerPlugin : FlutterPlugin, MethodCallHandler,
emitInstallStatus( emitInstallStatus(
packageName, packageName,
InstallStatus.FAILED, InstallStatus.FAILED,
verification.errorMessage, verification.errorTitle,
verification.errorCode verification.errorCode,
verification.errorDescription
) )
} }
verificationThreads.remove(packageName) verificationThreads.remove(packageName)
@@ -648,14 +653,20 @@ class AndroidPackageManagerPlugin : FlutterPlugin, MethodCallHandler,
private data class VerificationResult( private data class VerificationResult(
val isSuccess: Boolean, val isSuccess: Boolean,
val errorMessage: String = "", val errorTitle: String = "",
val errorDescription: String? = null,
val errorCode: String? = null val errorCode: String? = null
) )
private fun verifyApk(file: File, expectedHash: String, expectedSize: Long): VerificationResult { private fun verifyApk(file: File, expectedHash: String, expectedSize: Long): VerificationResult {
if (!isValidApkFormat(file)) { if (!isValidApkFormat(file)) {
file.delete() 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") val digest = MessageDigest.getInstance("SHA-256")
@@ -671,7 +682,8 @@ class AndroidPackageManagerPlugin : FlutterPlugin, MethodCallHandler,
if (actualHash.lowercase() != expectedHash.lowercase()) { if (actualHash.lowercase() != expectedHash.lowercase()) {
return VerificationResult( return VerificationResult(
false, 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 ErrorCode.HASH_MISMATCH
) )
} }
+9 -21
View File
@@ -205,24 +205,22 @@ class _UserZapsList extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
// Collect addressable tags for apps and metadata IDs separately // Don't query zaps if user has no apps
final allAppTags = <String, Set<String>>{}; if (apps.isEmpty) {
final metadataIds = <String>{}; return const SizedBox.shrink();
}
// Collect addressable tags for apps
final allAppTags = <String, Set<String>>{};
for (final app in apps) { for (final app in apps) {
final appTags = app.event.addressableIdTagMap; final appTags = app.event.addressableIdTagMap;
for (final entry in appTags.entries) { for (final entry in appTags.entries) {
allAppTags[entry.key] = {...?allAppTags[entry.key], ...entry.value}; 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 // Query zaps on apps (via #a tag)
final appZapsState = ref.watch( final zapsState = ref.watch(
query<Zap>( query<Zap>(
tags: allAppTags, tags: allAppTags,
source: const LocalAndRemoteSource(relays: 'social'), source: const LocalAndRemoteSource(relays: 'social'),
@@ -230,17 +228,7 @@ class _UserZapsList extends HookConsumerWidget {
), ),
); );
// Query zaps on metadata (via #e tag) - separate query final allZaps = zapsState.models;
final metadataZapsState = ref.watch(
query<Zap>(
tags: {'#e': metadataIds},
source: const LocalAndRemoteSource(relays: 'social'),
subscriptionPrefix: 'user-metadata-zaps',
),
);
// Combine zaps from both queries
final allZaps = {...appZapsState.models, ...metadataZapsState.models};
if (allZaps.isEmpty) { if (allZaps.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
@@ -99,10 +99,11 @@ final class AndroidPackageManager extends PackageManager {
final statusRaw = event['status'] as String?; final statusRaw = event['status'] as String?;
final message = event['message'] as String?; final message = event['message'] as String?;
final errorCode = event['errorCode'] as String?; final errorCode = event['errorCode'] as String?;
final description = event['description'] as String?;
final status = InstallStatusX.tryParse(statusRaw); final status = InstallStatusX.tryParse(statusRaw);
debugPrint( 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) { if (appId == null || statusRaw == null) {
@@ -196,6 +197,7 @@ final class AndroidPackageManager extends PackageManager {
target: target, target: target,
type: _errorCodeToFailureType(errorCode, message), type: _errorCodeToFailureType(errorCode, message),
message: message ?? 'Installation failed', message: message ?? 'Installation failed',
description: description,
filePath: filePath, filePath: filePath,
), ),
); );
@@ -132,12 +132,14 @@ class Uninstalling extends InstallOperation {
class OperationFailed extends InstallOperation { class OperationFailed extends InstallOperation {
final FailureType type; final FailureType type;
final String message; final String message;
final String? description;
final String? filePath; final String? filePath;
const OperationFailed({ const OperationFailed({
required super.target, required super.target,
required this.type, required this.type,
required this.message, required this.message,
this.description,
this.filePath, this.filePath,
}); });
+3 -25
View File
@@ -275,24 +275,14 @@ class _ZappersListSection extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final latestMetadata = app.latestFileMetadata; return _ZappersListSectionWithMetadata(app: app);
if (latestMetadata == null) return const SizedBox.shrink();
return _ZappersListSectionWithMetadata(
app: app,
metadataId: latestMetadata.id,
);
} }
} }
class _ZappersListSectionWithMetadata extends ConsumerWidget { class _ZappersListSectionWithMetadata extends ConsumerWidget {
const _ZappersListSectionWithMetadata({ const _ZappersListSectionWithMetadata({required this.app});
required this.app,
required this.metadataId,
});
final App app; final App app;
final String metadataId;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -305,19 +295,7 @@ class _ZappersListSectionWithMetadata extends ConsumerWidget {
), ),
); );
// Query zaps on metadata (via #e tag) final allZaps = zapsState.models;
final zapsOnMetadataState = ref.watch(
query<Zap>(
tags: {
'#e': {metadataId},
},
source: const LocalAndRemoteSource(relays: 'social'),
subscriptionPrefix: 'metadata-zaps',
),
);
// Combine zaps from both queries
final allZaps = {...zapsState.models, ...zapsOnMetadataState.models};
if (allZaps.isEmpty) return const SizedBox.shrink(); if (allZaps.isEmpty) return const SizedBox.shrink();
+8 -1
View File
@@ -604,7 +604,11 @@ class InstallButton extends ConsumerWidget {
final operation = ref.read(installOperationProvider(app.identifier)); final operation = ref.read(installOperationProvider(app.identifier));
if (operation is! OperationFailed) return; 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). // Always clear error after showing it (reckless mode removed).
final pm = ref.read(packageManagerProvider.notifier); final pm = ref.read(packageManagerProvider.notifier);
@@ -617,6 +621,9 @@ class InstallButton extends ConsumerWidget {
'Certificate mismatch', 'Certificate mismatch',
description: 'The app signature does not match. Force update required.', 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 // Other errors are shown when user taps the error button
} }