Files
buzz/mobile/lib/onboarding_app_entry.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

189 lines
6.0 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_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);
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 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
? 'Connected to Builderlab.'
: 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,
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,
),
);
}
}