From 0614b95462a389d67bd20ea2e9cda6e998e3ab56 Mon Sep 17 00:00:00 2001 From: Dennis Loose Date: Wed, 26 Jun 2024 16:39:24 +0200 Subject: [PATCH 1/7] feat: add error localizations --- packages/app_center/lib/error/error.dart | 1 + packages/app_center/lib/error/error_l10n.dart | 37 ++++++++++++++ packages/app_center/lib/src/l10n/app_en.arb | 12 ++++- packages/app_center/test/error_l10n_test.dart | 48 +++++++++++++++++++ 4 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 packages/app_center/lib/error/error_l10n.dart create mode 100644 packages/app_center/test/error_l10n_test.dart diff --git a/packages/app_center/lib/error/error.dart b/packages/app_center/lib/error/error.dart index 1028bde1c..39ab1d951 100644 --- a/packages/app_center/lib/error/error.dart +++ b/packages/app_center/lib/error/error.dart @@ -1 +1,2 @@ +export 'error_l10n.dart'; export 'error_observer.dart'; diff --git a/packages/app_center/lib/error/error_l10n.dart b/packages/app_center/lib/error/error_l10n.dart new file mode 100644 index 000000000..7f80c86b1 --- /dev/null +++ b/packages/app_center/lib/error/error_l10n.dart @@ -0,0 +1,37 @@ +import 'package:app_center/l10n.dart'; +import 'package:snapd/snapd.dart'; + +typedef PatternMap = ({ + RegExp pattern, + String Function(AppLocalizations l10n, RegExpMatch match) message, +}); + +final _patternMaps = [ + ( + pattern: RegExp('too many requests'), + message: (l10n, _) => l10n.snapdExceptionTooManyRequests, + ), + ( + pattern: + RegExp(r'cannot refresh "(.*?)": snap "\1" has running apps \((.*?)\)'), + message: (l10n, match) => l10n.snapdExceptionRunningApps( + match.group(1).toString(), + ), + ), +]; + +extension SnapdExceptionL10n on SnapdException { + String prettyFormat(AppLocalizations l10n) { + switch (kind) { + case 'network-timeout': + return l10n.snapdExceptionNetworkTimeout; + } + for (final patternMap in _patternMaps) { + final match = patternMap.pattern.firstMatch(message); + if (match != null) { + return patternMap.message(l10n, match); + } + } + return message; + } +} diff --git a/packages/app_center/lib/src/l10n/app_en.arb b/packages/app_center/lib/src/l10n/app_en.arb index 61d6d7d88..08e4bc6a7 100644 --- a/packages/app_center/lib/src/l10n/app_en.arb +++ b/packages/app_center/lib/src/l10n/app_en.arb @@ -253,5 +253,15 @@ "localDebWarningBody": "This package is provided by a third party. Installing packages from outside the App Center can increase the risk to your system and personal data. Ensure you trust the source before proceeding.", "localDebLearnMore": "Learn more about third party packages", "localDebDialogMessage": "This package is provided by a third party and may threaten your system and personal data.", - "localDebDialogConfirmation": "Are you sure you want to install it?" + "localDebDialogConfirmation": "Are you sure you want to install it?", + "snapdExceptionTooManyRequests": "Too many requests. Please try again later.", + "snapdExceptionRunningApps": "We couldn't update {snapName} because it is currently running.", + "@snapdExceptionRunningApps": { + "placeholders": { + "snapName": { + "type": "String" + } + } + }, + "snapdExceptionNetworkTimeout": "Network timeout. Please check your internet connection and try again." } \ No newline at end of file diff --git a/packages/app_center/test/error_l10n_test.dart b/packages/app_center/test/error_l10n_test.dart new file mode 100644 index 000000000..0011e8528 --- /dev/null +++ b/packages/app_center/test/error_l10n_test.dart @@ -0,0 +1,48 @@ +import 'package:app_center/error/error.dart'; +import 'package:app_center/l10n.dart'; +import 'package:app_center/src/l10n/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:snapd/snapd.dart'; + +import 'test_utils.dart'; + +void main() { + group('SnapdException', () { + final testCases = <({ + String name, + SnapdException exception, + String Function(AppLocalizations) expected + })>[ + ( + name: 'network timeout', + exception: SnapdException(kind: 'network-timeout', message: 'message'), + expected: (l10n) => l10n.snapdExceptionNetworkTimeout + ), + ( + name: 'too many requests', + exception: SnapdException(message: 'too many requests'), + expected: (l10n) => l10n.snapdExceptionTooManyRequests, + ), + ( + name: 'running apps', + exception: SnapdException( + kind: 'error', + message: + 'cannot refresh "testsnap": snap "testsnap" has running apps (testapp)', + ), + expected: (l10n) => l10n.snapdExceptionRunningApps('testsnap') + ), + ]; + + for (final testCase in testCases) { + testWidgets(testCase.name, (tester) async { + await tester.pumpApp((context) => const Scaffold()); + expect( + testCase.exception.prettyFormat(tester.l10n), + testCase.expected(tester.l10n), + ); + }); + } + }); +} From 29e9907a90ec46df0079781177b271ff145c4d0c Mon Sep 17 00:00:00 2001 From: Dennis Loose Date: Fri, 28 Jun 2024 11:39:50 +0200 Subject: [PATCH 2/7] feat: add ErrorView --- packages/app_center/assets/error.svg | 1 + packages/app_center/lib/deb/deb_page.dart | 3 +- .../app_center/lib/deb/local_deb_page.dart | 3 +- packages/app_center/lib/error/error.dart | 1 + packages/app_center/lib/error/error_view.dart | 49 +++++++++++++++++++ .../app_center/lib/manage/manage_page.dart | 3 +- .../app_center/lib/search/search_page.dart | 5 +- packages/app_center/lib/snapd/snap_model.dart | 1 - packages/app_center/lib/snapd/snap_page.dart | 4 +- packages/app_center/lib/snapd/snapd.dart | 1 + packages/app_center/lib/src/l10n/app_en.arb | 4 +- packages/app_center/lib/store/store_app.dart | 1 + packages/app_center/pubspec.yaml | 1 + packages/app_center/test/store_app_test.dart | 3 ++ 14 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 packages/app_center/assets/error.svg create mode 100644 packages/app_center/lib/error/error_view.dart diff --git a/packages/app_center/assets/error.svg b/packages/app_center/assets/error.svg new file mode 100644 index 000000000..10fe9ddce --- /dev/null +++ b/packages/app_center/assets/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/app_center/lib/deb/deb_page.dart b/packages/app_center/lib/deb/deb_page.dart index 78af5d3d9..6110970df 100644 --- a/packages/app_center/lib/deb/deb_page.dart +++ b/packages/app_center/lib/deb/deb_page.dart @@ -4,6 +4,7 @@ import 'package:app_center/appstream/appstream.dart'; import 'package:app_center/constants.dart'; import 'package:app_center/deb/deb_model.dart'; import 'package:app_center/deb/deb_providers.dart'; +import 'package:app_center/error/error.dart'; import 'package:app_center/l10n.dart'; import 'package:app_center/layout.dart'; import 'package:app_center/packagekit/packagekit.dart'; @@ -59,7 +60,7 @@ class _DebPageState extends ConsumerState { debModel: debModel, ), ), - error: (error, stackTrace) => ErrorWidget(error), + error: (error, stackTrace) => ErrorView(error: error), loading: () => const Center(child: YaruCircularProgressIndicator()), ); } diff --git a/packages/app_center/lib/deb/local_deb_page.dart b/packages/app_center/lib/deb/local_deb_page.dart index b9cc4de04..03d08bec0 100644 --- a/packages/app_center/lib/deb/local_deb_page.dart +++ b/packages/app_center/lib/deb/local_deb_page.dart @@ -1,5 +1,6 @@ import 'package:app_center/constants.dart'; import 'package:app_center/deb/local_deb_model.dart'; +import 'package:app_center/error/error.dart'; import 'package:app_center/l10n.dart'; import 'package:app_center/layout.dart'; import 'package:app_center/widgets/widgets.dart'; @@ -25,7 +26,7 @@ class LocalDebPage extends ConsumerWidget { return model.when( data: (debData) => _LocalDebPage(debData: debData), loading: () => const Center(child: YaruCircularProgressIndicator()), - error: (error, stackTrace) => ErrorWidget(error), + error: (error, stackTrace) => ErrorView(error: error), ); } } diff --git a/packages/app_center/lib/error/error.dart b/packages/app_center/lib/error/error.dart index 39ab1d951..cb8554e1c 100644 --- a/packages/app_center/lib/error/error.dart +++ b/packages/app_center/lib/error/error.dart @@ -1,2 +1,3 @@ export 'error_l10n.dart'; export 'error_observer.dart'; +export 'error_view.dart'; diff --git a/packages/app_center/lib/error/error_view.dart b/packages/app_center/lib/error/error_view.dart new file mode 100644 index 000000000..a44ff6aca --- /dev/null +++ b/packages/app_center/lib/error/error_view.dart @@ -0,0 +1,49 @@ +import 'package:app_center/error/error.dart'; +import 'package:app_center/l10n.dart'; +import 'package:app_center/layout.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:snapd/snapd.dart'; + +class ErrorView extends StatelessWidget { + const ErrorView({super.key, this.error, this.stackTrace}); + + final Object? error; + final StackTrace? stackTrace; + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context); + final message = switch (error) { + final SnapdException e => e.prettyFormat(l10n), + _ => l10n.errorViewUnknownError, + }; + return Padding( + padding: const EdgeInsets.all(kPagePadding), + child: Column( + children: [ + Flexible( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset('assets/error.svg'), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + l10n.errorViewTitle, + style: Theme.of(context).textTheme.titleMedium, + ), + Text(message), + ], + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/packages/app_center/lib/manage/manage_page.dart b/packages/app_center/lib/manage/manage_page.dart index 0e429b936..8161c1fcc 100644 --- a/packages/app_center/lib/manage/manage_page.dart +++ b/packages/app_center/lib/manage/manage_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:app_center/constants.dart'; +import 'package:app_center/error/error.dart'; import 'package:app_center/l10n.dart'; import 'package:app_center/layout.dart'; import 'package:app_center/manage/local_snap_providers.dart'; @@ -55,7 +56,7 @@ class _ManagePageState extends ConsumerState { final manageModel = ref.watch(manageModelProvider); return manageModel.state.when( data: (_) => _ManageView(manageModel: manageModel), - error: (error, stack) => ErrorWidget(error), + error: (error, stack) => ErrorView(error: error), loading: () => const Center(child: YaruCircularProgressIndicator()), ); } diff --git a/packages/app_center/lib/search/search_page.dart b/packages/app_center/lib/search/search_page.dart index 5db4d121a..03267d108 100644 --- a/packages/app_center/lib/search/search_page.dart +++ b/packages/app_center/lib/search/search_page.dart @@ -1,4 +1,5 @@ import 'package:app_center/appstream/appstream.dart'; +import 'package:app_center/error/error.dart'; import 'package:app_center/l10n.dart'; import 'package:app_center/layout.dart'; import 'package:app_center/search/search.dart'; @@ -232,7 +233,7 @@ class _DebSearchResults extends ConsumerWidget { ], ), ), - error: (error, stack) => ErrorWidget(error), + error: (error, stack) => ErrorView(error: error), loading: () => const Center(child: YaruCircularProgressIndicator()), ); } @@ -294,7 +295,7 @@ class _SnapSearchResults extends ConsumerWidget { ], ), ), - error: (error, stack) => ErrorWidget(error), + error: (error, stack) => ErrorView(error: error), loading: () => const Center(child: YaruCircularProgressIndicator()), ); } diff --git a/packages/app_center/lib/snapd/snap_model.dart b/packages/app_center/lib/snapd/snap_model.dart index 45b188b17..2868663eb 100644 --- a/packages/app_center/lib/snapd/snap_model.dart +++ b/packages/app_center/lib/snapd/snap_model.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:app_center/snapd/snap_data.dart'; import 'package:app_center/snapd/snapd.dart'; import 'package:app_center/snapd/snapd_cache.dart'; import 'package:collection/collection.dart'; diff --git a/packages/app_center/lib/snapd/snap_page.dart b/packages/app_center/lib/snapd/snap_page.dart index 6bb80a3ff..1dfed289a 100644 --- a/packages/app_center/lib/snapd/snap_page.dart +++ b/packages/app_center/lib/snapd/snap_page.dart @@ -1,9 +1,9 @@ import 'package:app_center/constants.dart'; +import 'package:app_center/error/error.dart'; import 'package:app_center/l10n.dart'; import 'package:app_center/layout.dart'; import 'package:app_center/ratings/ratings.dart'; import 'package:app_center/snapd/snap_action.dart'; -import 'package:app_center/snapd/snap_data.dart'; import 'package:app_center/snapd/snap_report.dart'; import 'package:app_center/snapd/snapd.dart'; import 'package:app_center/store/store_app.dart'; @@ -44,7 +44,7 @@ class SnapPage extends ConsumerWidget { ); }, ), - error: (error, stackTrace) => ErrorWidget(error), + error: (error, stackTrace) => ErrorView(error: error), loading: () => const Center(child: YaruCircularProgressIndicator()), ); } diff --git a/packages/app_center/lib/snapd/snapd.dart b/packages/app_center/lib/snapd/snapd.dart index 57360f023..7ae1804fd 100644 --- a/packages/app_center/lib/snapd/snapd.dart +++ b/packages/app_center/lib/snapd/snapd.dart @@ -1,4 +1,5 @@ export 'snap_category_enum.dart'; +export 'snap_data.dart'; export 'snap_l10n.dart'; export 'snap_launcher.dart'; export 'snap_model.dart'; diff --git a/packages/app_center/lib/src/l10n/app_en.arb b/packages/app_center/lib/src/l10n/app_en.arb index 08e4bc6a7..848f76a99 100644 --- a/packages/app_center/lib/src/l10n/app_en.arb +++ b/packages/app_center/lib/src/l10n/app_en.arb @@ -263,5 +263,7 @@ } } }, - "snapdExceptionNetworkTimeout": "Network timeout. Please check your internet connection and try again." + "snapdExceptionNetworkTimeout": "Network timeout. Please check your internet connection and try again.", + "errorViewTitle": "Something went wrong", + "errorViewUnknownError": "An unknown error occurred" } \ No newline at end of file diff --git a/packages/app_center/lib/store/store_app.dart b/packages/app_center/lib/store/store_app.dart index a9d1443ab..fad611a76 100644 --- a/packages/app_center/lib/store/store_app.dart +++ b/packages/app_center/lib/store/store_app.dart @@ -1,4 +1,5 @@ import 'package:app_center/deb/deb.dart'; +import 'package:app_center/error/error.dart'; import 'package:app_center/games/games.dart'; import 'package:app_center/l10n.dart'; import 'package:app_center/layout.dart'; diff --git a/packages/app_center/pubspec.yaml b/packages/app_center/pubspec.yaml index 794e0d6a5..446d07b6c 100644 --- a/packages/app_center/pubspec.yaml +++ b/packages/app_center/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: sdk: flutter flutter_markdown: ^0.6.16 flutter_riverpod: ^2.5.1 + flutter_svg: ^2.0.10+1 freezed_annotation: ^2.4.1 github: ^9.24.0 glib: ^0.0.1 diff --git a/packages/app_center/test/store_app_test.dart b/packages/app_center/test/store_app_test.dart index a219f118d..e45d5f930 100644 --- a/packages/app_center/test/store_app_test.dart +++ b/packages/app_center/test/store_app_test.dart @@ -72,6 +72,9 @@ void main() { testWidgets( 'errorStreamProvider receives exception when thrown', (tester) async { + registerMockService( + createMockGtkApplicationNotifier(), + ); final snapdService = registerMockSnapdService(); registerService(ErrorStreamController.new); From 63bf3cd375ccfdb7b2ac5281d36438bee2745f8b Mon Sep 17 00:00:00 2001 From: Dennis Loose Date: Fri, 28 Jun 2024 11:40:07 +0200 Subject: [PATCH 3/7] feat: show prettier messages in error dialog --- packages/app_center/lib/store/store_app.dart | 4 ++-- packages/app_center/test/store_app_test.dart | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/app_center/lib/store/store_app.dart b/packages/app_center/lib/store/store_app.dart index fad611a76..fefcc5e47 100644 --- a/packages/app_center/lib/store/store_app.dart +++ b/packages/app_center/lib/store/store_app.dart @@ -88,8 +88,8 @@ class _StoreAppHome extends ConsumerWidget { Future _showError(BuildContext context, SnapdException e) { return showErrorDialog( context: context, - title: e.kind ?? 'Unknown Snapd Exception', - message: e.message, + title: UbuntuLocalizations.of(context).errorLabel, + message: e.prettyFormat(AppLocalizations.of(context)), ); } diff --git a/packages/app_center/test/store_app_test.dart b/packages/app_center/test/store_app_test.dart index e45d5f930..07a634e1a 100644 --- a/packages/app_center/test/store_app_test.dart +++ b/packages/app_center/test/store_app_test.dart @@ -126,7 +126,6 @@ void main() { await tester.pump(); expect(find.text('error message'), findsOneWidget); - expect(find.text('error kind'), findsOneWidget); }); }); } From abcd42e0fddfd3183e232402c616a4af338039cb Mon Sep 17 00:00:00 2001 From: Dennis Loose Date: Fri, 28 Jun 2024 17:46:09 +0200 Subject: [PATCH 4/7] fix: remove unused imports --- packages/app_center/lib/snapd/snap_action.dart | 1 - packages/app_center/test/manage_page_test.dart | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/app_center/lib/snapd/snap_action.dart b/packages/app_center/lib/snapd/snap_action.dart index b4a6aa03b..1ea212af8 100644 --- a/packages/app_center/lib/snapd/snap_action.dart +++ b/packages/app_center/lib/snapd/snap_action.dart @@ -1,5 +1,4 @@ import 'package:app_center/l10n.dart'; -import 'package:app_center/snapd/snap_data.dart'; import 'package:app_center/snapd/snapd.dart'; import 'package:flutter/widgets.dart'; import 'package:yaru/icons.dart'; diff --git a/packages/app_center/test/manage_page_test.dart b/packages/app_center/test/manage_page_test.dart index ef2af9adc..e4a1ddf2d 100644 --- a/packages/app_center/test/manage_page_test.dart +++ b/packages/app_center/test/manage_page_test.dart @@ -1,7 +1,6 @@ import 'package:app_center/manage/local_snap_providers.dart'; import 'package:app_center/manage/manage.dart'; import 'package:app_center/manage/manage_model.dart'; -import 'package:app_center/snapd/snap_data.dart'; import 'package:app_center/snapd/snapd.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; From 45262212824b0a50690d7d1de2c5382d45c482c4 Mon Sep 17 00:00:00 2001 From: Dennis Loose Date: Wed, 10 Jul 2024 16:11:56 +0200 Subject: [PATCH 5/7] feat: match figma designs for error pages - add `ErrorMessage` structure for localized error title, body and actions - add retry logic where possible - link to snapcraft status page for server errors --- packages/app_center/lib/deb/deb_page.dart | 5 +- .../app_center/lib/deb/local_deb_page.dart | 5 +- packages/app_center/lib/error/error_l10n.dart | 97 ++++++++++++++----- packages/app_center/lib/error/error_view.dart | 38 ++++++-- .../app_center/lib/search/search_page.dart | 19 +++- packages/app_center/lib/snapd/snap_model.dart | 2 +- .../app_center/lib/snapd/snap_model.g.dart | 2 +- packages/app_center/lib/snapd/snap_page.dart | 6 +- packages/app_center/lib/src/l10n/app_en.arb | 13 ++- packages/app_center/lib/store/store_app.dart | 2 +- .../lib/widgets/iterable_extensions.dart | 10 ++ packages/app_center/test/error_l10n_test.dart | 36 +++++-- packages/app_center/test/store_app_test.dart | 16 ++- 13 files changed, 194 insertions(+), 57 deletions(-) create mode 100644 packages/app_center/lib/widgets/iterable_extensions.dart diff --git a/packages/app_center/lib/deb/deb_page.dart b/packages/app_center/lib/deb/deb_page.dart index 6110970df..7de80099c 100644 --- a/packages/app_center/lib/deb/deb_page.dart +++ b/packages/app_center/lib/deb/deb_page.dart @@ -60,7 +60,10 @@ class _DebPageState extends ConsumerState { debModel: debModel, ), ), - error: (error, stackTrace) => ErrorView(error: error), + error: (error, stackTrace) => ErrorView( + error: error, + onRetry: () => ref.invalidate(debModelProvider(widget.id)), + ), loading: () => const Center(child: YaruCircularProgressIndicator()), ); } diff --git a/packages/app_center/lib/deb/local_deb_page.dart b/packages/app_center/lib/deb/local_deb_page.dart index 03d08bec0..22ca6ae0f 100644 --- a/packages/app_center/lib/deb/local_deb_page.dart +++ b/packages/app_center/lib/deb/local_deb_page.dart @@ -26,7 +26,10 @@ class LocalDebPage extends ConsumerWidget { return model.when( data: (debData) => _LocalDebPage(debData: debData), loading: () => const Center(child: YaruCircularProgressIndicator()), - error: (error, stackTrace) => ErrorView(error: error), + error: (error, stackTrace) => ErrorView( + error: error, + onRetry: () => ref.invalidate(localDebModelProvider(path: path)), + ), ); } } diff --git a/packages/app_center/lib/error/error_l10n.dart b/packages/app_center/lib/error/error_l10n.dart index 7f80c86b1..1e43e63cd 100644 --- a/packages/app_center/lib/error/error_l10n.dart +++ b/packages/app_center/lib/error/error_l10n.dart @@ -1,37 +1,82 @@ import 'package:app_center/l10n.dart'; import 'package:snapd/snapd.dart'; -typedef PatternMap = ({ - RegExp pattern, - String Function(AppLocalizations l10n, RegExpMatch match) message, -}); - -final _patternMaps = [ - ( - pattern: RegExp('too many requests'), - message: (l10n, _) => l10n.snapdExceptionTooManyRequests, - ), - ( - pattern: - RegExp(r'cannot refresh "(.*?)": snap "\1" has running apps \((.*?)\)'), - message: (l10n, match) => l10n.snapdExceptionRunningApps( - match.group(1).toString(), - ), - ), -]; - -extension SnapdExceptionL10n on SnapdException { - String prettyFormat(AppLocalizations l10n) { - switch (kind) { +enum ErrorAction { + retry, + checkStatus, +} + +sealed class ErrorMessage { + const ErrorMessage(); + + factory ErrorMessage.fromObject(Object? e) { + if (e is! SnapdException) return ErrorMessageUnkown(); + + switch (e.kind) { case 'network-timeout': - return l10n.snapdExceptionNetworkTimeout; + return ErrorMessageNetwork(); } for (final patternMap in _patternMaps) { - final match = patternMap.pattern.firstMatch(message); + final match = patternMap.pattern.firstMatch(e.message); if (match != null) { - return patternMap.message(l10n, match); + return patternMap.message(match); } } - return message; + return ErrorMessageUnkown(); } + + static final _patternMaps = + <({RegExp pattern, ErrorMessage Function(Match) message})>[ + ( + pattern: RegExp('too many requests'), + message: (_) => ErrorMessageTooManyRequests(), + ), + ( + pattern: RegExp( + r'cannot refresh "(.*?)": snap "\1" has running apps \((.*?)\)', + ), + message: (match) => ErrorMessageRunningApps(match.group(1)!), + ), + ( + pattern: RegExp('persistent network error'), + message: (_) => ErrorMessageNetwork(), + ), + ]; + + String body(AppLocalizations l10n) => switch (this) { + ErrorMessageNetwork() => l10n.errorViewNetworkErrorDescription, + ErrorMessageTooManyRequests() => l10n.errorViewServerErrorDescription, + ErrorMessageRunningApps(snap: final snap) => + l10n.snapdExceptionRunningApps(snap), + _ => l10n.errorViewUnknownErrorDescription, + }; + + String title(AppLocalizations l10n) => switch (this) { + ErrorMessageNetwork() => l10n.errorViewNetworkErrorTitle, + _ => l10n.errorViewUnknownErrorTitle, + }; + + String actionLabel(AppLocalizations l10n) => switch (this) { + ErrorMessageNetwork() => l10n.errorViewNetworkErrorAction, + ErrorMessageTooManyRequests() => l10n.errorViewServerErrorAction, + _ => l10n.errorViewUnknownErrorAction, + }; + + List get actions => switch (this) { + ErrorMessageNetwork() => [ErrorAction.retry], + ErrorMessageTooManyRequests() => [ErrorAction.checkStatus], + ErrorMessageRunningApps() => [], + _ => [ErrorAction.retry, ErrorAction.checkStatus], + }; } + +class ErrorMessageNetwork extends ErrorMessage {} + +class ErrorMessageTooManyRequests extends ErrorMessage {} + +class ErrorMessageRunningApps extends ErrorMessage { + const ErrorMessageRunningApps(this.snap); + final String snap; +} + +class ErrorMessageUnkown extends ErrorMessage {} diff --git a/packages/app_center/lib/error/error_view.dart b/packages/app_center/lib/error/error_view.dart index a44ff6aca..a4c93e508 100644 --- a/packages/app_center/lib/error/error_view.dart +++ b/packages/app_center/lib/error/error_view.dart @@ -1,28 +1,32 @@ import 'package:app_center/error/error.dart'; import 'package:app_center/l10n.dart'; import 'package:app_center/layout.dart'; +import 'package:app_center/widgets/iterable_extensions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:snapd/snapd.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class ErrorView extends StatelessWidget { - const ErrorView({super.key, this.error, this.stackTrace}); + const ErrorView({super.key, this.error, this.stackTrace, this.onRetry}); + + static const statusUrl = 'https://status.snapcraft.io/'; final Object? error; final StackTrace? stackTrace; + final VoidCallback? onRetry; @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context); - final message = switch (error) { - final SnapdException e => e.prettyFormat(l10n), - _ => l10n.errorViewUnknownError, - }; + final message = ErrorMessage.fromObject(error); + return Padding( padding: const EdgeInsets.all(kPagePadding), child: Column( children: [ + const Spacer(), Flexible( + flex: 2, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -33,15 +37,33 @@ class ErrorView extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - l10n.errorViewTitle, + message.title(l10n), style: Theme.of(context).textTheme.titleMedium, ), - Text(message), + Flexible(child: Text(message.body(l10n))), + const SizedBox(height: kPagePadding), + Row( + children: [ + if (message.actions.contains(ErrorAction.retry)) + OutlinedButton( + onPressed: onRetry, + child: Text( + UbuntuLocalizations.of(context).retryLabel, + ), + ), + if (message.actions.contains(ErrorAction.checkStatus)) + OutlinedButton( + onPressed: () => launchUrlString(statusUrl), + child: Text(l10n.errorViewCheckStatusLabel), + ), + ].separatedBy(const SizedBox(width: 10)), + ), ], ), ], ), ), + const Spacer(flex: 5), ], ), ); diff --git a/packages/app_center/lib/search/search_page.dart b/packages/app_center/lib/search/search_page.dart index 03267d108..65acc845c 100644 --- a/packages/app_center/lib/search/search_page.dart +++ b/packages/app_center/lib/search/search_page.dart @@ -233,7 +233,10 @@ class _DebSearchResults extends ConsumerWidget { ], ), ), - error: (error, stack) => ErrorView(error: error), + error: (error, stack) => ErrorView( + error: error, + onRetry: () => ref.invalidate(appstreamSearchProvider(query ?? '')), + ), loading: () => const Center(child: YaruCircularProgressIndicator()), ); } @@ -295,7 +298,19 @@ class _SnapSearchResults extends ConsumerWidget { ], ), ), - error: (error, stack) => ErrorView(error: error), + error: (error, stack) => ErrorView( + error: error, + onRetry: () { + ref.invalidate( + snapSearchProvider( + SnapSearchParameters( + query: query, + category: category, + ), + ), + ); + }, + ), loading: () => const Center(child: YaruCircularProgressIndicator()), ); } diff --git a/packages/app_center/lib/snapd/snap_model.dart b/packages/app_center/lib/snapd/snap_model.dart index 2868663eb..185a49f29 100644 --- a/packages/app_center/lib/snapd/snap_model.dart +++ b/packages/app_center/lib/snapd/snap_model.dart @@ -25,7 +25,7 @@ class SnapModel extends _$SnapModel { final storeSnap = await ref .watch(storeSnapProvider(snapName).future) - .onError((_, __) => null); + .onError((_, __) => null, test: (_) => localSnap != null); final activeChangeId = (await _snapd.getChanges(name: snapName)) .firstWhereOrNull((change) => !change.ready) diff --git a/packages/app_center/lib/snapd/snap_model.g.dart b/packages/app_center/lib/snapd/snap_model.g.dart index 35488eea6..cad092ded 100644 --- a/packages/app_center/lib/snapd/snap_model.g.dart +++ b/packages/app_center/lib/snapd/snap_model.g.dart @@ -6,7 +6,7 @@ part of 'snap_model.dart'; // RiverpodGenerator // ************************************************************************** -String _$snapModelHash() => r'fb77fb4987e65704071ba6dae0fae5a2dd99dd2b'; +String _$snapModelHash() => r'ff068b85e25f4e16765c867e3096dfce06bcf742'; /// Copied from Dart SDK class _SystemHash { diff --git a/packages/app_center/lib/snapd/snap_page.dart b/packages/app_center/lib/snapd/snap_page.dart index 1dfed289a..ed16e4bb2 100644 --- a/packages/app_center/lib/snapd/snap_page.dart +++ b/packages/app_center/lib/snapd/snap_page.dart @@ -6,6 +6,7 @@ import 'package:app_center/ratings/ratings.dart'; import 'package:app_center/snapd/snap_action.dart'; import 'package:app_center/snapd/snap_report.dart'; import 'package:app_center/snapd/snapd.dart'; +import 'package:app_center/snapd/snapd_cache.dart'; import 'package:app_center/store/store_app.dart'; import 'package:app_center/widgets/widgets.dart'; import 'package:collection/collection.dart'; @@ -44,7 +45,10 @@ class SnapPage extends ConsumerWidget { ); }, ), - error: (error, stackTrace) => ErrorView(error: error), + error: (error, stackTrace) => ErrorView( + error: error, + onRetry: () => ref.invalidate(storeSnapProvider(snapName)), + ), loading: () => const Center(child: YaruCircularProgressIndicator()), ); } diff --git a/packages/app_center/lib/src/l10n/app_en.arb b/packages/app_center/lib/src/l10n/app_en.arb index 848f76a99..2b570b135 100644 --- a/packages/app_center/lib/src/l10n/app_en.arb +++ b/packages/app_center/lib/src/l10n/app_en.arb @@ -254,7 +254,6 @@ "localDebLearnMore": "Learn more about third party packages", "localDebDialogMessage": "This package is provided by a third party and may threaten your system and personal data.", "localDebDialogConfirmation": "Are you sure you want to install it?", - "snapdExceptionTooManyRequests": "Too many requests. Please try again later.", "snapdExceptionRunningApps": "We couldn't update {snapName} because it is currently running.", "@snapdExceptionRunningApps": { "placeholders": { @@ -263,7 +262,13 @@ } } }, - "snapdExceptionNetworkTimeout": "Network timeout. Please check your internet connection and try again.", - "errorViewTitle": "Something went wrong", - "errorViewUnknownError": "An unknown error occurred" + "errorViewCheckStatusLabel": "Check status", + "errorViewNetworkErrorTitle": "Connect to internet", + "errorViewNetworkErrorDescription": "We can't load content in the App Center without an internet connection.", + "errorViewNetworkErrorAction": "Check your connection and retry.", + "errorViewServerErrorDescription": "We're sorry, we are currently experiencing problems with the App Center.", + "errorViewServerErrorAction": "Check the status for updates or try again later.", + "errorViewUnknownErrorTitle": "Something went wrong", + "errorViewUnknownErrorDescription": "We're sorry, but we’re not sure what the error is.", + "errorViewUnknownErrorAction": "You can retry now, check the status for updates, or try again later." } \ No newline at end of file diff --git a/packages/app_center/lib/store/store_app.dart b/packages/app_center/lib/store/store_app.dart index fefcc5e47..b1346a206 100644 --- a/packages/app_center/lib/store/store_app.dart +++ b/packages/app_center/lib/store/store_app.dart @@ -89,7 +89,7 @@ class _StoreAppHome extends ConsumerWidget { return showErrorDialog( context: context, title: UbuntuLocalizations.of(context).errorLabel, - message: e.prettyFormat(AppLocalizations.of(context)), + message: ErrorMessage.fromObject(e).body(AppLocalizations.of(context)), ); } diff --git a/packages/app_center/lib/widgets/iterable_extensions.dart b/packages/app_center/lib/widgets/iterable_extensions.dart new file mode 100644 index 000000000..280f7fd1c --- /dev/null +++ b/packages/app_center/lib/widgets/iterable_extensions.dart @@ -0,0 +1,10 @@ +import 'package:flutter/widgets.dart'; + +extension WidgetIterableExtension on Iterable { + List separatedBy(Widget separator) { + return expand((item) sync* { + yield separator; + yield item; + }).skip(1).toList(); + } +} diff --git a/packages/app_center/test/error_l10n_test.dart b/packages/app_center/test/error_l10n_test.dart index 0011e8528..7e472840d 100644 --- a/packages/app_center/test/error_l10n_test.dart +++ b/packages/app_center/test/error_l10n_test.dart @@ -1,6 +1,6 @@ import 'package:app_center/error/error.dart'; +import 'package:app_center/error/error_l10n.dart'; import 'package:app_center/l10n.dart'; -import 'package:app_center/src/l10n/l10n.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:snapd/snapd.dart'; @@ -12,17 +12,26 @@ void main() { final testCases = <({ String name, SnapdException exception, - String Function(AppLocalizations) expected + String Function(AppLocalizations l10n) expectedTitle, + String Function(AppLocalizations l10n) expectedBody, + String Function(AppLocalizations l10n) expectedActionLabel, + List expectedActions, })>[ ( name: 'network timeout', exception: SnapdException(kind: 'network-timeout', message: 'message'), - expected: (l10n) => l10n.snapdExceptionNetworkTimeout + expectedTitle: (l10n) => l10n.errorViewNetworkErrorTitle, + expectedBody: (l10n) => l10n.errorViewNetworkErrorDescription, + expectedActionLabel: (l10n) => l10n.errorViewNetworkErrorAction, + expectedActions: [ErrorAction.retry], ), ( name: 'too many requests', exception: SnapdException(message: 'too many requests'), - expected: (l10n) => l10n.snapdExceptionTooManyRequests, + expectedTitle: (l10n) => l10n.errorViewUnknownErrorTitle, + expectedBody: (l10n) => l10n.errorViewServerErrorDescription, + expectedActionLabel: (l10n) => l10n.errorViewServerErrorAction, + expectedActions: [ErrorAction.checkStatus], ), ( name: 'running apps', @@ -31,17 +40,30 @@ void main() { message: 'cannot refresh "testsnap": snap "testsnap" has running apps (testapp)', ), - expected: (l10n) => l10n.snapdExceptionRunningApps('testsnap') + expectedTitle: (l10n) => l10n.errorViewUnknownErrorTitle, + expectedBody: (l10n) => l10n.snapdExceptionRunningApps('testsnap'), + expectedActionLabel: (l10n) => l10n.errorViewUnknownErrorAction, + expectedActions: [], ), ]; for (final testCase in testCases) { testWidgets(testCase.name, (tester) async { await tester.pumpApp((context) => const Scaffold()); + final message = ErrorMessage.fromObject(testCase.exception); expect( - testCase.exception.prettyFormat(tester.l10n), - testCase.expected(tester.l10n), + message.title(tester.l10n), + testCase.expectedTitle(tester.l10n), ); + expect( + message.body(tester.l10n), + testCase.expectedBody(tester.l10n), + ); + expect( + message.actionLabel(tester.l10n), + testCase.expectedActionLabel(tester.l10n), + ); + expect(message.actions, testCase.expectedActions); }); } }); diff --git a/packages/app_center/test/store_app_test.dart b/packages/app_center/test/store_app_test.dart index 07a634e1a..76f16a0b0 100644 --- a/packages/app_center/test/store_app_test.dart +++ b/packages/app_center/test/store_app_test.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:app_center/error/error_l10n.dart'; import 'package:app_center/providers/error_stream_provider.dart'; import 'package:app_center/ratings/ratings.dart'; import 'package:app_center/snapd/snapd.dart'; @@ -16,6 +17,7 @@ import 'package:yaru/yaru.dart'; import 'test_utils.dart'; void main() { + tearDown(resetAllServices); group('updates badge', () { testWidgets('no updates available', (tester) async { registerMockService( @@ -111,13 +113,16 @@ void main() { testWidgets('showing error from error stream', (tester) async { registerMockSnapdService(); + registerMockService( + createMockGtkApplicationNotifier(), + ); + final error = + SnapdException(message: 'error message', kind: 'error kind'); await tester.pumpApp( (_) => ProviderScope( overrides: [ errorStreamProvider.overrideWith( - (ref) => Stream.value( - SnapdException(message: 'error message', kind: 'error kind'), - ), + (ref) => Stream.value(error), ), ], child: const StoreApp(), @@ -125,7 +130,10 @@ void main() { ); await tester.pump(); - expect(find.text('error message'), findsOneWidget); + expect( + find.text(ErrorMessage.fromObject(error).body(tester.l10n)), + findsOneWidget, + ); }); }); } From 145e571215f7c853542d5711f0e1fbae3db6718d Mon Sep 17 00:00:00 2001 From: Dennis Loose Date: Wed, 10 Jul 2024 17:20:50 +0200 Subject: [PATCH 6/7] fix(error_view): add missing action text --- packages/app_center/lib/error/error_view.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/app_center/lib/error/error_view.dart b/packages/app_center/lib/error/error_view.dart index a4c93e508..6ef79a95a 100644 --- a/packages/app_center/lib/error/error_view.dart +++ b/packages/app_center/lib/error/error_view.dart @@ -42,6 +42,10 @@ class ErrorView extends StatelessWidget { ), Flexible(child: Text(message.body(l10n))), const SizedBox(height: kPagePadding), + if (message.actions.isNotEmpty) ...[ + Flexible(child: Text(message.actionLabel(l10n))), + const SizedBox(height: kPagePadding), + ], Row( children: [ if (message.actions.contains(ErrorAction.retry)) From d6bbe1ee6f384f24e1826ae36a3dce6f2586c529 Mon Sep 17 00:00:00 2001 From: Dennis Loose Date: Thu, 11 Jul 2024 11:46:49 +0200 Subject: [PATCH 7/7] feat(error_view): use column layout --- packages/app_center/lib/error/error_view.dart | 70 ++++++++----------- 1 file changed, 29 insertions(+), 41 deletions(-) diff --git a/packages/app_center/lib/error/error_view.dart b/packages/app_center/lib/error/error_view.dart index 6ef79a95a..543885a00 100644 --- a/packages/app_center/lib/error/error_view.dart +++ b/packages/app_center/lib/error/error_view.dart @@ -23,51 +23,39 @@ class ErrorView extends StatelessWidget { return Padding( padding: const EdgeInsets.all(kPagePadding), child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, children: [ const Spacer(), - Flexible( - flex: 2, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset('assets/error.svg'), - Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - message.title(l10n), - style: Theme.of(context).textTheme.titleMedium, - ), - Flexible(child: Text(message.body(l10n))), - const SizedBox(height: kPagePadding), - if (message.actions.isNotEmpty) ...[ - Flexible(child: Text(message.actionLabel(l10n))), - const SizedBox(height: kPagePadding), - ], - Row( - children: [ - if (message.actions.contains(ErrorAction.retry)) - OutlinedButton( - onPressed: onRetry, - child: Text( - UbuntuLocalizations.of(context).retryLabel, - ), - ), - if (message.actions.contains(ErrorAction.checkStatus)) - OutlinedButton( - onPressed: () => launchUrlString(statusUrl), - child: Text(l10n.errorViewCheckStatusLabel), - ), - ].separatedBy(const SizedBox(width: 10)), - ), - ], + SvgPicture.asset('assets/error.svg'), + Text( + message.title(l10n), + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: kPagePadding), + Flexible(child: Text(message.body(l10n))), + if (message.actions.isNotEmpty) ...[ + Flexible(child: Text(message.actionLabel(l10n))), + const SizedBox(height: kPagePadding), + ], + Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (message.actions.contains(ErrorAction.retry)) + OutlinedButton( + onPressed: onRetry, + child: Text( + UbuntuLocalizations.of(context).retryLabel, + ), + ), + if (message.actions.contains(ErrorAction.checkStatus)) + OutlinedButton( + onPressed: () => launchUrlString(statusUrl), + child: Text(l10n.errorViewCheckStatusLabel), ), - ], - ), + ].separatedBy(const SizedBox(width: 10)), ), - const Spacer(flex: 5), + const Spacer(flex: 3), ], ), );