diff --git a/mobile/lib/features/channels/channels_page/badges.dart b/mobile/lib/features/channels/channels_page/badges.dart index c2641a433db..d47eb2cd8ca 100644 --- a/mobile/lib/features/channels/channels_page/badges.dart +++ b/mobile/lib/features/channels/channels_page/badges.dart @@ -27,7 +27,7 @@ class _EphemeralBadge extends StatelessWidget { } } -class _ErrorView extends StatelessWidget { +class _ErrorView extends HookConsumerWidget { final Object error; final VoidCallback onRetry; @@ -35,8 +35,11 @@ class _ErrorView extends StatelessWidget { 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.'; @@ -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), @@ -65,7 +79,9 @@ 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, ), @@ -73,12 +89,14 @@ class _ErrorView extends StatelessWidget { 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'), + ), + ], ], ), ), diff --git a/mobile/test/features/channels/channels_page_test.dart b/mobile/test/features/channels/channels_page_test.dart index 5c3a04e7f08..e01fa5e6901 100644 --- a/mobile/test/features/channels/channels_page_test.dart +++ b/mobile/test/features/channels/channels_page_test.dart @@ -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( @@ -3966,6 +4029,12 @@ class _ErrorNotifier extends ChannelsNotifier { Future> build() => Future.error('Connection refused'); } +class _UnauthorizedNotifier extends ChannelsNotifier { + @override + Future> build() => + Future.error(RelayException(401, 'authentication required\n')); +} + class _LoadingNotifier extends ChannelsNotifier { @override Future> build() => Completer>().future;