feat: guided setup — clickable next steps, signature relaxation, fresh-install auto-open
Post-apply the wizard now renders real Studio actions instead of CLI
text: a start-hub button that polls until the daemon answers,
per-module install buttons (capability-name install via the hub's
store index) with done/progress states, and an open-the-starter-flow
button that navigates to the Flows page. Regulated plans explain in
plain language that modules come from a signed source; the preview
offers 'allow installing from the public store' as one deliberate,
reversible switch that re-assembles the plan (allow_unsigned_modules).
Fresh installs (no config, no setup-plan.yaml) auto-open the wizard
once per run — the wizard IS the onboarding — and it steps back once
a setup exists. The welcome CTA is framed honestly ('get started in
3 questions'), and after the wizard closes the onboarding checklist
remounts, re-probes, and says what the assistant already covered
(profile line from setup-plan.yaml) instead of acting as a second,
disconnected onboarding surface.
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
140408d26a
commit
1f1e050b42
8 changed files with 704 additions and 51 deletions
|
|
@ -22,9 +22,44 @@ import '../theme/tokens.dart';
|
|||
import '../widgets/guided_setup_dialog.dart';
|
||||
import '../widgets/widgets.dart';
|
||||
|
||||
class WelcomePage extends StatelessWidget {
|
||||
class WelcomePage extends StatefulWidget {
|
||||
const WelcomePage({super.key});
|
||||
|
||||
@override
|
||||
State<WelcomePage> createState() => _WelcomePageState();
|
||||
}
|
||||
|
||||
class _WelcomePageState extends State<WelcomePage> {
|
||||
/// Bumped when the guided-setup dialog closes so the onboarding
|
||||
/// checklist remounts and re-probes — the assistant may have just
|
||||
/// installed the modules the checklist looks for.
|
||||
int _checklistEpoch = 0;
|
||||
|
||||
/// One auto-open per app run: a fresh install that dismisses the
|
||||
/// wizard should not have it spring back on every rebuild.
|
||||
static bool _autoOpenedThisRun = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _maybeAutoOpen());
|
||||
}
|
||||
|
||||
/// On a fresh hub (no config, no recorded setup plan) the wizard
|
||||
/// IS the onboarding — open it rather than hoping the operator
|
||||
/// finds the button. Once a setup exists it steps back for good.
|
||||
Future<void> _maybeAutoOpen() async {
|
||||
if (_autoOpenedThisRun || !mounted) return;
|
||||
if (!GuidedSetupDialog.isFreshInstall()) return;
|
||||
_autoOpenedThisRun = true;
|
||||
await _openSetup();
|
||||
}
|
||||
|
||||
Future<void> _openSetup() async {
|
||||
await GuidedSetupDialog.show(context);
|
||||
if (mounted) setState(() => _checklistEpoch++);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
|
@ -65,11 +100,12 @@ class WelcomePage extends StatelessWidget {
|
|||
alignment: Alignment.centerLeft,
|
||||
// High-emphasis: the guided setup is the fastest path
|
||||
// to a working first run, so lead with a filled button
|
||||
// rather than a low-key outlined one.
|
||||
// rather than a low-key outlined one. Honest framing:
|
||||
// "get started in 3 questions", not tool-speak.
|
||||
child: FilledButton.icon(
|
||||
onPressed: () => GuidedSetupDialog.show(context),
|
||||
onPressed: _openSetup,
|
||||
icon: const Icon(Icons.auto_fix_high),
|
||||
label: Text(AppLocalizations.of(context)!.guidedSetupTitle),
|
||||
label: Text(l.setupStartCta),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: ChainSpace.xxl),
|
||||
|
|
@ -81,7 +117,7 @@ class WelcomePage extends StatelessWidget {
|
|||
const _HubDownHero(),
|
||||
const SizedBox(height: ChainSpace.xxl),
|
||||
] else
|
||||
const _OnboardingChecklist(),
|
||||
_OnboardingChecklist(key: ValueKey(_checklistEpoch)),
|
||||
const _PillarRow(),
|
||||
const SizedBox(height: ChainSpace.xxl),
|
||||
const _SectionLabel(textKey: _SectionLabelKey.trust),
|
||||
|
|
@ -515,7 +551,7 @@ const String _kChecklistDismissedKey = 'welcome.checklist.dismissed';
|
|||
/// parallel; failures stay false and the checklist still
|
||||
/// renders (with the "Refresh" button for retry).
|
||||
class _OnboardingChecklist extends StatefulWidget {
|
||||
const _OnboardingChecklist();
|
||||
const _OnboardingChecklist({super.key});
|
||||
|
||||
@override
|
||||
State<_OnboardingChecklist> createState() => _OnboardingChecklistState();
|
||||
|
|
@ -530,6 +566,12 @@ class _OnboardingChecklistState extends State<_OnboardingChecklist> {
|
|||
bool _moduleOk = false;
|
||||
bool _flowOk = false;
|
||||
|
||||
/// Profile the guided setup applied (from `setup-plan.yaml`), or
|
||||
/// null when the assistant never ran. Connects the two onboarding
|
||||
/// surfaces: the checklist says what the assistant already covered
|
||||
/// instead of ignoring it.
|
||||
String? _setupProfile;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -541,6 +583,7 @@ class _OnboardingChecklistState extends State<_OnboardingChecklist> {
|
|||
if (!mounted) return;
|
||||
setState(() {
|
||||
_dismissed = prefs.getBool(_kChecklistDismissedKey) ?? false;
|
||||
_setupProfile = GuidedSetupDialog.appliedSetupProfile();
|
||||
_initialised = true;
|
||||
});
|
||||
if (!_dismissed) {
|
||||
|
|
@ -598,6 +641,13 @@ class _OnboardingChecklistState extends State<_OnboardingChecklist> {
|
|||
setState(() => _dismissed = true);
|
||||
}
|
||||
|
||||
/// Localized plain-language label for the applied setup profile.
|
||||
String _profileLabel(AppLocalizations l) => switch (_setupProfile) {
|
||||
'enterprise' => l.setupProfileEnterprise,
|
||||
'air-gapped' => l.setupProfileAirgapped,
|
||||
_ => l.setupProfileDev,
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_initialised || _dismissed) return const SizedBox.shrink();
|
||||
|
|
@ -674,6 +724,27 @@ class _OnboardingChecklistState extends State<_OnboardingChecklist> {
|
|||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
// The guided setup already ran: say so, so the checklist
|
||||
// reads as "what's left", not as a second, disconnected
|
||||
// onboarding that ignores the assistant's work.
|
||||
if (_setupProfile != null) ...[
|
||||
const SizedBox(height: ChainSpace.sm),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.check_circle, size: 16, color: ChainColors.success),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l.welcomeChecklistSetupDone(_profileLabel(l)),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: ChainSpace.md),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue