feat: add-account wizard, edit account, inbox tap, connection status

- Add account wizard: email-first flow with JMAP/IMAP auto-detection
  via well-known URLs; falls back to manual type selection
- Fix JMAP connection probe: GET session URL with Basic auth instead
  of the API endpoint, so 401 reliably signals bad credentials
- Account list tile: tap → open INBOX directly; popup menu for
  all mailboxes / edit / delete (with confirmation dialog)
- Show account type (JMAP/IMAP) and async connection status per tile:
  spinner while checking, green check on success, red error on failure
- Add EditAccountScreen: edit name, password, server settings; runs
  connection test only when password is changed
- Fix GTK window initialisation order so app starts with correct size
- Fix 42 lint issues (avoid_redundant_argument_values,
  unnecessary_non_null_assertion, unawaited_futures)
- 147 tests, 87% coverage, task check green

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Güttler
2026-04-18 15:13:47 +02:00
co-authored by Claude Sonnet 4.6
parent e2a87fc2b0
commit 442c3c4087
26 changed files with 1452 additions and 283 deletions
+62 -4
View File
@@ -9,14 +9,19 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:sharedinbox/core/models/account.dart';
import 'package:sharedinbox/core/models/discovery_result.dart';
import 'package:sharedinbox/core/models/email.dart';
import 'package:sharedinbox/core/models/mailbox.dart';
import 'package:sharedinbox/core/repositories/account_repository.dart';
import 'package:sharedinbox/core/repositories/email_repository.dart';
import 'package:sharedinbox/core/repositories/mailbox_repository.dart';
import 'package:sharedinbox/core/services/account_discovery_service.dart';
import 'package:sharedinbox/core/services/connection_test_service.dart';
import 'package:sharedinbox/di.dart';
import 'package:sharedinbox/ui/screens/account_list_screen.dart';
import 'package:sharedinbox/ui/screens/add_account_screen.dart';
import 'package:sharedinbox/ui/screens/compose_screen.dart';
import 'package:sharedinbox/ui/screens/edit_account_screen.dart';
import 'package:sharedinbox/ui/screens/email_detail_screen.dart';
import 'package:sharedinbox/ui/screens/email_list_screen.dart';
import 'package:sharedinbox/ui/screens/mailbox_list_screen.dart';
@@ -47,6 +52,12 @@ class FakeAccountRepository implements AccountRepository {
Future<void> addAccount(Account account, String password) async =>
_accounts.add(account);
@override
Future<void> updateAccount(Account account, {String? password}) async {
final idx = _accounts.indexWhere((a) => a.id == account.id);
if (idx >= 0) _accounts[idx] = account;
}
@override
Future<void> removeAccount(String id) async =>
_accounts.removeWhere((a) => a.id == id);
@@ -121,6 +132,28 @@ class FakeEmailRepository implements EmailRepository {
_searchResults;
}
// ---------------------------------------------------------------------------
// Fake services
// ---------------------------------------------------------------------------
class FakeDiscoveryService implements AccountDiscoveryService {
FakeDiscoveryService(this._result);
final DiscoveryResult _result;
@override
Future<DiscoveryResult> discover(String email) async => _result;
}
class FakeConnectionTestService implements ConnectionTestService {
FakeConnectionTestService({Exception? error}) : _error = error;
final Exception? _error;
@override
Future<void> testConnection(Account account, String password) async {
if (_error != null) throw _error;
}
}
// ---------------------------------------------------------------------------
// App builder
// ---------------------------------------------------------------------------
@@ -144,6 +177,12 @@ Widget buildApp({
path: 'add',
builder: (ctx, state) => const AddAccountScreen(),
),
GoRoute(
path: ':accountId/edit',
builder: (ctx, state) => EditAccountScreen(
accountId: state.pathParameters['accountId']!,
),
),
GoRoute(
path: ':accountId/mailboxes',
builder: (ctx, state) => MailboxListScreen(
@@ -202,6 +241,29 @@ Widget buildApp({
);
}
/// Convenience override list used by most widget tests.
///
/// Includes fakes for all repositories and the two network services so tests
/// never hit the real database, network, or IMAP server.
List<Override> baseOverrides({
List<Account>? accounts,
List<Mailbox>? mailboxes,
DiscoveryResult? discovery,
Exception? connectionError,
}) =>
[
accountRepositoryProvider
.overrideWithValue(FakeAccountRepository(accounts)),
mailboxRepositoryProvider.overrideWithValue(FakeMailboxRepository(mailboxes)),
emailRepositoryProvider.overrideWithValue(FakeEmailRepository()),
accountDiscoveryServiceProvider.overrideWithValue(
FakeDiscoveryService(discovery ?? UnknownDiscovery()),
),
connectionTestServiceProvider.overrideWithValue(
FakeConnectionTestService(error: connectionError),
),
];
// ---------------------------------------------------------------------------
// Common test fixtures
// ---------------------------------------------------------------------------
@@ -211,11 +273,7 @@ const kTestAccount = Account(
displayName: 'Alice',
email: 'alice@example.com',
imapHost: 'imap.example.com',
imapPort: 993,
imapSsl: true,
smtpHost: 'smtp.example.com',
smtpPort: 587,
smtpSsl: false,
);
const kTestMailbox = Mailbox(