Files
buzz/mobile/lib/onboarding_app_entry.dart
klopez4212 9c3ba83adb Simplify signed-in mobile community onboarding (#8852)
Replace the signed-in mobile welcome screen with **Add a community**: QR
scan first, then grey secondary actions for community links, finding
saved communities, and creation. Hide **Find my communities** when none
are known; refresh on entry/resume and every 30 seconds while active,
retaining results on transient failure.

Community switcher → Add keeps its two-action flow and uses the same
grey secondary link button.

Validation: Flutter analysis, full mobile suite (4,014 tests),
Builderlab-enabled full suite (4,036 tests), widget interactions, and
independent agent review. Draft pending native sign-in and human
testing. Snapshots render the production Flutter widgets; native
navigation/status bars are omitted.

| Communities available | No communities | Switcher → Add |
|---|---|---|
| ![Onboarding with
communities](https://github.com/user-attachments/assets/444b05c5-711f-41df-9c50-293ad9e13767)
| ![Onboarding without
communities](https://github.com/user-attachments/assets/db78a490-bd8e-4d78-97b4-a391b52290d8)
| ![Switcher Add with grey secondary
button](https://github.com/user-attachments/assets/de9e42d2-dd53-462b-98c7-da87223e5dd4)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-31 07:46:44 -07:00

206 lines
6.7 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'features/pairing/pairing_page.dart';
import 'features/pairing/pairing_provider.dart';
import 'shared/hosted/hosted_flow_provider.dart';
import 'shared/hosted/hosted_flow.dart';
import 'shared/hosted/hosted_communities.dart';
import 'shared/hosted/use_known_communities.dart';
import 'shared/hosted/hosted_environment.dart';
import 'shared/hosted/hosted_community_creation.dart';
import 'shared/hosted/hosted_flow_problem_message.dart';
/// Navigation over the existing branded welcome surface.
class OnboardingAppEntry extends HookConsumerWidget {
const OnboardingAppEntry({
super.key,
this.addingCommunity = false,
this.restoreAccount = true,
this.onExit,
});
final bool addingCommunity;
final bool restoreAccount;
final VoidCallback? onExit;
@override
Widget build(BuildContext context, WidgetRef ref) {
final existingUser = useState(false);
final creating = useState(false);
final enteringCommunityLink = useState(false);
final flow = HostedEnvironment.enabled
? ref.watch(hostedFlowProvider)
: null;
useListenable(flow);
useListenable(flow?.account.subjectChanges);
useEffect(() {
if (flow == null || !restoreAccount) return null;
Future<void> restore() async {
await flow.initialize();
if (context.mounted) await flow.signInAccount(openBrowser: false);
}
unawaited(restore());
return null;
}, [flow]);
void finishEntry() {
if (!addingCommunity) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!context.mounted) return;
final route = ModalRoute.of(context);
if (route != null && route.isActive) {
route.navigator?.removeRoute(route);
}
});
}
ref.listen(communityEntryCompletionProvider, (_, _) => finishEntry());
useEffect(() {
final presentation = ref.read(hostedInlineEntryProvider.notifier);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.mounted) presentation.setActive(true);
});
void completed() {
if (flow?.stage == HostedFlowStage.complete) finishEntry();
}
flow?.addListener(completed);
return () {
flow?.removeListener(completed);
scheduleMicrotask(() {
presentation.setActive(false);
if (addingCommunity &&
onExit == null &&
flow != null &&
flow.stage != HostedFlowStage.complete) {
unawaited(flow.leave());
}
});
};
}, [flow]);
final signingIn = flow?.stage == HostedFlowStage.signIn;
final busy =
flow != null &&
flow.accountOnly &&
(flow.stage == HostedFlowStage.working ||
flow.stage == HostedFlowStage.signIn);
final connected = flow?.account.account != null;
final hasKnownCommunities = useKnownCommunities(
accountKey: flow?.account.account?.subject,
active:
!addingCommunity &&
!existingUser.value &&
!enteringCommunityLink.value &&
!creating.value &&
flow?.stage == HostedFlowStage.idle &&
(ModalRoute.of(context)?.isCurrent ?? true),
load: () async =>
(await loadKnownHostedCommunities(flow!.account)).isNotEmpty,
);
final problem = flow != null && flow.accountOnly ? flow.problem : null;
final status = flow == null
? null
: problem != null
? hostedFlowProblemMessage(problem)
: busy
? null
: flow.noCommunitiesToAdd
? 'No communities available to add. Scan a QR code or use an invite link.'
: connected
? 'Scan a QR code or enter a community link.'
: null;
final handlingBack = useRef(false);
void back() {
// Nested page and entry PopScopes receive the same system Back event.
if (handlingBack.value) return;
handlingBack.value = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
handlingBack.value = false;
});
if (!existingUser.value &&
!enteringCommunityLink.value &&
!creating.value &&
(onExit != null || addingCommunity)) {
if (onExit != null) {
onExit!();
} else {
Navigator.of(context).pop();
}
return;
}
unawaited(flow?.leave());
existingUser.value = false;
creating.value = false;
enteringCommunityLink.value = false;
FocusManager.instance.primaryFocus?.unfocus();
ref.read(pairingProvider.notifier).reset();
}
return PopScope(
canPop:
onExit == null &&
!existingUser.value &&
!enteringCommunityLink.value &&
!creating.value,
onPopInvokedWithResult: (didPop, _) {
if (!didPop &&
onExit != null &&
!existingUser.value &&
!enteringCommunityLink.value &&
!creating.value) {
back();
return;
}
if (!didPop &&
(existingUser.value ||
creating.value ||
enteringCommunityLink.value)) {
back();
}
},
child: PairingPage(
connectionLanding: true,
showConnectionOptions:
!HostedEnvironment.enabled || addingCommunity || connected,
addingCommunity: addingCommunity,
signedInOnboarding: connected && !addingCommunity && !signingIn,
hasKnownCommunities: hasKnownCommunities,
existingUser: existingUser.value,
enteringCommunityLink: enteringCommunityLink.value,
creatingCommunity: creating.value,
onSubmitCommunity: flow == null
? null
: (name) {
creating.value = false;
unawaited(flow.start(HostedCreateCommunity(name)));
},
onCommunityLink: () => enteringCommunityLink.value = true,
onCreateCommunity: flow == null || busy
? null
: () => creating.value = true,
onBuilderlab: flow == null
? null
: signingIn
? () => unawaited(flow.leave())
: busy
? null
: () => unawaited(flow.signInAccount()),
accountStatus: status,
builderlabBusy: busy,
signInPending: signingIn || busy,
builderlabLabel: signingIn
? 'Cancel sign-in'
: busy
? 'Connecting…'
: connected
? 'Continue with Builderlab'
: 'Sign in with email',
onWelcomeBack: back,
onAlreadyUseBuzz: () => existingUser.value = true,
),
);
}
}