Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 28 additions & 10 deletions mobile/lib/features/channels/channels_page/badges.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,19 @@ class _EphemeralBadge extends StatelessWidget {
}
}

class _ErrorView extends StatelessWidget {
class _ErrorView extends HookConsumerWidget {
final Object error;
final VoidCallback onRetry;

const _ErrorView({required this.error, required this.onRetry});

static String _userMessage(Object error) {
if (error is RelayException) {
// There is no API token to check. A protected community's Retry gets a
// fresh token; once the organization sign-in has ended, the shell
// banner owns the action instead (see build).
if (error.statusCode == 401) {
return 'Not authorized. Check your API token.';
return "This community couldn't authenticate the request. Try again.";
}
if (error.statusCode == 403) {
return 'Access denied.';
Expand All @@ -50,7 +53,18 @@ class _ErrorView extends StatelessWidget {
}

@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final signIn = ref.watch(organizationSignInProvider);
final auth = ref.watch(hostedRelayAuthProvider);
useListenable(signIn);
useListenable(auth);
// With the organization sign-in ended, a read goes out unsigned and Retry
// cannot succeed. The Home banner then shows the working action.
final signInOwned = organizationSignInPromptVisible(
signIn,
auth,
ref.watch(relayConfigProvider),
);
return Center(
child: Padding(
padding: const EdgeInsets.all(Grid.sm),
Expand All @@ -65,20 +79,24 @@ class _ErrorView extends StatelessWidget {
),
const SizedBox(height: Grid.xxs),
Text(
_userMessage(error),
signInOwned
? 'Use the sign-in prompt below to reconnect.'
: _userMessage(error),
style: context.textTheme.bodySmall?.copyWith(
color: context.colors.onSurfaceVariant,
),
textAlign: TextAlign.center,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: Grid.xs),
FilledButton.icon(
onPressed: onRetry,
icon: const Icon(BuzzIcons.refreshCw),
label: const Text('Retry'),
),
if (!signInOwned) ...[
const SizedBox(height: Grid.xs),
FilledButton.icon(
onPressed: onRetry,
icon: const Icon(BuzzIcons.refreshCw),
label: const Text('Retry'),
),
],
],
),
),
Expand Down
69 changes: 69 additions & 0 deletions mobile/test/features/channels/channels_page_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3547,6 +3547,69 @@ void main() {
expect(find.text('Retry'), findsOneWidget);
});

testWidgets('a refused sign-in names no API token and keeps retry', (
tester,
) async {
await tester.pumpWidget(
buildTestable(
overrides: [
channelsProvider.overrideWith(() => _UnauthorizedNotifier()),
],
),
);
await tester.pump();
await tester.pump(const Duration(seconds: 3));
await tester.pump();

expect(
find.text("This community couldn't authenticate the request. Try again."),
findsOneWidget,
);
expect(find.textContaining('API token'), findsNothing);
expect(find.text('Retry'), findsOneWidget);
});

testWidgets(
'an ended organization sign-in leaves the action to the banner, not Retry',
(tester) async {
final account = BuilderlabAccountClient();
final signIn = OrganizationSignIn(
account: account,
browser: BrowserAuth(),
reconnect: () async {},
)..status = OrganizationSessionStatus.signedOut;
final auth = HostedRelayAuth(
BuilderlabAccountClient(),
organization: account,
organizationRelay: (_) => true,
);
addTearDown(() {
signIn.dispose();
auth.dispose();
account.dispose();
});
await tester.pumpWidget(
buildTestable(
overrides: [
organizationSignInProvider.overrideWithValue(signIn),
hostedRelayAuthProvider.overrideWithValue(auth),
channelsProvider.overrideWith(() => _UnauthorizedNotifier()),
],
),
);
await tester.pump();
await tester.pump(const Duration(seconds: 3));
await tester.pump();

expect(find.text('Could not load channels'), findsOneWidget);
expect(
find.text('Use the sign-in prompt below to reconnect.'),
findsOneWidget,
);
expect(find.text('Retry'), findsNothing);
},
);

testWidgets('bolds and clears unread channel labels', (tester) async {
final channels = [
Channel(
Expand Down Expand Up @@ -3966,6 +4029,12 @@ class _ErrorNotifier extends ChannelsNotifier {
Future<List<Channel>> build() => Future.error('Connection refused');
}

class _UnauthorizedNotifier extends ChannelsNotifier {
@override
Future<List<Channel>> build() =>
Future.error(RelayException(401, 'authentication required\n'));
}

class _LoadingNotifier extends ChannelsNotifier {
@override
Future<List<Channel>> build() => Completer<List<Channel>>().future;
Expand Down
Loading