Skip to content

Commit 4526221

Browse files
committed
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
1 parent abcd42e commit 4526221

File tree

13 files changed

+194
-57
lines changed

13 files changed

+194
-57
lines changed

packages/app_center/lib/deb/deb_page.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ class _DebPageState extends ConsumerState<DebPage> {
6060
debModel: debModel,
6161
),
6262
),
63-
error: (error, stackTrace) => ErrorView(error: error),
63+
error: (error, stackTrace) => ErrorView(
64+
error: error,
65+
onRetry: () => ref.invalidate(debModelProvider(widget.id)),
66+
),
6467
loading: () => const Center(child: YaruCircularProgressIndicator()),
6568
);
6669
}

packages/app_center/lib/deb/local_deb_page.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ class LocalDebPage extends ConsumerWidget {
2626
return model.when(
2727
data: (debData) => _LocalDebPage(debData: debData),
2828
loading: () => const Center(child: YaruCircularProgressIndicator()),
29-
error: (error, stackTrace) => ErrorView(error: error),
29+
error: (error, stackTrace) => ErrorView(
30+
error: error,
31+
onRetry: () => ref.invalidate(localDebModelProvider(path: path)),
32+
),
3033
);
3134
}
3235
}
Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,82 @@
11
import 'package:app_center/l10n.dart';
22
import 'package:snapd/snapd.dart';
33

