Files
buzz/mobile/test/shared/hosted/hosted_communities_flow_test.dart
klopez4212 e922dc08ff Unify mobile onboarding and community entry screens (#8687)
## Summary

Use consistent fullscreen screens for identity setup, community
selection, joining, and Add Community. Restore all memberships and open
the selected community; share the community grid with the switcher.

Add QR-first choices, animated link entry with 12px keyboard clearance,
a two-finger double-tap pairing toggle, and the centered reversing bee
loader. Preserve cancellation, Back, and reduced-motion behavior.

Add intentionally omits community creation and Builderlab sign-in: this
route follows sign-in and the existing-community picker.

### Related issue

Related: #8628 (native profile setup). No duplicate fullscreen
onboarding PR found.

### Testing

- Full standard and hosted-authentication mobile suites, static
analysis, and required push checks passed.
- iPhone preview iterated with Kenny; final simulator walkthrough below
uses synthetic data and does not authenticate or save account changes.
- Independent review completed; regressions cover cancellation during
loading, Add recovery after failed/conflicting memberships, retrying
skipped communities, and explicit picker stages.

Walkthrough recorded before the copy and recovery followups:


https://github.com/user-attachments/assets/bfea3ad3-1dde-40c4-8748-44c84226c292

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-29 16:12:31 +00:00

129 lines
4.3 KiB
Dart

import 'dart:async';
import 'package:buzz/shared/hosted/builderlab_account.dart';
import 'package:buzz/shared/hosted/browser_auth.dart';
import 'package:buzz/shared/hosted/account_identity_store.dart';
import 'package:buzz/shared/hosted/hosted_flow.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../helpers/hosted_communities_fixture.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUp(() => FlutterSecureStorage.setMockInitialValues({}));
Future<HostedCommunitiesFixture> fixture({bool backup = true}) async {
final result = HostedCommunitiesFixture();
await result.initialize(backup: backup);
addTearDown(result.dispose);
return result;
}
for (final failure in ['binding', 'backup']) {
test(
'$failure proof failure blocks discovery and preserves saved identity',
() async {
final f = await fixture(backup: failure != 'backup');
if (failure == 'binding') f.bound = '2' * 64;
final before = await const FlutterSecureStorage().readAll();
await f.flow.signInAccount();
expect(f.flow.stage, HostedFlowStage.blocked);
if (failure == 'binding') {
expect(f.flow.problem, HostedFlowProblem.identityMismatch);
}
expect(
f.requests.any((path) => path.endsWith('/known-communities/list')),
isFalse,
);
expect(await const FlutterSecureStorage().readAll(), before);
expect(f.admitted, isEmpty);
},
);
}
test(
'selecting a skipped relay retries it as the landing community',
() async {
final f = await fixture();
f.rows = [f.row('one'), f.row('two'), f.row('three')];
f.beforeAdmission = (name) async {
if (name != 'one.communities.buzz.xyz') {
throw AccountFailure.unavailable;
}
};
await f.flow.signInAccount();
await f.flow.addSelectedCommunities();
await f.flow.skipFailedCommunity();
expect(f.flow.stage, HostedFlowStage.blocked);
f.flow.backToCommunityChoices();
f.flow.chooseLandingCommunity('wss://two.communities.buzz.xyz');
f.beforeAdmission = null;
await f.flow.addSelectedCommunities();
expect(f.admitted, [
'one.communities.buzz.xyz',
'two.communities.buzz.xyz',
'three.communities.buzz.xyz',
]);
expect(f.openedCommunity, 'wss://two.communities.buzz.xyz');
},
);
test('malformed list targets cannot authorize admission', () async {
final f = await fixture();
f.rows = [
{...f.row('one'), 'community_url': 42},
];
await f.flow.signInAccount();
expect(f.flow.problem, HostedFlowProblem.invalidResponse);
expect(f.admitted, isEmpty);
});
test('expired A batch is discarded after browser login as B', () async {
final f = await fixture();
await f.flow.initialize();
f.rows = [f.row('a-one'), f.row('a-two')];
f.beforeAdmission = (name) async {
if (name == 'a-two.communities.buzz.xyz') {
throw AccountFailure.unavailable;
}
};
await f.flow.signInAccount();
expect(f.flow.stage, HostedFlowStage.chooseCommunities);
expect(f.admitted, isEmpty);
await f.flow.addSelectedCommunities();
expect(f.admitted, ['a-one.communities.buzz.xyz']);
f.expired = true;
await f.flow.retry();
expect(f.flow.stage, HostedFlowStage.signIn);
expect(f.browser.startedIntent, {'type': 'account'});
f.expired = false;
f.subject = 'other-owner';
f.rows = [f.row('b-one')];
await f.store.create(f.keys.nsec);
await f.store.confirmBackup(
BackupReceipt(
backend: BuilderlabAccountClient.origin.toString(),
subject: f.subject,
pubkey: f.keys.public,
backupId: 'other-backup',
),
);
final completed = Completer<void>();
f.flow.addListener(() {
if (f.flow.stage == HostedFlowStage.chooseCommunities &&
!completed.isCompleted) {
completed.complete();
}
});
f.browser.callbacks.add(
BrowserAuthCode('other-code', 'a' * 43, {'type': 'account'}),
);
await completed.future.timeout(const Duration(seconds: 2));
await f.flow.addSelectedCommunities();
expect(f.admitted, [
'a-one.communities.buzz.xyz',
'b-one.communities.buzz.xyz',
]);
});
}