feat(app): a wait screen for accounts that wait for the operator (ADR 0021, step 1)

An account signed in or up to on an instance whose operator approves
new accounts, or one suspended, gets no session but a wait: the device
keeps its pass, words and handle in the keyring per instance
(SecureAccountWaitStore, InstanceStorage.waitOf), the auth status is
Waiting, and SessionGate shows WaitScreen in its own app, so the start
screen's sheets go with it. Every sign-in flow hands over to it: the
password's sign-in, sign-up, reset and account reset, SimpleLogin's
sign-in, sign-up and mending; their cubits show no problem then.

The wait screen shows the handle, the four words the operator reads out
for the person to match, and that Halkyon never asks anyone to read them
out (D33); it asks where the wait stands as it opens, every 30 seconds,
as the app comes back and when asked. A new account can be deleted with
its pass, a suspended one can't (ADR 0017). Approved, it leads to the
usual sign-in, once more as before (D31); a pass the instance knows no
more ends the wait. The start screen says where new accounts wait for
approval, and offers no Create an account where the operator takes none;
both refusals (closed, too many waiting) have their words, en and de.

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-25 15:29:22 +01:00
commit 7ac715cd97
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 2284 additions and 103 deletions

View file

@ -46,6 +46,8 @@
"startOn": "Starte auf {instance}:",
"startOtherInstance": "Andere Instanz verwenden",
"startNoWay": "{instance} bietet keinen Weg, ein Konto zu erstellen.",
"startSignUpsApproved": "Neue Konten auf {instance} warten, bis sie freigegeben werden.",
"startSignUpsClosed": "{instance} nimmt keine neuen Konten an: Melde dich mit deinem an.",
"syncTitle": "Synchronisierung",
"syncNow": "Jetzt synchronisieren",
"syncStateSyncing": "Wird synchronisiert…",
@ -63,6 +65,19 @@
"syncMismatchMore": "Mehr",
"startSignIn": "Anmelden",
"startCreateAccount": "Konto erstellen",
"waitWaiting": "Dein Konto wartet, bis {instance} es freigibt.",
"waitSuspended": "{instance} hat dein Konto gesperrt. Nur wer {instance} betreibt, kann die Sperre aufheben.",
"waitApproved": "{instance} hat dein Konto freigegeben. Melde dich noch einmal an, so wie vorhin.",
"waitEnded": "Diese Registrierung ist beendet: {instance} hat das Konto abgelehnt, oder es hat zu lange gewartet.",
"waitBack": "Zurück zum Start",
"waitMatchingCode": "Kontrollwörter",
"waitMatchingNote": "Wenn dir jemand von {instance} vier Wörter vorliest, prüf, ob es diese sind. Lies sie selbst nie vor: Halkyon bittet niemanden darum.",
"waitCheckNow": "Jetzt nachsehen",
"waitUnreachable": "{instance} war nicht erreichbar. Alle 30 Sekunden wird erneut nachgefragt.",
"waitDelete": "Dieses Konto löschen",
"waitDeleteTitle": "Dieses Konto löschen?",
"waitDeleteMessage": "Es wird mit allem gelöscht, was es hat, und niemand kann sich mehr damit anmelden.",
"waitDeleteFailed": "Das Konto konnte nicht gelöscht werden. Versuch es noch einmal.",
"passwordAddress": "E-Mail-Adresse",
"passwordPassword": "Passwort",
"passwordNewPassword": "Neues Passwort",
@ -200,6 +215,8 @@
"passwordUnusable": "Dieses Passwort geht nicht: Es ist leer oder enthält Zeichen, die kein Text sind.",
"passwordInstanceUnproven": "Die Instanz konnte nicht beweisen, dass sie dein Passwort kennt, daher vertraut dieses Gerät ihr nicht. Versuch es noch einmal; passiert es wieder, ist die Instanz nicht die, die sie zu sein behauptet.",
"passwordKeysMissing": "Die Schlüssel deines Kontos sind nicht auf diesem Gerät. Melde dich hier ab und mit deinem Passwort wieder an.",
"signUpsClosed": "Diese Instanz nimmt keine neuen Konten an.",
"signUpsTooManyWaiting": "Es warten schon so viele neue Konten auf ihre Freigabe, wie dürfen. Versuch es später noch einmal.",
"passwordUnsafeCosts": "Die Instanz wollte dein Passwort auf eine Weise prüfen lassen, die kein Konto nutzt, daher wurde nichts gesendet. Passiert es wieder, ist die Instanz nicht die, die sie zu sein behauptet.",
"passwordFailed": "Die Instanz war nicht erreichbar. Prüf die Verbindung und versuch es noch einmal.",
"passwordMinLength": "Mindestens {count} Zeichen",

