Files
buzz/mobile/lib/shared/widgets/onboarding_step_page.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

224 lines
10 KiB
Dart

import 'dart:ui' show ImageFilter;
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/semantics.dart';
import '../theme/theme.dart';
import 'ios_navigation_bar.dart';
/// Opaque onboarding chrome shared by account setup and identity import.
/// A scrollable minimum-height body keeps actions at the bottom without
/// clipping content when the keyboard or larger accessibility text is shown.
class OnboardingStepPage extends StatelessWidget {
const OnboardingStepPage({
super.key,
required this.title,
this.subtitle,
this.onConnectionOptions,
this.foregroundProgress = 0,
this.keyboardBottomSpacing = Grid.sm,
required this.onBack,
required this.content,
this.actions = const [],
});
final String title;
final String? subtitle;
final VoidCallback? onConnectionOptions;
final double foregroundProgress;
final double keyboardBottomSpacing;
final VoidCallback? onBack;
final Widget content;
final List<Widget> actions;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final ios = defaultTargetPlatform == TargetPlatform.iOS;
final buttonStyle = FilledButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: theme.colorScheme.onPrimary,
minimumSize: const Size.fromHeight(52),
shape: const StadiumBorder(),
);
return Theme(
data: theme.copyWith(
filledButtonTheme: FilledButtonThemeData(style: buttonStyle),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: theme.colorScheme.onSurface,
minimumSize: const Size.fromHeight(52),
shape: const StadiumBorder(),
),
),
),
child: PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, _) {
if (!didPop) onBack?.call();
},
child: Scaffold(
backgroundColor: theme.colorScheme.surface,
body: SafeArea(
top: !ios,
child: ExcludeFocus(
excluding: foregroundProgress > 0,
child: ExcludeSemantics(
excluding: foregroundProgress > 0,
child: IgnorePointer(
ignoring: foregroundProgress > 0,
child: Opacity(
key: const Key('scanner-welcome-opacity'),
opacity: 1 - foregroundProgress,
child: Transform.scale(
key: const Key('scanner-welcome-scale'),
scale: MediaQuery.disableAnimationsOf(context)
? 1
: 1 - .04 * foregroundProgress,
child: ImageFiltered(
enabled:
!MediaQuery.disableAnimationsOf(context) &&
foregroundProgress > 0 &&
foregroundProgress < 1,
imageFilter: ImageFilter.blur(
sigmaX: 16 * foregroundProgress,
sigmaY: 16 * foregroundProgress,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (ios)
SizedBox(
height:
MediaQuery.paddingOf(context).top +
IosNavigationMetrics.of(
context,
).compactHeight,
child: IosNavigationBar(
title: '',
leading: onBack == null
? null
: IosNavigationAction(
label: 'Back',
symbol: 'chevron.backward',
onPressed: onBack,
),
),
)
else if (onBack != null)
Align(
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: Grid.xxs,
),
child: IconButton(
tooltip: 'Back',
onPressed: onBack,
icon: const BackButtonIcon(),
),
),
),
Expanded(
child: CustomScrollView(
keyboardDismissBehavior:
ScrollViewKeyboardDismissBehavior.onDrag,
slivers: [
SliverFillRemaining(
hasScrollBody: false,
child: Padding(
padding: EdgeInsets.fromLTRB(
Grid.sm,
Grid.sm,
Grid.sm,
MediaQuery.viewInsetsOf(
context,
).bottom >
0
? keyboardBottomSpacing
: Grid.sm,
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.stretch,
children: [
GestureDetector(
key: onConnectionOptions == null
? null
: const Key(
'developer-pairing-trigger',
),
excludeFromSemantics: true,
child: Semantics(
header: true,
namesRoute: true,
customSemanticsActions:
onConnectionOptions == null
? null
: {
const CustomSemanticsAction(
label:
'More connection options',
): onConnectionOptions!,
},
child: Text(
title,
style: OnboardingTypography
.title
.copyWith(
color: theme
.colorScheme
.onSurface,
),
),
),
),
if (subtitle != null) ...[
const SizedBox(height: Grid.xs),
Text(
subtitle!,
style: OnboardingTypography
.subtitle
.copyWith(
color: theme
.colorScheme
.onSurfaceVariant,
),
),
],
const SizedBox(height: Grid.xs),
content,
const Spacer(),
if (actions.isNotEmpty)
const SizedBox(height: Grid.sm),
for (
var i = 0;
i < actions.length;
i++
) ...[
if (i > 0)
const SizedBox(height: Grid.xxs),
actions[i],
],
],
),
),
),
],
),
),
],
),
),
),
),
),
),
),
),
),
),
);
}
}