Files
buzz/mobile/lib/onboarding_app_entry.dart
klopez4212 f3a7bab098 Clarify mobile community onboarding and saved identity selection (#8729)
## Summary

Make community onboarding easier to scan: shorten hosted hostname
fallbacks, clarify Add Community instructions, and replace raw saved-key
buttons with identity cards. Use white pages, grey cards, ghost
secondary actions, standard page transitions, and a “Getting ready”
screen during joining. Native profile setup keeps a horizontal modal
transition. Identity cards load signed name/avatar metadata from saved
communities without switching accounts; unavailable profiles use a
neutral fallback. Cards show only the avatar, a 16px medium-weight name,
and muted npub; community lists are omitted. Retiring profile setup
closes its avatar editor too.

### Related issue

Follow-up to #8687 and #8599; no duplicate PR found.

### Testing

- Formatting and analysis passed. Full suite with hosted-auth defines:
4,019 passed, 5 skipped; includes profile-fetch and cancellation
regressions.
- Prior iOS device preview and native profile tests passed; hosted CI
native Swift checks passed on the preceding head. Latest Dart fixes are
not yet reinstalled on the phone.
- Simulator walkthrough and owner-tested iPhone preview use sample
identities and simulated admission.

Sample-data previews; the identity snapshot shows the
unavailable-profile fallback rendered by Flutter. Media is attached here
only.

| Community picker | Add Community | Saved identity |
| --- | --- | --- |
| ![Shortened community
name](https://github.com/user-attachments/assets/cf0af6aa-3a3d-4ffe-b469-e26f9bb92c30)
| ![White Add Community
page](https://github.com/user-attachments/assets/b2f71c77-1aa0-465a-b302-7eb5f1e92819)
| ![Saved identity card and ghost import
action](https://github.com/user-attachments/assets/47f20f34-f047-4ce5-befa-f56834096e95)
|

---------

Signed-off-by: kenny lopez <klopez4212@gmail.com>
Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz>
2026-10-30 19:00:42 +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
? '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,
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,
),
);
}
}