View file

@ -164,6 +164,24 @@
}
}
},
"startSignUpsApproved": "New accounts on {instance} wait until its operator approves them.",
"@startSignUpsApproved": {
"description": "On the start screen, where new accounts wait for the operator's approval (Halkyon Learn's ADR 0021, D19).",
"placeholders": {
"instance": {
"type": "String"
}
}
},
"startSignUpsClosed": "{instance} takes no new accounts: sign in with yours.",
"@startSignUpsClosed": {
"description": "On the start screen, where the operator takes no new accounts (D19); Create an account is gone.",
"placeholders": {
"instance": {
"type": "String"
}
}
},
"syncTitle": "Sync",
"syncNow": "Sync now",
"syncStateSyncing": "Syncing…",
@ -216,6 +234,88 @@
"syncMismatchMore": "More",
"startSignIn": "Sign in",
"startCreateAccount": "Create an account",
"waitWaiting": "Your account waits until the operator of {instance} approves it.",
"@waitWaiting": {
"description": "On the wait screen of a new account (D31), under its handle.",
"placeholders": {
"instance": {
"type": "String"
}
}
},
"waitSuspended": "The operator of {instance} has suspended your account. Only they can lift it.",
"@waitSuspended": {
"description": "On the wait screen of a suspended account (D31).",
"placeholders": {
"instance": {
"type": "String"
}
}
},
"waitApproved": "The operator of {instance} has approved your account. Sign in once more, as you did before.",
"@waitApproved": {
"description": "On the wait screen once the account is approved (D31), above Sign in.",
"placeholders": {
"instance": {
"type": "String"
}
}
},
"waitEnded": "This sign-up has ended: the operator of {instance} refused it, or it waited too long.",
"@waitEnded": {
"description": "On the wait screen once the instance knows the wait no more: refused, past its 14 days, or its pass past its 30 (D31).",
"placeholders": {
"instance": {
"type": "String"
}
}
},
"waitBack": "Back to the start",
"@waitBack": {
"description": "The button of an ended wait: the start screen."
},
"waitMatchingCode": "Matching code",
"@waitMatchingCode": {
"description": "Above the four words the operator reads out for the person to match (D33)."
},
"waitMatchingNote": "When the operator of {instance} reads you four words, check that they are these. Never read them out yourself: Halkyon never asks anyone to.",
"@waitMatchingNote": {
"description": "Under the matching code (D33): the operator reads, the person confirms, so nobody posing as the operator learns the words.",
"placeholders": {
"instance": {
"type": "String"
}
}
},
"waitCheckNow": "Check now",
"@waitCheckNow": {
"description": "Asks the instance at once where the wait stands; it asks every 30 seconds by itself."
},
"waitUnreachable": "{instance} couldn't be reached. It's asked again every 30 seconds.",
"@waitUnreachable": {
"description": "On the wait screen, when asking where the wait stands failed.",
"placeholders": {
"instance": {
"type": "String"
}
}
},
"waitDelete": "Delete this account",
"@waitDelete": {
"description": "On the wait screen of a new account: deletes it with its pass (D31)."
},
"waitDeleteTitle": "Delete this account?",
"@waitDeleteTitle": {
"description": "The question before a waiting account is deleted."
},
"waitDeleteMessage": "It goes with everything it has, and nobody can sign in to it again.",
"@waitDeleteMessage": {
"description": "Under that question."
},
"waitDeleteFailed": "Couldn't delete the account. Try again.",
"@waitDeleteFailed": {
"description": "On the wait screen, when deleting the account failed."
},
"passwordAddress": "Email address",
"passwordPassword": "Password",
"passwordNewPassword": "New password",
@ -437,6 +537,14 @@
"passwordUnusable": "This password can't be used: it's empty, or holds characters that aren't text.",
"passwordInstanceUnproven": "The instance couldn't prove it knows your password, so this device trusts nothing it says. Try again; if it happens again, the instance isn't the one it claims to be.",
"passwordKeysMissing": "Your account's keys aren't on this device. Sign out and in again with your password here.",
"signUpsClosed": "This instance takes no new accounts.",
"@signUpsClosed": {
"description": "A sign-up refused: the operator takes no new accounts (D19)."
},
"signUpsTooManyWaiting": "As many new accounts wait for approval as may. Try again later.",
"@signUpsTooManyWaiting": {
"description": "A sign-up refused: ten new accounts wait for the operator's approval already (D19)."
},
"passwordUnsafeCosts": "The instance asked for your password to be checked in a way no account uses, so nothing was sent. If it happens again, the instance isn't the one it claims to be.",
"passwordFailed": "Couldn't reach the instance. Check the connection and try again.",
"passwordMinLength": "At least {count} characters",