4-
typedef PatternMap = ({
5-
RegExp pattern,
6-
String Function(AppLocalizations l10n, RegExpMatch match) message,
7-
});
8-
9-
final _patternMaps = <PatternMap>[
10-
(
11-
pattern: RegExp('too many requests'),
12-
message: (l10n, _) => l10n.snapdExceptionTooManyRequests,
13-
),
14-
(
15-
pattern:
16-
RegExp(r'cannot refresh "(.*?)": snap "\1" has running apps \((.*?)\)'),
17-
message: (l10n, match) => l10n.snapdExceptionRunningApps(
18-
match.group(1).toString(),
19-
),
20-
),
21-
];
22-
23-
extension SnapdExceptionL10n on SnapdException {
24-
String prettyFormat(AppLocalizations l10n) {
25-
switch (kind) {
4+
enum ErrorAction {
5+
retry,
6+
checkStatus,
7+
}
8+
9+
sealed class ErrorMessage {
10+
const ErrorMessage();
11+
12+
factory ErrorMessage.fromObject(Object? e) {
13+
if (e is! SnapdException) return ErrorMessageUnkown();
14+
15+
switch (e.kind) {
2616
case 'network-timeout':
27-
return l10n.snapdExceptionNetworkTimeout;
17+
return ErrorMessageNetwork();
2818
}
2919
for (final patternMap in _patternMaps) {
30-
final match = patternMap.pattern.firstMatch(message);
20+
final match = patternMap.pattern.firstMatch(e.message);
3121
if (match != null) {
32-
return patternMap.message(l10n, match);
22+
return patternMap.message(match);
3323
}
3424
}
35-
return message;
25+
return ErrorMessageUnkown();
3626
}
27+
28+
static final _patternMaps =
29+
<({RegExp pattern, ErrorMessage Function(Match) message})>[
30+
(
31+
pattern: RegExp('too many requests'),
32+
message: (_) => ErrorMessageTooManyRequests(),
33+
),
34+
(
35+
pattern: RegExp(
36+
r'cannot refresh "(.*?)": snap "\1" has running apps \((.*?)\)',
37+
),
38+
message: (match) => ErrorMessageRunningApps(match.group(1)!),
39+
),
40+
(
41+
pattern: RegExp('persistent network error'),
42+
message: (_) => ErrorMessageNetwork(),
43+
),
44+
];
45+
46+
String body(AppLocalizations l10n) => switch (this) {
47+
ErrorMessageNetwork() => l10n.errorViewNetworkErrorDescription,
48+
ErrorMessageTooManyRequests() => l10n.errorViewServerErrorDescription,
49+
ErrorMessageRunningApps(snap: final snap) =>
50+
l10n.snapdExceptionRunningApps(snap),
51+
_ => l10n.errorViewUnknownErrorDescription,
52+
};
53+
54+
String title(AppLocalizations l10n) => switch (this) {
55+
ErrorMessageNetwork() => l10n.errorViewNetworkErrorTitle,
56+
_ => l10n.errorViewUnknownErrorTitle,
57+
};
58+
59+
String actionLabel(AppLocalizations l10n) => switch (this) {
60+
ErrorMessageNetwork() => l10n.errorViewNetworkErrorAction,
61+
ErrorMessageTooManyRequests() => l10n.errorViewServerErrorAction,
62+
_ => l10n.errorViewUnknownErrorAction,
63+
};
64+
65+
List<ErrorAction> get actions => switch (this) {
66+
ErrorMessageNetwork() => [ErrorAction.retry],
67+
ErrorMessageTooManyRequests() => [ErrorAction.checkStatus],
68+
ErrorMessageRunningApps() => [],
69+
_ => [ErrorAction.retry, ErrorAction.checkStatus],
70+
};
3771
}
72+
73+
class ErrorMessageNetwork extends ErrorMessage {}
74+
75+
class ErrorMessageTooManyRequests extends ErrorMessage {}
76+
77+
class ErrorMessageRunningApps extends ErrorMessage {
78+
const ErrorMessageRunningApps(this.snap);
79+
final String snap;
80+
}
81+
82+
class ErrorMessageUnkown extends ErrorMessage {}

packages/app_center/lib/error/error_view.dart

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,32 @@
11
import 'package:app_center/error/error.dart';
22
import 'package:app_center/l10n.dart';
33
import 'package:app_center/layout.dart';
4+
import 'package:app_center/widgets/iterable_extensions.dart';
45
import 'package:flutter/material.dart';
56
import 'package:flutter_svg/flutter_svg.dart';
6-
import 'package:snapd/snapd.dart';
7+
import 'package:url_launcher/url_launcher_string.dart';
78

89
class ErrorView extends StatelessWidget {
9-
const ErrorView({super.key, this.error, this.stackTrace});
10+
const ErrorView({super.key, this.error, this.stackTrace, this.onRetry});
11+
12+
static const statusUrl = 'https://status.snapcraft.io/';
1013

1114
final Object? error;
1215
final StackTrace? stackTrace;
16+
final VoidCallback? onRetry;
1317

1418
@override
1519
Widget build(BuildContext context) {
1620
final l10n = AppLocalizations.of(context);
17-
final message = switch (error) {
18-
final SnapdException e => e.prettyFormat(l10n),
19-
_ => l10n.errorViewUnknownError,
20-
};
21+
final message = ErrorMessage.fromObject(error);
22+
2123
return Padding(
2224
padding: const EdgeInsets.all(kPagePadding),
2325
child: Column(
2426
children: [
27+
const Spacer(),
2528
Flexible(
29+
flex: 2,
2630
child: Row(
2731
mainAxisAlignment: MainAxisAlignment.center,
2832
children: [
@@ -33,15 +37,33 @@ class ErrorView extends StatelessWidget {
3337
mainAxisSize: MainAxisSize.min,
3438
children: [
3539
Text(
36-
l10n.errorViewTitle,
40+
message.title(l10n),
3741
style: Theme.of(context).textTheme.titleMedium,
3842
),
39-
Text(message),
43+
Flexible(child: Text(message.body(l10n))),
44+
const SizedBox(height: kPagePadding),
45+
Row(
46+
children: [
47+
if (message.actions.contains(ErrorAction.retry))
48+
OutlinedButton(
49+
onPressed: onRetry,
50+
child: Text(
51+
UbuntuLocalizations.of(context).retryLabel,
52+
),
53+
),
54+
if (message.actions.contains(ErrorAction.checkStatus))
55+
OutlinedButton(
56+
onPressed: () => launchUrlString(statusUrl),
57+
child: Text(l10n.errorViewCheckStatusLabel),
58+
),
59+
].separatedBy(const SizedBox(width: 10)),
60+
),
4061
],
4162
),
4263
],
4364
),
4465
),
66+
const Spacer(flex: 5),
4567
],
4668
),
4769
);

packages/app_center/lib/search/search_page.dart

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,10 @@ class _DebSearchResults extends ConsumerWidget {
233233
],
234234
),
235235
),
236-
error: (error, stack) => ErrorView(error: error),
236+
error: (error, stack) => ErrorView(
237+
error: error,
238+
onRetry: () => ref.invalidate(appstreamSearchProvider(query ?? '')),
239+
),
237240
loading: () => const Center(child: YaruCircularProgressIndicator()),
238241
);
239242
}
@@ -295,7 +298,19 @@ class _SnapSearchResults extends ConsumerWidget {
295298
],
296299
),
297300
),
298-
error: (error, stack) => ErrorView(error: error),
301+
error: (error, stack) => ErrorView(
302+
error: error,
303+
onRetry: () {
304+
ref.invalidate(
305+
snapSearchProvider(
306+
SnapSearchParameters(
307+
query: query,
308+
category: category,
309+
),
310+
),
311+
);
312+
},
313+
),
299314
loading: () => const Center(child: YaruCircularProgressIndicator()),
300315
);
301316
}

