From 12639d1e248b5046637025558448954ee509c1e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bot=20of=20Thomas=20G=C3=BCttler?= Date: Thu, 14 May 2026 11:57:08 +0200 Subject: [PATCH] feat: onboarding walkthrough for first-time users (U7) (#55) --- lib/ui/screens/account_list_screen.dart | 121 +++++++++++++++++++--- test/widget/account_list_screen_test.dart | 4 +- test/widget/add_account_screen_test.dart | 4 +- 3 files changed, 111 insertions(+), 18 deletions(-) diff --git a/lib/ui/screens/account_list_screen.dart b/lib/ui/screens/account_list_screen.dart index 692d1f9..9aaaf18 100644 --- a/lib/ui/screens/account_list_screen.dart +++ b/lib/ui/screens/account_list_screen.dart @@ -60,20 +60,7 @@ class AccountListScreen extends ConsumerWidget { } final accounts = snap.data!; if (accounts.isEmpty) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('No accounts yet.'), - const SizedBox(height: 12), - FilledButton.icon( - onPressed: () => context.push('/accounts/add'), - icon: const Icon(Icons.add), - label: const Text('Add account'), - ), - ], - ), - ); + return const _OnboardingView(); } return ListView.builder( itemCount: accounts.length, @@ -233,6 +220,112 @@ class _AccountTile extends ConsumerWidget { } } +class _OnboardingView extends StatelessWidget { + const _OnboardingView(); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.mail_outline, + size: 64, + color: theme.colorScheme.primary, + ), + const SizedBox(height: 16), + Text( + 'Welcome to SharedInbox', + style: theme.textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Get started in three steps:', + style: theme.textTheme.bodyMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + const _Step( + number: '1', + title: 'Add an account', + description: 'Connect your IMAP or JMAP email account.', + ), + const _Step( + number: '2', + title: 'Wait for sync', + description: + 'SharedInbox downloads your messages in the background.', + ), + const _Step( + number: '3', + title: 'Open your inbox', + description: + 'Tap the account to browse mailboxes and read emails.', + ), + const SizedBox(height: 32), + FilledButton.icon( + onPressed: () => context.push('/accounts/add'), + icon: const Icon(Icons.add), + label: const Text('Add account'), + ), + ], + ), + ), + ); + } +} + +class _Step extends StatelessWidget { + const _Step({ + required this.number, + required this.title, + required this.description, + }); + + final String number; + final String title; + final String description; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CircleAvatar( + radius: 16, + backgroundColor: theme.colorScheme.primaryContainer, + child: Text( + number, + style: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: theme.textTheme.titleSmall), + Text(description, style: theme.textTheme.bodySmall), + ], + ), + ), + ], + ), + ); + } +} + enum _AccountAction { syncLog, verifySync, edit, emailFilters, delete } /// Whether to surface the "Email filters" (Sieve) entry for [account]. diff --git a/test/widget/account_list_screen_test.dart b/test/widget/account_list_screen_test.dart index 9d311b6..3c19555 100644 --- a/test/widget/account_list_screen_test.dart +++ b/test/widget/account_list_screen_test.dart @@ -5,7 +5,7 @@ import 'helpers.dart'; void main() { group('AccountListScreen', () { - testWidgets('shows "No accounts yet." when repository is empty', ( + testWidgets('shows onboarding walkthrough when repository is empty', ( tester, ) async { await tester.pumpWidget( @@ -13,7 +13,7 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.text('No accounts yet.'), findsOneWidget); + expect(find.text('Welcome to SharedInbox'), findsOneWidget); expect(find.text('Add account'), findsOneWidget); }); diff --git a/test/widget/add_account_screen_test.dart b/test/widget/add_account_screen_test.dart index bde12bf..255626d 100644 --- a/test/widget/add_account_screen_test.dart +++ b/test/widget/add_account_screen_test.dart @@ -203,7 +203,7 @@ void main() { await tester.tap(find.text('Save')); await tester.pumpAndSettle(); - expect(find.text('No accounts yet.'), findsOneWidget); + expect(find.text('Welcome to SharedInbox'), findsOneWidget); }); testWidgets('JMAP connection failure shows error message', (tester) async { @@ -284,7 +284,7 @@ void main() { await tester.tap(find.text('Save')); await tester.pumpAndSettle(); - expect(find.text('No accounts yet.'), findsOneWidget); + expect(find.text('Welcome to SharedInbox'), findsOneWidget); }); testWidgets(