From f7d021c62a26db1575ea0035369459514a4ffd78 Mon Sep 17 00:00:00 2001 From: Thomas SharedInbox Date: Fri, 22 May 2026 13:01:34 +0200 Subject: [PATCH] fix: survive MissingPluginException on startup, fix crash report URL (#146) Two fixes: 1. notification_service.dart: initNotifications() now catches MissingPluginException (and any other init failure) so the app no longer crashes when flutter_local_notifications is unavailable on some Android devices. _initialized tracks success; showNewMailNotification skips the plugin call when it never initialised. 2. crash_screen.dart: "Report Issue on Codeberg" no longer puts the full report in the URL query string. Long stack traces exceeded browser URL-length limits and caused "create issue failed". The URL now carries only the pre-filled title; the user copies the full report via "Copy to Clipboard" and pastes it in the issue body. Tests added: - test/unit/notification_service_test.dart: verifies initNotifications() completes without throwing when the plugin channel is unavailable. - test/widget/crash_screen_test.dart: verifies the Codeberg URL contains the title but no &body= parameter. Co-Authored-By: Claude Sonnet 4.6 --- lib/core/services/notification_service.dart | 29 ++++++++++++++------- lib/ui/screens/crash_screen.dart | 8 +++--- test/unit/notification_service_test.dart | 26 ++++++++++++++++++ test/widget/crash_screen_test.dart | 9 +++++-- 4 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 test/unit/notification_service_test.dart diff --git a/lib/core/services/notification_service.dart b/lib/core/services/notification_service.dart index dfcbb54..3e366af 100644 --- a/lib/core/services/notification_service.dart +++ b/lib/core/services/notification_service.dart @@ -1,26 +1,35 @@ import 'dart:io'; +import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; const _kChannelId = 'new_mail'; const _kChannelName = 'New mail'; final _plugin = FlutterLocalNotificationsPlugin(); +bool _initialized = false; Future initNotifications() async { - const android = AndroidInitializationSettings('@mipmap/ic_launcher'); - await _plugin.initialize( - const InitializationSettings(android: android), - onDidReceiveNotificationResponse: (_) {}, - ); - await _plugin - .resolvePlatformSpecificImplementation< - AndroidFlutterLocalNotificationsPlugin>() - ?.requestNotificationsPermission(); + try { + const android = AndroidInitializationSettings('@mipmap/ic_launcher'); + await _plugin.initialize( + const InitializationSettings(android: android), + onDidReceiveNotificationResponse: (_) {}, + ); + await _plugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.requestNotificationsPermission(); + _initialized = true; + } on MissingPluginException { + // Plugin not registered on this device; notifications silently disabled. + } catch (_) { + // Unexpected initialization failure; notifications silently disabled. + } } Future showNewMailNotification(String accountEmail) async { - if (!Platform.isAndroid) return; + if (!Platform.isAndroid || !_initialized) return; await _plugin.show( accountEmail.hashCode & 0x7FFFFFFF, 'New mail', diff --git a/lib/ui/screens/crash_screen.dart b/lib/ui/screens/crash_screen.dart index a712db7..0badbae 100644 --- a/lib/ui/screens/crash_screen.dart +++ b/lib/ui/screens/crash_screen.dart @@ -112,13 +112,15 @@ class CrashScreen extends StatelessWidget { const SizedBox(height: 16), OutlinedButton.icon( onPressed: () async { - final report = await _buildReport(); + // URL carries only the title to avoid exceeding browser + // URL-length limits — long stack traces caused "create + // issue failed" (#146). Use "Copy to Clipboard" first to + // get the full report, then paste it in the issue body. final title = Uri.encodeComponent( 'Crash: ${exception.toString().split('\n').first}', ); - final body = Uri.encodeComponent(report); final url = Uri.parse( - 'https://codeberg.org/guettli/sharedinbox/issues/new?title=$title&body=$body', + 'https://codeberg.org/guettli/sharedinbox/issues/new?title=$title', ); try { final launched = await launchUrl( diff --git a/test/unit/notification_service_test.dart b/test/unit/notification_service_test.dart new file mode 100644 index 0000000..f876f42 --- /dev/null +++ b/test/unit/notification_service_test.dart @@ -0,0 +1,26 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sharedinbox/core/services/notification_service.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Regression test for https://codeberg.org/guettli/sharedinbox/issues/146: + // On some Android devices the flutter_local_notifications plugin channel is + // absent at startup, throwing MissingPluginException (or a similar error). + // initNotifications() must absorb the failure and let the app continue. + test( + 'initNotifications completes without throwing when plugin is unavailable', + () async { + // In the unit-test environment the native plugin is not registered, so + // _plugin.initialize() throws. The fix catches it and keeps _initialized + // false. This test fails before the fix (exception propagates) and passes + // after it (exception is swallowed). + await expectLater(initNotifications(), completes); + }); + + test('showNewMailNotification completes without throwing', () async { + // Platform.isAndroid is false in tests, so this returns early without + // touching the plugin. Ensures the guard path is exercised. + await expectLater(showNewMailNotification('test@example.com'), completes); + }); +} diff --git a/test/widget/crash_screen_test.dart b/test/widget/crash_screen_test.dart index c5c4898..c897fe5 100644 --- a/test/widget/crash_screen_test.dart +++ b/test/widget/crash_screen_test.dart @@ -59,6 +59,10 @@ void main() { await tester.tap(find.text('Report Issue on Codeberg')); await tester.pumpAndSettle(); + // Regression for #146: URL must contain only the title, NOT the full + // report body. Long stack traces caused "create issue failed" by + // exceeding browser URL-length limits. The report is copied to clipboard + // so the user can paste it into the issue body. expect( mock.launchedUrl, contains('https://codeberg.org/guettli/sharedinbox/issues/new'), @@ -67,8 +71,9 @@ void main() { mock.launchedUrl, contains('title=Crash%3A%20TestException%3A%20something%20broke'), ); - expect(mock.launchedUrl, contains('App%20Version%3A%201.0.0%2B42')); - expect(mock.launchedUrl, contains('TestException%3A%20something%20broke')); + expect(mock.launchedUrl, isNot(contains('&body='))); + expect(mock.launchedUrl, isNot(contains('App%20Version'))); + expect(mock.launchedUrl, isNot(contains('Stack%20Trace'))); }); testWidgets(