From 6891b52a1d0774eed7d09a75a9c6df10a03d2982 Mon Sep 17 00:00:00 2001 From: Wes Date: Thu, 15 Oct 2026 18:12:22 -0700 Subject: [PATCH] fix(mobile): keep hosted setup on the existing welcome surface Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz> Signed-off-by: Wes --- .../onboarding/hosted_connect_page.dart | 21 ++++---- mobile/lib/hosted_app_gate.dart | 4 +- mobile/lib/onboarding_app_entry.dart | 11 +++- .../lib/shared/hosted/builderlab_account.dart | 5 ++ mobile/lib/shared/hosted/hosted_flow.dart | 6 ++- .../onboarding/hosted_communities_test.dart | 50 ++++++++--------- .../hosted/hosted_communities_flow_test.dart | 53 ++++++++----------- 7 files changed, 75 insertions(+), 75 deletions(-) diff --git a/mobile/lib/features/onboarding/hosted_connect_page.dart b/mobile/lib/features/onboarding/hosted_connect_page.dart index 4c914c6bc9..d2b0ab102d 100644 --- a/mobile/lib/features/onboarding/hosted_connect_page.dart +++ b/mobile/lib/features/onboarding/hosted_connect_page.dart @@ -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 { ), ), ), - ), + ], ), ); } diff --git a/mobile/lib/hosted_app_gate.dart b/mobile/lib/hosted_app_gate.dart index ed7e9c9764..cc1279cfa3 100644 --- a/mobile/lib/hosted_app_gate.dart +++ b/mobile/lib/hosted_app_gate.dart @@ -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, diff --git a/mobile/lib/onboarding_app_entry.dart b/mobile/lib/onboarding_app_entry.dart index 5cc028fa63..7f2dbc55cd 100644 --- a/mobile/lib/onboarding_app_entry.dart +++ b/mobile/lib/onboarding_app_entry.dart @@ -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…' diff --git a/mobile/lib/shared/hosted/builderlab_account.dart b/mobile/lib/shared/hosted/builderlab_account.dart index 7129c31f97..dfcb128d0f 100644 --- a/mobile/lib/shared/hosted/builderlab_account.dart +++ b/mobile/lib/shared/hosted/builderlab_account.dart @@ -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)) { diff --git a/mobile/lib/shared/hosted/hosted_flow.dart b/mobile/lib/shared/hosted/hosted_flow.dart index 68ee400ae3..fa2c92ddb0 100644 --- a/mobile/lib/shared/hosted/hosted_flow.dart +++ b/mobile/lib/shared/hosted/hosted_flow.dart @@ -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(); diff --git a/mobile/test/features/onboarding/hosted_communities_test.dart b/mobile/test/features/onboarding/hosted_communities_test.dart index 6f0b291f84..d87ac14914 100644 --- a/mobile/test/features/onboarding/hosted_communities_test.dart +++ b/mobile/test/features/onboarding/hosted_communities_test.dart @@ -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()); + 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); } }, ); diff --git a/mobile/test/shared/hosted/hosted_communities_flow_test.dart b/mobile/test/shared/hosted/hosted_communities_flow_test.dart index d0cade2dce..be739125a3 100644 --- a/mobile/test/shared/hosted/hosted_communities_flow_test.dart +++ b/mobile/test/shared/hosted/hosted_communities_flow_test.dart @@ -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();