View file

@ -379,6 +379,18 @@ abstract class HalkyonAppLocalizations {
/// **'{instance} offers no way to create an account.'**
String startNoWay(String instance);
/// On the start screen, where new accounts wait for the operator's approval (Halkyon Learn's ADR 0021, D19).
///
/// In en, this message translates to:
/// **'New accounts on {instance} wait until its operator approves them.'**
String startSignUpsApproved(String instance);
/// On the start screen, where the operator takes no new accounts (D19); Create an account is gone.
///
/// In en, this message translates to:
/// **'{instance} takes no new accounts: sign in with yours.'**
String startSignUpsClosed(String instance);
/// No description provided for @syncTitle.
///
/// In en, this message translates to:
@ -481,6 +493,84 @@ abstract class HalkyonAppLocalizations {
/// **'Create an account'**
String get startCreateAccount;
/// On the wait screen of a new account (D31), under its handle.
///
/// In en, this message translates to:
/// **'Your account waits until the operator of {instance} approves it.'**
String waitWaiting(String instance);
/// On the wait screen of a suspended account (D31).
///
/// In en, this message translates to:
/// **'The operator of {instance} has suspended your account. Only they can lift it.'**
String waitSuspended(String instance);
/// On the wait screen once the account is approved (D31), above Sign in.
///
/// In en, this message translates to:
/// **'The operator of {instance} has approved your account. Sign in once more, as you did before.'**
String waitApproved(String instance);
/// On the wait screen once the instance knows the wait no more: refused, past its 14 days, or its pass past its 30 (D31).
///
/// In en, this message translates to:
/// **'This sign-up has ended: the operator of {instance} refused it, or it waited too long.'**
String waitEnded(String instance);
/// The button of an ended wait: the start screen.
///
/// In en, this message translates to:
/// **'Back to the start'**
String get waitBack;
/// Above the four words the operator reads out for the person to match (D33).
///
/// In en, this message translates to:
/// **'Matching code'**
String get waitMatchingCode;
/// Under the matching code (D33): the operator reads, the person confirms, so nobody posing as the operator learns the words.
///
/// In en, this message translates to:
/// **'When the operator of {instance} reads you four words, check that they are these. Never read them out yourself: Halkyon never asks anyone to.'**
String waitMatchingNote(String instance);
/// Asks the instance at once where the wait stands; it asks every 30 seconds by itself.
///
/// In en, this message translates to:
/// **'Check now'**
String get waitCheckNow;
/// On the wait screen, when asking where the wait stands failed.
///
/// In en, this message translates to:
/// **'{instance} couldn\'t be reached. It\'s asked again every 30 seconds.'**
String waitUnreachable(String instance);
/// On the wait screen of a new account: deletes it with its pass (D31).
///
/// In en, this message translates to:
/// **'Delete this account'**
String get waitDelete;
/// The question before a waiting account is deleted.
///
/// In en, this message translates to:
/// **'Delete this account?'**
String get waitDeleteTitle;
/// Under that question.
///
/// In en, this message translates to:
/// **'It goes with everything it has, and nobody can sign in to it again.'**
String get waitDeleteMessage;
/// On the wait screen, when deleting the account failed.
///
/// In en, this message translates to:
/// **'Couldn\'t delete the account. Try again.'**
String get waitDeleteFailed;
/// No description provided for @passwordAddress.
///
/// In en, this message translates to:
@ -1303,6 +1393,18 @@ abstract class HalkyonAppLocalizations {
/// **'Your account\'s keys aren\'t on this device. Sign out and in again with your password here.'**
String get passwordKeysMissing;
/// A sign-up refused: the operator takes no new accounts (D19).
///
/// In en, this message translates to:
/// **'This instance takes no new accounts.'**
String get signUpsClosed;
/// A sign-up refused: ten new accounts wait for the operator's approval already (D19).
///
/// In en, this message translates to:
/// **'As many new accounts wait for approval as may. Try again later.'**
String get signUpsTooManyWaiting;
/// No description provided for @passwordUnsafeCosts.
///
/// In en, this message translates to:

View file

@ -183,6 +183,16 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
return '$instance bietet keinen Weg, ein Konto zu erstellen.';
}
@override
String startSignUpsApproved(String instance) {
return 'Neue Konten auf $instance warten, bis sie freigegeben werden.';
}
@override
String startSignUpsClosed(String instance) {
return '$instance nimmt keine neuen Konten an: Melde dich mit deinem an.';
}
@override
String get syncTitle => 'Synchronisierung';
@ -252,6 +262,59 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
@override
String get startCreateAccount => 'Konto erstellen';
@override
String waitWaiting(String instance) {
return 'Dein Konto wartet, bis $instance es freigibt.';
}
@override
String waitSuspended(String instance) {
return '$instance hat dein Konto gesperrt. Nur wer $instance betreibt, kann die Sperre aufheben.';
}
@override
String waitApproved(String instance) {
return '$instance hat dein Konto freigegeben. Melde dich noch einmal an, so wie vorhin.';
}
@override
String waitEnded(String instance) {
return 'Diese Registrierung ist beendet: $instance hat das Konto abgelehnt, oder es hat zu lange gewartet.';
}
@override
String get waitBack => 'Zurück zum Start';
@override
String get waitMatchingCode => 'Kontrollwörter';
@override
String waitMatchingNote(String instance) {
return 'Wenn dir jemand von $instance vier Wörter vorliest, prüf, ob es diese sind. Lies sie selbst nie vor: Halkyon bittet niemanden darum.';
}
@override
String get waitCheckNow => 'Jetzt nachsehen';
@override
String waitUnreachable(String instance) {
return '$instance war nicht erreichbar. Alle 30 Sekunden wird erneut nachgefragt.';
}
@override
String get waitDelete => 'Dieses Konto löschen';
@override
String get waitDeleteTitle => 'Dieses Konto löschen?';
@override
String get waitDeleteMessage =>
'Es wird mit allem gelöscht, was es hat, und niemand kann sich mehr damit anmelden.';
@override
String get waitDeleteFailed =>
'Das Konto konnte nicht gelöscht werden. Versuch es noch einmal.';
@override
String get passwordAddress => 'E-Mail-Adresse';
@ -753,6 +816,13 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
String get passwordKeysMissing =>
'Die Schlüssel deines Kontos sind nicht auf diesem Gerät. Melde dich hier ab und mit deinem Passwort wieder an.';
@override
String get signUpsClosed => 'Diese Instanz nimmt keine neuen Konten an.';
@override
String get signUpsTooManyWaiting =>
'Es warten schon so viele neue Konten auf ihre Freigabe, wie dürfen. Versuch es später noch einmal.';
@override
String get passwordUnsafeCosts =>
'Die Instanz wollte dein Passwort auf eine Weise prüfen lassen, die kein Konto nutzt, daher wurde nichts gesendet. Passiert es wieder, ist die Instanz nicht die, die sie zu sein behauptet.';

View file

@ -181,6 +181,16 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
return '$instance offers no way to create an account.';
}
@override
String startSignUpsApproved(String instance) {
return 'New accounts on $instance wait until its operator approves them.';
}
@override
String startSignUpsClosed(String instance) {
return '$instance takes no new accounts: sign in with yours.';
}
@override
String get syncTitle => 'Sync';
@ -249,6 +259,58 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
@override
String get startCreateAccount => 'Create an account';
@override
String waitWaiting(String instance) {
return 'Your account waits until the operator of $instance approves it.';
}
@override
String waitSuspended(String instance) {
return 'The operator of $instance has suspended your account. Only they can lift it.';
}
@override
String waitApproved(String instance) {
return 'The operator of $instance has approved your account. Sign in once more, as you did before.';
}
@override
String waitEnded(String instance) {
return 'This sign-up has ended: the operator of $instance refused it, or it waited too long.';
}
@override
String get waitBack => 'Back to the start';
@override
String get waitMatchingCode => 'Matching code';
@override
String waitMatchingNote(String instance) {
return 'When the operator of $instance reads you four words, check that they are these. Never read them out yourself: Halkyon never asks anyone to.';
}
@override
String get waitCheckNow => 'Check now';
@override
String waitUnreachable(String instance) {
return '$instance couldn\'t be reached. It\'s asked again every 30 seconds.';
}
@override
String get waitDelete => 'Delete this account';
@override
String get waitDeleteTitle => 'Delete this account?';
@override
String get waitDeleteMessage =>
'It goes with everything it has, and nobody can sign in to it again.';
@override
String get waitDeleteFailed => 'Couldn\'t delete the account. Try again.';
@override
String get passwordAddress => 'Email address';
@ -736,6 +798,13 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
String get passwordKeysMissing =>
'Your account\'s keys aren\'t on this device. Sign out and in again with your password here.';
@override
String get signUpsClosed => 'This instance takes no new accounts.';
@override
String get signUpsTooManyWaiting =>
'As many new accounts wait for approval as may. Try again later.';
@override
String get passwordUnsafeCosts =>
'The instance asked for your password to be checked in a way no account uses, so nothing was sent. If it happens again, the instance isn\'t the one it claims to be.';