mirror of
https://github.com/block/buzz.git
synced 2026-11-12 20:31:12 +00:00
fix(mobile): stop asking for an API token when the channel list is refused (#9327)
## Summary - When the relay refused the channel-list request (HTTP 401), the mobile app said "Not authorized. Check your API token." The app has no API token, so the advice could not be followed. - The message now reads "This community couldn't authenticate the request. Try again." Retry stays: for org and hosted communities the refused token is dropped and Retry gets a fresh one, and a self-hosted relay that refuses the key has no other recovery on this screen. - One exception: when the organization sign-in has ended, the app sends reads unsigned and Retry can never succeed, while the Home banner already offers a working Sign in. In that state the channel list says "Use the sign-in prompt below to reconnect." and hides Retry. ## Details - `_ErrorView` (`mobile/lib/features/channels/channels_page/badges.dart`) only appears on a cold load with no saved channel list. It replaces the list, so Retry is the only control on that screen. - It reuses the shared `organizationSignInPromptVisible` predicate, the same one the Home banner and the community switcher use, so the error view and the banner agree on who owns the action. - The 403 "Access denied." message is unchanged. - Widget tests cover the new 401 copy with Retry, and the signed-out organization state with no Retry. ## Screenshots Android emulator, debug build, against a local test relay that refuses the channel-list read with HTTP 401 (no saved channel list, so the error view shows): <img src="https://github.com/user-attachments/assets/feebc324-a4cf-49ca-876a-1ae026bda753" alt="Channel list error: This community couldn't authenticate the request. Try again. With a Retry button" width="360"> 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Tom Brow <tomb@squareup.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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,20 @@ 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);
|
||||
// When the relay refuses the read while the Home banner is up, the banner
|
||||
// shows the working action. Other errors keep their message and Retry: a
|
||||
// relay that does not require the organization sign-in can still serve
|
||||
// the read.
|
||||
final config = ref.watch(relayConfigProvider);
|
||||
final signInOwned =
|
||||
error is RelayException &&
|
||||
(error as RelayException).statusCode == 401 &&
|
||||
organizationSignInPromptVisible(signIn, auth, config);
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(Grid.sm),
|
||||
@@ -65,7 +81,9 @@ class _ErrorView extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: Grid.xxs),
|
||||
Text(
|
||||
_userMessage(error),
|
||||
signInOwned
|
||||
? 'Use the prompt above to reconnect.'
|
||||
: _userMessage(error),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.colors.onSurfaceVariant,
|
||||
),
|
||||
@@ -73,12 +91,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'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -3547,6 +3547,79 @@ 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();
|
||||
});
|
||||
Future<void> pumpError(ChannelsNotifier Function() notifier) async {
|
||||
await tester.pumpWidget(const SizedBox());
|
||||
await tester.pumpWidget(
|
||||
buildTestable(
|
||||
overrides: [
|
||||
organizationSignInProvider.overrideWithValue(signIn),
|
||||
hostedRelayAuthProvider.overrideWithValue(auth),
|
||||
channelsProvider.overrideWith(notifier),
|
||||
],
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 3));
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
await pumpError(_UnauthorizedNotifier.new);
|
||||
expect(find.text('Could not load channels'), findsOneWidget);
|
||||
expect(find.text('Use the prompt above to reconnect.'), findsOneWidget);
|
||||
expect(find.text('Retry'), findsNothing);
|
||||
|
||||
// A relay that does not require the sign-in can still serve the read,
|
||||
// so an error other than the refusal keeps its message and Retry.
|
||||
await pumpError(_ErrorNotifier.new);
|
||||
expect(
|
||||
find.text('Something went wrong. Check your connection.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Retry'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('bolds and clears unread channel labels', (tester) async {
|
||||
final channels = [
|
||||
Channel(
|
||||
@@ -3966,6 +4039,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;
|
||||
|
||||
Reference in New Issue
Block a user