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
8 changed files with 981 additions and 3339 deletions

View file

@ -5,14 +5,11 @@
"launchProduct": "Todo",
"instanceFirstStartSubtitle": "Wähle die Instanz, auf der deine Aufgaben liegen sollen. Jede betreibt jemand; weitere kannst du später hinzufügen.",
"emailSignInSwitchMessage": "Diese Adresse hat bereits ein Konto. Die Aufgaben auf diesem Gerät lassen sich noch nicht damit zusammenführen: Wenn du wechselst, werden sie gelöscht.",
"startOtherWays": "Starte ohne Konto oder fahre mit deiner E-Mail-Adresse fort.",
"homeEmptyTitle": "Hier ist noch nichts",
"homeEmptyHint": "Aufgaben kommen mit einer späteren Version von Halkyon Todo.",
"accountTitle": "Konto",
"accountSignOutAnonymousMessage": "Bei diesem Konto kannst du dich nicht wieder anmelden, deshalb wird es beim Abmelden gelöscht.",
"accountDeleteMessage": "Dein Konto wird auf dem Server und auf all deinen Geräten gelöscht. Dies kann nicht rückgängig gemacht werden.",
"aboutTitle": "Über"

View file

@ -7,15 +7,11 @@
"@launchProduct": {"description": "The app's word in its wordmark, under \"Halkyon\"."},
"instanceFirstStartSubtitle": "Choose the instance to keep your tasks on. Someone runs each one; you can add others later.",
"emailSignInSwitchMessage": "This address already has an account. The tasks on this device can't be combined with it yet: if you switch, they are deleted.",
"startOtherWays": "Start without an account, or continue with your email address.",
"@startOtherWays": {"description": "Said after a GitHub account nobody on the instance signs in with: the ways in the start screen offers instead."},
"homeEmptyTitle": "Nothing here yet",
"homeEmptyHint": "Tasks come with a later version of Halkyon Todo.",
"accountTitle": "Account",
"accountSignOutAnonymousMessage": "This account can't be signed into again, so signing out deletes it.",
"accountDeleteMessage": "Your account is deleted on the server and on all your devices. This can't be undone.",
"aboutTitle": "About"

View file

