fix(mobile): keep hosted setup on the existing welcome surface

Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
This commit is contained in:
Wes
2026-10-15 18:12:22 -07:00
parent de44b43d8e
commit 6891b52a1d
7 changed files with 75 additions and 75 deletions

View File

@@ -55,17 +55,14 @@ class HostedConnectPage extends HookConsumerWidget {
onPopInvokedWithResult: (didPop, _) {
if (!didPop) close();
},
child: Scaffold(
appBar: AppBar(
title: const Text('Connect to Buzz'),
leading: BackButton(onPressed: close),
),
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(Grid.sm),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 440),
child: Stack(
children: [
ModalBarrier(color: Colors.black54, dismissible: false),
AlertDialog(
title: const Text('Connect to Builderlab'),
content: SizedBox(
width: 440,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -134,7 +131,7 @@ class HostedConnectPage extends HookConsumerWidget {
),
),
),
),
],
),
);
}

View File

@@ -185,7 +185,9 @@ class HostedAppGate extends HookConsumerWidget {
fit: StackFit.expand,
children: [
child,
if (flow.stage == HostedFlowStage.signIn)
if (flow.stage == HostedFlowStage.signIn &&
authenticated &&
!inlineEntry)
Positioned(
left: Grid.sm,
right: Grid.sm,

View File

@@ -27,6 +27,7 @@ class OnboardingAppEntry extends HookConsumerWidget {
unawaited(restore());
return null;
}, [flow]);
final signingIn = flow.stage == HostedFlowStage.signIn;
final busy =
flow.accountOnly &&
(flow.stage == HostedFlowStage.working ||
@@ -63,9 +64,15 @@ class OnboardingAppEntry extends HookConsumerWidget {
existingUser: existingUser.value,
enteringCommunityLink: enteringCommunityLink.value,
onCommunityLink: () => enteringCommunityLink.value = true,
onBuilderlab: busy ? null : () => unawaited(flow.signInAccount()),
onBuilderlab: signingIn
? () => unawaited(flow.leave())
: busy
? null
: () => unawaited(flow.signInAccount()),
accountStatus: status,
builderlabLabel: connected
builderlabLabel: signingIn
? 'Cancel sign-in'
: connected
? 'Continue with Builderlab'
: busy
? 'Signing in…'

View File

@@ -257,6 +257,11 @@ class BuilderlabAccountClient {
final response = await _http
.send(request)
.timeout(const Duration(seconds: 30));
if (kDebugMode && response.statusCode >= 400) {
debugPrint(
'Builderlab ${request.method} $path: HTTP ${response.statusCode}',
);
}
if (response.statusCode == 401) throw AccountFailure.signedOut;
if (response.statusCode == 403) throw AccountFailure.denied;
if (const {400, 404, 409, 422}.contains(response.statusCode)) {

View File

@@ -479,6 +479,10 @@ class HostedFlow extends ChangeNotifier {
if (!current()) return;
communities = listed;
selectedRelays = Set.unmodifiable(listed.map((item) => item.relayUrl));
if (listed.isEmpty) {
await leave();
return;
}
if (listed.length != 1) {
_set(HostedFlowStage.chooseCommunities);
return;
@@ -510,7 +514,7 @@ class HostedFlow extends ChangeNotifier {
communities = List.unmodifiable(
communities.where((item) => selectedRelays.contains(item.relayUrl)),
);
_set(HostedFlowStage.chooseCommunities);
await leave();
return;
}
await browser.clearIntent();

View File

@@ -6,8 +6,6 @@ import 'package:buzz/shared/hosted/hosted_flow.dart';
import 'package:buzz/shared/hosted/builderlab_account.dart';
import 'package:buzz/shared/hosted/hosted_community_connection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:buzz/shared/deeplink/deep_link.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -48,11 +46,10 @@ void main() {
expect(f.flow.stage, HostedFlowStage.signIn);
expect(f.browser.startedIntent, {'type': 'account'});
expect(find.byType(HostedConnectPage), findsNothing);
expect(find.text('Signing in…').hitTestable(), findsOneWidget);
expect(find.text('Cancel sign-in').hitTestable(), findsOneWidget);
expect(find.text('Enter a community link'), findsOneWidget);
expect(find.text('Start login again').hitTestable(), findsOneWidget);
f.expired = false;
await tester.tap(find.text('Leave setup').hitTestable());
await tester.tap(find.text('Cancel sign-in').hitTestable());
await tester.pumpAndSettle();
expect(f.flow.stage, HostedFlowStage.idle);
await tester.pumpAndSettle();
@@ -113,38 +110,33 @@ void main() {
PendingDeepLinkNotifier.debugUriStreamOverride = null;
await f.dispose();
});
await f.flow.signInAccount();
await tester.pumpWidget(
WidgetHelpers.testable(
disableAnimations: true,
child: HostedConnectPage(flow: f.flow, onClose: f.flow.leave),
child: const HostedAppGate(
authenticated: false,
child: OnboardingAppEntry(),
),
overrides: [hostedFlowProvider.overrideWithValue(f.flow)],
),
);
await tester.pumpAndSettle();
expect(find.text('Create a community in Builderlab'), findsOneWidget);
await tester.tap(find.text('Create a community in Builderlab'));
await tester.pumpAndSettle();
expect(f.opened?.path, '/buzz');
expect(find.text('Refresh communities'), findsOneWidget);
await tester.enterText(find.byType(TextField), 'not a valid url');
await tester.tap(find.text('Open community link'));
await tester.pumpAndSettle();
expect(
find.text('Enter a valid community address or invite link.'),
findsOneWidget,
);
await tester.enterText(
find.byType(TextField),
'https://one.communities.buzz.xyz/invite/code',
);
await tester.testTextInput.receiveAction(TextInputAction.go);
await tester.tap(find.text('Continue with Builderlab'));
await tester.pumpAndSettle();
expect(f.flow.stage, HostedFlowStage.idle);
final container = ProviderScope.containerOf(
tester.element(find.byType(HostedConnectPage)),
listen: false,
);
expect(container.read(pendingDeepLinkProvider), isA<InviteDeepLink>());
expect(find.byType(HostedConnectPage), findsNothing);
expect(find.text('Scan a QR code'), findsOneWidget);
expect(find.text('Use pairing code'), findsOneWidget);
f.rows = [f.row('denied')];
f.beforeAdmission = (_) async => throw AccountFailure.denied;
await tester.tap(find.text('Continue with Builderlab'));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsOneWidget);
expect(find.text('Scan a QR code'), findsOneWidget);
await tester.tap(find.text('Leave setup').hitTestable());
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsNothing);
expect(find.text('Scan a QR code'), findsOneWidget);
}
},
);

View File

@@ -154,19 +154,15 @@ void main() {
expect(f.flow.selectedRelays, isEmpty);
});
test(
'zero communities can open creation and refresh into single entry',
() async {
final f = await fixture();
await f.flow.signInAccount();
expect(f.flow.stage, HostedFlowStage.chooseCommunities);
await f.flow.openCommunityCreation();
expect(f.opened, Uri.parse('https://app.builderlab.xyz/buzz'));
f.rows = [f.row('created')];
await f.flow.refreshCommunities();
expect(f.admitted, ['created']);
},
);
test('zero communities return to welcome and allow reconnect', () async {
final f = await fixture();
await f.flow.signInAccount();
expect(f.flow.stage, HostedFlowStage.idle);
expect(f.account.account, isNotNull);
f.rows = [f.row('created')];
await f.flow.signInAccount();
expect(f.admitted, ['created']);
});
test('identity mismatch blocks list and preserves saved key', () async {
final f = await fixture();
@@ -283,23 +279,20 @@ void main() {
},
);
test(
'single conflicting community can be skipped then creation refreshed',
() async {
final f = await fixture();
f.rows = [f.row('conflict')];
f.beforeAdmission = (_) async =>
throw const SavedCommunityIdentityConflict();
await f.flow.signInAccount();
await f.flow.skipConflictingCommunity();
expect(f.flow.stage, HostedFlowStage.chooseCommunities);
expect(f.flow.communities, isEmpty);
f.rows = [f.row('created')];
f.beforeAdmission = null;
await f.flow.refreshCommunities();
expect(f.admitted, ['created']);
},
);
test('single conflicting community can be skipped back to welcome', () async {
final f = await fixture();
f.rows = [f.row('conflict')];
f.beforeAdmission = (_) async =>
throw const SavedCommunityIdentityConflict();
await f.flow.signInAccount();
await f.flow.skipConflictingCommunity();
expect(f.flow.stage, HostedFlowStage.idle);
expect(f.flow.communities, isEmpty);
f.rows = [f.row('created')];
f.beforeAdmission = null;
await f.flow.signInAccount();
expect(f.admitted, ['created']);
});
test('unsupported rows are skipped and wss prefix canonicalized', () async {
final f = await fixture();