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

185 lines
6.5 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/channels/deep_link_dispatcher.dart';
import 'features/profile/profile_setup_page.dart';
import 'features/onboarding/hosted_connect_page.dart';
import 'features/onboarding/self_hosted_entry_sheet.dart';
import 'shared/auth/auth.dart';
import 'shared/theme/grid.dart';
import 'shared/theme/theme_extensions.dart';
import 'shared/deeplink/deep_link.dart';
import 'shared/hosted/hosted_community_connection.dart';
import 'shared/hosted/hosted_flow.dart';
import 'shared/hosted/hosted_environment.dart';
import 'features/onboarding/hosted_reconnect_banner.dart';
import 'shared/hosted/hosted_flow_provider.dart';
/// Composition seam shared by signed-in and signed-out application states.
class HostedAppGate extends HookConsumerWidget {
const HostedAppGate({
super.key,
required this.child,
required this.authenticated,
});
final Widget child;
final bool authenticated;
@override
Widget build(BuildContext context, WidgetRef ref) {
final flow = HostedEnvironment.enabled
? ref.watch(hostedFlowProvider)
: null;
useListenable(flow);
useEffect(() {
if (flow == null) return null;
void onFlowChanged() {
if (flow.stage == HostedFlowStage.complete) {
ref.read(hostedInlineEntryProvider.notifier).setActive(false);
}
}
flow.addListener(onFlowChanged);
unawaited(flow.initialize());
return () => flow.removeListener(onFlowChanged);
}, [flow]);
final inlineEntry = ref.watch(hostedInlineEntryProvider);
final visible =
flow != null &&
flow.stage != HostedFlowStage.idle &&
flow.stage != HostedFlowStage.complete;
// Native browser authentication presents its own alert over the current
// screen. Keep that screen in place while checking the session/opening it.
final openingSignIn =
flow != null &&
(flow.stage == HostedFlowStage.signIn ||
(flow.accountOnly &&
flow.stage == HostedFlowStage.working &&
flow.account.account == null));
final fullScreenSetup = visible && !openingSignIn;
return DeepLinkDispatcher(
dispatchMessageLinks: authenticated && !visible,
onboardingHandler: (routeContext, link) async {
final relay = link.relayUrl;
if (relay != null) {
final host = Uri.parse(relay).host.toLowerCase();
final requiresAuth = HostedEnvironment.requiresAuth(host);
if (link is InviteDeepLink && !requiresAuth) return false;
final communities = await ref
.read(communityStorageProvider)
.loadAll();
final existing = communities
.where(
(item) => relayIdentity(item.relayUrl) == relayIdentity(relay),
)
.firstOrNull;
if (existing != null &&
!(existing.starterSetupIncomplete && requiresAuth)) {
await ref
.read(communityListProvider.notifier)
.switchCommunity(existing.id);
if (context.mounted) {
ref.read(communityEntryCompletionProvider.notifier).complete();
}
return true;
}
if (!requiresAuth && link is AddCommunityDeepLink) {
if (routeContext.mounted) {
final joined = await showSelfHostedEntry(
routeContext,
relay: link.relayUrl,
name: link.name,
fullScreen: inlineEntry,
);
if (joined && context.mounted) {
ref.read(communityEntryCompletionProvider.notifier).complete();
}
}
return true;
}
if (requiresAuth &&
!HostedEnvironment.supportsRelay(Uri.parse(relay))) {
if (routeContext.mounted) {
await showDialog<void>(
context: routeContext,
builder: (context) => AlertDialog(
title: const Text('Community setup unavailable'),
content: const Text(
'This community needs a different or unsupported Builderlab environment. Use an app build configured for this community.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
),
);
}
return true;
}
}
if (flow == null) return false;
await flow.start(link);
return true;
},
child: Stack(
fit: StackFit.expand,
children: [
ExcludeFocus(
excluding: fullScreenSetup,
child: ExcludeSemantics(
excluding: fullScreenSetup,
child: Offstage(offstage: fullScreenSetup, child: child),
),
),
if (fullScreenSetup) ColoredBox(color: context.colors.surface),
if (flow != null) ...[
if (authenticated && !visible) HostedReconnectBanner(flow: flow),
if (flow.stage == HostedFlowStage.signIn &&
authenticated &&
!inlineEntry)
Positioned(
left: Grid.sm,
right: Grid.sm,
bottom: 0,
child: SafeArea(
child: Material(
color: Colors.transparent,
child: Wrap(
alignment: WrapAlignment.center,
children: [
TextButton(
onPressed: flow.retry,
child: const Text('Start login again'),
),
TextButton(
onPressed: flow.leave,
child: Text(
flow.reconnecting
? 'Cancel sign-in'
: 'Leave setup',
),
),
],
),
),
),
),
if (fullScreenSetup)
HostedConnectPage(
flow: flow,
onClose: () => unawaited(flow.leave()),
profileBuilder: (_) => ProfileSetupPage(flow: flow),
),
],
],
),
);
}
}