feat(app)!: no anonymous accounts on the device (ADR 0020, D4, D25)

Every account has a way in, so the apps make no anonymous account any
more. The start screen shows whenever nobody is signed in, a first
start included, and offers SimpleLogin, a password and GitHub for those
who have it, with no start without an account. An instance out of reach
is said so, with Try again, and one offering no way to create an account
too. An anonymous account's session left from before is deleted as the
app starts (D25), falling back to signing out on the device.

What only anonymous accounts needed goes: signing in anonymously, the
signed-out mark, adding GitHub to an account and switching to a GitHub
account from the Profile tab (with its Cancel, the only one), an
anonymous device's own keys, the warning that signing out deletes the
account, and the texts about keeping progress. Settings says on which
instance the account is signed in. Learn's publishing, reports and
suggestions and its learner count no longer ask whether the person has
an identity: everyone signed in has one.

The server still offers anonymous sign-in until the next commit, so
older apps keep working until v0.20.0 raises the minimum build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-24 14:50:55 +02:00
commit 5b9df73756
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -10,7 +10,6 @@ void main() {
final builtIn = Instance.parse('todo.example.org')!;
late MemoryInstanceStore store;
late FakeAuthGateway gateway;
late MemorySignedOutMark signedOut;
late List<SignedIn> opened;
late MemorySignInAddresses addresses;
@ -18,16 +17,16 @@ void main() {
store = MemoryInstanceStore();
gateway = FakeAuthGateway();
addresses = MemorySignInAddresses();
signedOut = MemorySignedOutMark();
opened = [];
});
/// Starts Todo on a new install ([usedBefore] false) or on one that chose
/// its instance before, signed out as [wasSignedOut] says.
/// Starts Todo on a new install ([usedBefore] false), where nobody is
/// signed in, or on one that chose its instance before, signed in unless
/// [signedOut].
Future<void> start(
WidgetTester tester, {
bool usedBefore = false,
bool wasSignedOut = false,
bool signedOut = false,
bool passwords = true,
FakeSimpleLoginGateway? simpleLogin,
}) async {
@ -41,7 +40,9 @@ void main() {
sharesStorage: (a, b) => false,
);
await choice.load(usedBefore: usedBefore);
await signedOut.write(signedOut: wasSignedOut);
if (!usedBefore || signedOut) {
gateway.revokeSession();
}
await tester.pumpWidget(
TodoApp(
choice: choice,
@ -50,7 +51,6 @@ void main() {
open: (instance) async => TodoDependencies(
serverName: instance.name,
auth: AuthRepository(gateway, addresses),
signedOut: signedOut,
passwords: PasswordRepository(
FakePasswordGateway(gateway)..offered = passwords,
FakeAccountCrypto(),
@ -110,14 +110,22 @@ void main() {
expect(opened, isEmpty, reason: 'nothing made anywhere yet');
});
testWidgets("the build's instance in one tap: an account without a name, "
'and an empty home', (tester) async {
testWidgets("the build's instance in one tap, then how to start on it, "
'making no account by itself (ADR 0020, D25)', (tester) async {
await start(tester);
await tester.tap(find.text('todo.example.org'));
await tester.pumpAndSettle();
expect(opened.single.isIdentified, isFalse);
expect(find.text('Start on todo.example.org:'), findsOneWidget);
expect(find.text('Create an account'), findsOneWidget);
expect(opened, isEmpty);
});
testWidgets("an account's home is empty until tasks come", (tester) async {
await start(tester, usedBefore: true);
expect(opened.single, FakeAuthGateway.person);
expect(find.text('Nothing here yet'), findsOneWidget);
expect(
find.text('Tasks come with a later version of Halkyon Todo.'),
@ -128,7 +136,7 @@ void main() {
testWidgets('signed out, it asks how to start; a GitHub account nobody here '
'signs in with is said so, with an account to create in its place '
'where passwords are offered (ADR 0017)', (tester) async {
await start(tester, usedBefore: true, wasSignedOut: true);
await start(tester, usedBefore: true, signedOut: true);
expect(find.text('Sign in with GitHub'), findsOneWidget);
gateway.gitHubError = const GitHubSignInFailed(
@ -139,18 +147,20 @@ void main() {
expect(
find.text(
'No account here signs in with this GitHub account yet. Create an '
'No account here signs in with this GitHub account. Create an '
'account with your email address and a password instead.',
),
findsOneWidget,
);
});
testWidgets("where no passwords are offered, with Todo's ways in", (
tester,
) async {
await start(tester, usedBefore: true, wasSignedOut: true, passwords: false);
testWidgets('where no account can be made, it says so', (tester) async {
await start(tester, usedBefore: true, signedOut: true, passwords: false);
expect(
find.text('todo.example.org offers no way to create an account.'),
findsOneWidget,
);
gateway.gitHubError = const GitHubSignInFailed(
GitHubSignInProblem.unknownAccount,
);
@ -158,30 +168,24 @@ void main() {
await tester.pumpAndSettle();
expect(
find.text(
'No account here signs in with this GitHub account yet. Start '
'without an account, or continue with your email address.',
),
find.text('No account here signs in with this GitHub account.'),
findsOneWidget,
);
});
testWidgets('the account says whose it is, on which instance, and signing '
'out of an anonymous one asks first, as it deletes it', (tester) async {
testWidgets('the account says on which instance it is signed in; signing '
'out deletes nothing and asks how to start', (tester) async {
await start(tester, usedBefore: true);
await openAccount(tester);
expect(find.text('Anonymous on todo.example.org'), findsOneWidget);
expect(find.text('Signed in on todo.example.org'), findsOneWidget);
expect(find.text('Switch account'), findsOneWidget);
await tester.tap(find.text('Sign out'));
await tester.pumpAndSettle();
expect(find.text('Sign out?'), findsOneWidget);
await tester.tap(find.text('Sign out').last);
await tester.pumpAndSettle();
expect(gateway.accountDeletions, 1);
expect(gateway.signOuts, 1);
expect(gateway.accountDeletions, 0);
expect(gateway.status, const SignedOut());
expect(find.text('Sign in with GitHub'), findsOneWidget);
});