@ -118,18 +118,6 @@ abstract class AppLocalizations {
/// **'Choose the instance to keep your tasks on. Someone runs each one; you can add others later.'**
String get instanceFirstStartSubtitle;
/// No description provided for @emailSignInSwitchMessage.
///
/// In en, this message translates to:
/// **'This address already has an account. The tasks on this device can\'t be combined with it yet: if you switch, they are deleted.'**
String get emailSignInSwitchMessage;
/// Said after a GitHub account nobody on the instance signs in with: the ways in the start screen offers instead.
///
/// In en, this message translates to:
/// **'Start without an account, or continue with your email address.'**
String get startOtherWays;
/// No description provided for @homeEmptyTitle.
///
/// In en, this message translates to:
@ -148,12 +136,6 @@ abstract class AppLocalizations {
/// **'Account'**
String get accountTitle;
/// No description provided for @accountSignOutAnonymousMessage.
///
/// In en, this message translates to:
/// **'This account can\'t be signed into again, so signing out deletes it.'**
String get accountSignOutAnonymousMessage;
/// No description provided for @accountDeleteMessage.
///
/// In en, this message translates to:

View file

@ -19,14 +19,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get instanceFirstStartSubtitle =>
'Wähle die Instanz, auf der deine Aufgaben liegen sollen. Jede betreibt jemand; weitere kannst du später hinzufügen.';
@override
String get emailSignInSwitchMessage =>
'Diese Adresse hat bereits ein Konto. Die Aufgaben auf diesem Gerät lassen sich noch nicht damit zusammenführen: Wenn du wechselst, werden sie gelöscht.';
@override
String get startOtherWays =>
'Starte ohne Konto oder fahre mit deiner E-Mail-Adresse fort.';
@override
String get homeEmptyTitle => 'Hier ist noch nichts';
@ -37,10 +29,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get accountTitle => 'Konto';
@override
String get accountSignOutAnonymousMessage =>
'Bei diesem Konto kannst du dich nicht wieder anmelden, deshalb wird es beim Abmelden gelöscht.';
@override
String get accountDeleteMessage =>
'Dein Konto wird auf dem Server und auf all deinen Geräten gelöscht. Dies kann nicht rückgängig gemacht werden.';

View file

@ -19,14 +19,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get instanceFirstStartSubtitle =>
'Choose the instance to keep your tasks on. Someone runs each one; you can add others later.';
@override
String get emailSignInSwitchMessage =>
'This address already has an account. The tasks on this device can\'t be combined with it yet: if you switch, they are deleted.';
@override
String get startOtherWays =>
'Start without an account, or continue with your email address.';
@override
String get homeEmptyTitle => 'Nothing here yet';
@ -37,10 +29,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get accountTitle => 'Account';
@override
String get accountSignOutAnonymousMessage =>
'This account can\'t be signed into again, so signing out deletes it.';
@override
String get accountDeleteMessage =>
'Your account is deleted on the server and on all your devices. This can\'t be undone.';

View file

@ -27,10 +27,7 @@ class const _AccountSheet() extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AccountTiles(
signOutAnonymousMessage: l10n.accountSignOutAnonymousMessage,
deleteMessage: l10n.accountDeleteMessage,
),
AccountTiles(deleteMessage: l10n.accountDeleteMessage),
// SimpleLogin keeps the account where the instance offers it
// (ADR 0020, D21), and a password and an authenticator, signing
// in on other devices and in the instance's other apps (ADR 0017).

View file

@ -33,9 +33,6 @@ class TodoDependencies({
/// Who is signed in, and signing in and out.
@override required final AuthRepository auth,
/// Whether the person here signed out, so the app asks how to start.
@override required final SignedOutMark signedOut,
/// Signing in with a password and TOTP, and what keeps an account (ADR
/// 0017).
@override required final PasswordRepository passwords,
@ -82,8 +79,8 @@ class TodoDependencies({
authSuccessStorageKey: device.storage.sessionKeyOf(instance),
),
);
// Restores a stored session before anything asks who is signed in;
// otherwise the first launch logic would sign in a second anonymous person.
// Restores a stored session before anything asks who is signed in, so
// the app doesn't ask how to start while someone is.
await client.auth.initialize();
final authGateway = ServerpodAuthGateway(
@ -121,7 +118,6 @@ class TodoDependencies({
return TodoDependencies(
serverName: instance.name,
auth: AuthRepository(authGateway, addresses),
signedOut: FileSignedOutMark(people),
passwords: passwords,
simpleLogin: simpleLogin,
fileSaver: PlatformFileSaver(),

View file

@ -9,15 +9,13 @@ const todoIcon = AssetImage('assets/icon/halkyon_todo_icon.png');
/// Halkyon Todo as halkyon_app's own screens know it (the first start, the
/// start screen, the account switcher, signing in): its name and word, its
/// icon, its look and languages, and the three sentences only it can say.
/// icon, its look and languages, and the sentence only it can say.
const todoFrame = AppFrame(
name: _name,
product: _product,
icon: todoIcon,
exampleAddress: 'todo.example.org',
firstStartSubtitle: _firstStartSubtitle,
switchLeavesBehind: _switchLeavesBehind,
thenAddGitHub: _startOtherWays,
theme: appTheme,
localizationsDelegates: todoLocalizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
@ -33,12 +31,6 @@ String _product(BuildContext context) =>
String _firstStartSubtitle(BuildContext context) =>
AppLocalizations.of(context).instanceFirstStartSubtitle;
String _switchLeavesBehind(BuildContext context) =>
AppLocalizations.of(context).emailSignInSwitchMessage;
String _startOtherWays(BuildContext context) =>
AppLocalizations.of(context).startOtherWays;
/// What shows while the app opens: halkyon_ui's [LaunchScreen] with Todo's
/// icon, "Todo" beneath "Halkyon", read out as the app's name.
class const TodoLaunchScreen({super.key}) extends StatelessWidget {