mirror of
https://github.com/block/buzz.git
synced 2026-10-31 06:21:34 +00:00
### What changed? After a new mobile invite claim succeeds, Buzz now best-effort ensures membership in the same public starter channels as desktop: - `#general` - `#welcome-everyone` Missing starters use desktop's deterministic per-relay IDs and exact public channel configuration, so concurrent mobile and desktop setup converges safely. Setup failures do not invalidate or retry an already successful invite claim, and failure for one starter does not block the other. The success sheet offers **Continue to #welcome-everyone** when that channel is available. Mobile does not create the private `Welcome` channel because it cannot provision the desktop Welcome agents that make that channel useful. This PR is stacked on #6145 because it deliberately reuses that PR's open-channel directory and join behavior. Once #6145 merges, this PR can be retargeted to `main` without changing its BUZZ-12 diff. Fixes [BUZZ-12](https://linear.app/squareup/issue/BUZZ-12/bug-community-appears-empty-after-using-invite-link-on-mobile). ### How is it tested? - Desktop/mobile deterministic starter-ID parity coverage. - Existing-channel join and missing-channel creation coverage. - Duplicate-create convergence and per-channel failure isolation coverage. - Invite success remains successful when starter setup fails. - Widget coverage for continuing directly into `#welcome-everyone`. - Focused invite/deep-link tests: 23 passed. - `just mobile-check`: passed. - `just mobile-test`: 1,483 passed. - Pre-push repository checks: passed. --------- Signed-off-by: Tom Brow <tomb@block.xyz> Co-authored-by: Codex <noreply@openai.com>
120 lines
3.3 KiB
Dart
120 lines
3.3 KiB
Dart
import 'package:buzz/features/invites/invite_join_provider.dart';
|
|
import 'package:buzz/features/invites/invite_join_sheet.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import '../../helpers/widget_helpers.dart';
|
|
|
|
void main() {
|
|
testWidgets('recovery error is scrollable and exposes retry setup', (
|
|
tester,
|
|
) async {
|
|
tester.view.physicalSize = const Size(375, 400);
|
|
tester.view.devicePixelRatio = 1;
|
|
addTearDown(tester.view.reset);
|
|
|
|
await tester.pumpWidget(
|
|
WidgetHelpers.testable(
|
|
child: const InviteJoinSheet(),
|
|
overrides: [
|
|
inviteJoinProvider.overrideWith(_RecoveryErrorInviteJoinNotifier.new),
|
|
],
|
|
),
|
|
);
|
|
await tester.pump();
|
|
|
|
expect(find.text('Finish setting up'), findsOneWidget);
|
|
expect(find.widgetWithText(FilledButton, 'Retry setup'), findsOneWidget);
|
|
expect(find.byType(SingleChildScrollView), findsOneWidget);
|
|
expect(tester.takeException(), isNull);
|
|
});
|
|
|
|
for (final fixture in [
|
|
(
|
|
name: 'membership claim',
|
|
state: const InviteJoinState(
|
|
status: InviteJoinStatus.claiming,
|
|
host: 'relay.example.com',
|
|
),
|
|
label: 'Joining…',
|
|
),
|
|
(
|
|
name: 'starter recovery',
|
|
state: const InviteJoinState(
|
|
status: InviteJoinStatus.claiming,
|
|
host: 'relay.example.com',
|
|
isStarterSetupRecovery: true,
|
|
),
|
|
label: 'Finishing setup…',
|
|
),
|
|
]) {
|
|
testWidgets('${fixture.name} cannot dismiss the in-flight invite sheet', (
|
|
tester,
|
|
) async {
|
|
await tester.pumpWidget(
|
|
WidgetHelpers.testable(
|
|
child: const _InviteJoinSheetLauncher(),
|
|
overrides: [
|
|
inviteJoinProvider.overrideWith(
|
|
() => _StaticInviteJoinNotifier(fixture.state),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
await tester.tap(find.text('Open invite'));
|
|
await tester.pump();
|
|
await tester.pump(const Duration(milliseconds: 300));
|
|
expect(find.text(fixture.label), findsOneWidget);
|
|
expect(
|
|
find.byKey(const ValueKey('buzz-sheet-drag-handle')),
|
|
findsNothing,
|
|
);
|
|
expect(find.byTooltip('Close sheet'), findsNothing);
|
|
|
|
await tester.tapAt(const Offset(8, 8));
|
|
await tester.pump();
|
|
expect(find.text(fixture.label), findsOneWidget);
|
|
|
|
await tester.binding.handlePopRoute();
|
|
await tester.pump();
|
|
expect(find.text(fixture.label), findsOneWidget);
|
|
});
|
|
}
|
|
}
|
|
|
|
class _InviteJoinSheetLauncher extends StatelessWidget {
|
|
const _InviteJoinSheetLauncher();
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Scaffold(
|
|
body: Center(
|
|
child: FilledButton(
|
|
onPressed: () => showInviteJoinSheet(context),
|
|
child: const Text('Open invite'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
class _StaticInviteJoinNotifier extends InviteJoinNotifier {
|
|
_StaticInviteJoinNotifier(this._state);
|
|
|
|
final InviteJoinState _state;
|
|
|
|
@override
|
|
InviteJoinState build() => _state;
|
|
}
|
|
|
|
class _RecoveryErrorInviteJoinNotifier extends InviteJoinNotifier {
|
|
@override
|
|
InviteJoinState build() => const InviteJoinState(
|
|
status: InviteJoinStatus.error,
|
|
host: 'relay.example.com',
|
|
communityName: 'Example',
|
|
errorMessage:
|
|
'Starter setup could not reach the relay. Retry when the connection is available.',
|
|
isStarterSetupRecovery: true,
|
|
);
|
|
}
|