packages/app_center/lib/snapd/snap_model.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class SnapModel extends _$SnapModel {
2525

2626
final storeSnap = await ref
2727
.watch(storeSnapProvider(snapName).future)
28-
.onError((_, __) => null);
28+
.onError((_, __) => null, test: (_) => localSnap != null);
2929

3030
final activeChangeId = (await _snapd.getChanges(name: snapName))
3131
.firstWhereOrNull((change) => !change.ready)

packages/app_center/lib/snapd/snap_model.g.dart

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/app_center/lib/snapd/snap_page.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:app_center/ratings/ratings.dart';
66
import 'package:app_center/snapd/snap_action.dart';
77
import 'package:app_center/snapd/snap_report.dart';
88
import 'package:app_center/snapd/snapd.dart';
9+
import 'package:app_center/snapd/snapd_cache.dart';
910
import 'package:app_center/store/store_app.dart';
1011
import 'package:app_center/widgets/widgets.dart';
1112
import 'package:collection/collection.dart';
@@ -44,7 +45,10 @@ class SnapPage extends ConsumerWidget {
4445
);
4546
},
4647
),
47-
error: (error, stackTrace) => ErrorView(error: error),
48+
error: (error, stackTrace) => ErrorView(
49+
error: error,
50+
onRetry: () => ref.invalidate(storeSnapProvider(snapName)),
51+
),
4852
loading: () => const Center(child: YaruCircularProgressIndicator()),
4953
);
5054
}

packages/app_center/lib/src/l10n/app_en.arb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,6 @@
254254
"localDebLearnMore": "Learn more about third party packages",
255255
"localDebDialogMessage": "This package is provided by a third party and may threaten your system and personal data.",
256256
"localDebDialogConfirmation": "Are you sure you want to install it?",
257-
"snapdExceptionTooManyRequests": "Too many requests. Please try again later.",
258257
"snapdExceptionRunningApps": "We couldn't update {snapName} because it is currently running.",
259258
"@snapdExceptionRunningApps": {
260259
"placeholders": {
@@ -263,7 +262,13 @@
263262
}
264263
}
265264
},
266-
"snapdExceptionNetworkTimeout": "Network timeout. Please check your internet connection and try again.",
267-
"errorViewTitle": "Something went wrong",
268-
"errorViewUnknownError": "An unknown error occurred"
265+
"errorViewCheckStatusLabel": "Check status",
266+
"errorViewNetworkErrorTitle": "Connect to internet",
267+
"errorViewNetworkErrorDescription": "We can't load content in the App Center without an internet connection.",
268+
"errorViewNetworkErrorAction": "Check your connection and retry.",
269+
"errorViewServerErrorDescription": "We're sorry, we are currently experiencing problems with the App Center.",
270+
"errorViewServerErrorAction": "Check the status for updates or try again later.",
271+
"errorViewUnknownErrorTitle": "Something went wrong",
272+
"errorViewUnknownErrorDescription": "We're sorry, but we’re not sure what the error is.",
273+
"errorViewUnknownErrorAction": "You can retry now, check the status for updates, or try again later."
269274
}

packages/app_center/lib/store/store_app.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ class _StoreAppHome extends ConsumerWidget {
8989
return showErrorDialog(
9090
context: context,
9191
title: UbuntuLocalizations.of(context).errorLabel,
92-
message: e.prettyFormat(AppLocalizations.of(context)),
92+
message: ErrorMessage.fromObject(e).body(AppLocalizations.of(context)),
9393
);
9494
}
9595

0 commit comments

Comments
 (0)