feat(auth)!: codes by mail no longer sign in
ADR 0017, step 3 (D4: nobody signs in with email codes yet, so the password replaces them outright). `emailCode.verifyCode`, `emailSignIn.signIn` and the switch token go, with the register's `verifySignInCode` and `completeEmailSwitch` and their operations between services. `emailCode.sendCode` stays, now without a session (signing up and getting back in have none), as what proves an address for a password's setup, a sign-up, a setup never finished and the resets; `accountRecovery.sendCode`, which did that for the resets, goes. The `switchChallengeId` column stays: Serverpod's challenges still link a completion token, and older servers read it during a deploy. The apps: halkyon_app's email sign-in (its bloc, sheet, gateway calls and the start screen's Continue with email) goes; Learn's profile and Todo's account sheet show `AccountSecuritySection` (a password set up by someone anonymous or signing in with GitHub, whose end it names; the authenticators; changing the password; new recovery codes and phrase), which reads anew after every change the repository makes. BREAKING CHANGE: Learn's `minimumAppBuild` is 22: builds up to 21 sign in with the endpoints that went. Todo was never released and stays at build 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
735dfa892a
commit
ae57cbe864
68 changed files with 429 additions and 3835 deletions
|
|
@ -477,37 +477,13 @@
|
||||||
"profileSwitchMessage": "Dieses GitHub-Konto hat bereits Fortschritt von einem anderen Gerät. Die Lernsets auf diesem Gerät lassen sich noch nicht damit zusammenführen: Wenn du wechselst, werden sie gelöscht.",
|
"profileSwitchMessage": "Dieses GitHub-Konto hat bereits Fortschritt von einem anderen Gerät. Die Lernsets auf diesem Gerät lassen sich noch nicht damit zusammenführen: Wenn du wechselst, werden sie gelöscht.",
|
||||||
"profileSwitchStay": "Anonym bleiben",
|
"profileSwitchStay": "Anonym bleiben",
|
||||||
"profileSwitchConfirm": "Wechseln",
|
"profileSwitchConfirm": "Wechseln",
|
||||||
"profileContinueWithEmail": "Mit E-Mail fortfahren",
|
|
||||||
"profileSignInWays": "Anmeldewege",
|
"profileSignInWays": "Anmeldewege",
|
||||||
"profileWayGitHub": "GitHub",
|
"profileWayGitHub": "GitHub",
|
||||||
"profileWayEmail": "E-Mail-Adresse",
|
"profileWayEmail": "E-Mail-Adresse",
|
||||||
"profileWayLinked": "Meldet dich an",
|
"profileWayLinked": "Meldet dich an",
|
||||||
"profileAddGitHub": "GitHub hinzufügen",
|
"profileAddGitHub": "GitHub hinzufügen",
|
||||||
"profileAddEmail": "E-Mail-Adresse hinzufügen",
|
|
||||||
"profileGitHubTaken": "Mit diesem GitHub-Konto meldet sich schon ein anderes Konto an.",
|
"profileGitHubTaken": "Mit diesem GitHub-Konto meldet sich schon ein anderes Konto an.",
|
||||||
"emailSignInTitle": "Mit E-Mail fortfahren",
|
|
||||||
"emailSignInIntro": "Wir schicken dir einen Code zum Anmelden. Diese Instanz behält nur einen Fingerabdruck deiner Adresse, nie die Adresse selbst, und schreibt ihr nur, wenn du einen Code anforderst.",
|
|
||||||
"emailSignInAddress": "E-Mail-Adresse",
|
|
||||||
"emailSignInSend": "Code senden",
|
|
||||||
"emailSignInCodeSent": "Wir haben einen Code an {address} geschickt. Er gilt 15 Minuten.",
|
|
||||||
"emailSignInResent": "Ein neuer Code ist unterwegs.",
|
|
||||||
"emailSignInCode": "Code",
|
|
||||||
"emailSignInContinue": "Weiter",
|
|
||||||
"emailSignInResend": "Neuen Code senden",
|
|
||||||
"emailSignInOtherAddress": "Andere Adresse verwenden",
|
|
||||||
"emailSignInSwitchTitle": "Zum Konto von {address} wechseln?",
|
|
||||||
"emailSignInSwitchMessage": "Diese Adresse hat bereits Fortschritt von einem anderen Gerät. Die Lernsets auf diesem Gerät lassen sich noch nicht damit zusammenführen: Wenn du wechselst, werden sie gelöscht.",
|
"emailSignInSwitchMessage": "Diese Adresse hat bereits Fortschritt von einem anderen Gerät. Die Lernsets auf diesem Gerät lassen sich noch nicht damit zusammenführen: Wenn du wechselst, werden sie gelöscht.",
|
||||||
"emailSignInTaken": "Mit {address} meldet sich schon ein anderes Konto an. Um zu ihm zu wechseln, melde dich zuerst ab.",
|
|
||||||
"emailSignInDone": "Mit {address} angemeldet.",
|
|
||||||
"emailSignInUnavailable": "Diese Instanz bietet keine Anmeldung mit E-Mail an.",
|
|
||||||
"emailSignInNotAnAddress": "Das ist keine E-Mail-Adresse.",
|
|
||||||
"emailSignInTooManyCodes": "Zu viele Codes in der letzten Stunde. Versuche es später noch einmal.",
|
|
||||||
"emailSignInWrongCode": "Dieser Code stimmt nicht.",
|
|
||||||
"emailSignInTooManyTries": "Zu viele falsche Codes. Fordere einen neuen an.",
|
|
||||||
"emailSignInCodeExpired": "Dieser Code gilt nicht mehr. Fordere einen neuen an.",
|
|
||||||
"emailSignInOtherAddressLinked": "Du meldest dich schon mit einer anderen Adresse an.",
|
|
||||||
"emailSignInFailed": "Die Instanz war nicht erreichbar. Versuche es noch einmal.",
|
|
||||||
"emailSignInMailFailed": "Die Instanz konnte die Mail nicht senden. Versuche es später noch einmal.",
|
|
||||||
"publicProfileTitle": "Öffentliches Profil",
|
"publicProfileTitle": "Öffentliches Profil",
|
||||||
"profileSheetTitle": "Profil",
|
"profileSheetTitle": "Profil",
|
||||||
"profileSheetNotFound": "Niemand auf dieser Instanz hat diesen Handle.",
|
"profileSheetNotFound": "Niemand auf dieser Instanz hat diesen Handle.",
|
||||||
|
|
|
||||||
|
|
@ -542,41 +542,13 @@
|
||||||
"profileSwitchMessage": "This GitHub account already has progress from another device. The studysets on this device can't be combined with it yet: if you switch, they are deleted.",
|
"profileSwitchMessage": "This GitHub account already has progress from another device. The studysets on this device can't be combined with it yet: if you switch, they are deleted.",
|
||||||
"profileSwitchStay": "Stay anonymous",
|
"profileSwitchStay": "Stay anonymous",
|
||||||
"profileSwitchConfirm": "Switch",
|
"profileSwitchConfirm": "Switch",
|
||||||
"profileContinueWithEmail": "Continue with email",
|
|
||||||
"profileSignInWays": "Ways to sign in",
|
"profileSignInWays": "Ways to sign in",
|
||||||
"profileWayGitHub": "GitHub",
|
"profileWayGitHub": "GitHub",
|
||||||
"profileWayEmail": "Email address",
|
"profileWayEmail": "Email address",
|
||||||
"profileWayLinked": "Signs you in",
|
"profileWayLinked": "Signs you in",
|
||||||
"profileAddGitHub": "Add GitHub",
|
"profileAddGitHub": "Add GitHub",
|
||||||
"profileAddEmail": "Add an email address",
|
|
||||||
"profileGitHubTaken": "This GitHub account already signs in to another account.",
|
"profileGitHubTaken": "This GitHub account already signs in to another account.",
|
||||||
"emailSignInTitle": "Continue with email",
|
|
||||||
"emailSignInIntro": "We mail you a code to sign in. This instance keeps only a fingerprint of your address, never the address itself, and writes to it only when you ask for a code.",
|
|
||||||
"emailSignInAddress": "Email address",
|
|
||||||
"emailSignInSend": "Send code",
|
|
||||||
"emailSignInCodeSent": "We mailed a code to {address}. It's valid for 15 minutes.",
|
|
||||||
"@emailSignInCodeSent": {"placeholders": {"address": {"type": "String"}}},
|
|
||||||
"emailSignInResent": "A new code is on its way.",
|
|
||||||
"emailSignInCode": "Code",
|
|
||||||
"emailSignInContinue": "Continue",
|
|
||||||
"emailSignInResend": "Send a new code",
|
|
||||||
"emailSignInOtherAddress": "Use another address",
|
|
||||||
"emailSignInSwitchTitle": "Switch to the account of {address}?",
|
|
||||||
"@emailSignInSwitchTitle": {"placeholders": {"address": {"type": "String"}}},
|
|
||||||
"emailSignInSwitchMessage": "This address already has progress from another device. The studysets on this device can't be combined with it yet: if you switch, they are deleted.",
|
"emailSignInSwitchMessage": "This address already has progress from another device. The studysets on this device can't be combined with it yet: if you switch, they are deleted.",
|
||||||
"emailSignInTaken": "{address} already signs in to another account. To switch to it, sign out first.",
|
|
||||||
"@emailSignInTaken": {"placeholders": {"address": {"type": "String"}}},
|
|
||||||
"emailSignInDone": "Signed in with {address}.",
|
|
||||||
"@emailSignInDone": {"placeholders": {"address": {"type": "String"}}},
|
|
||||||
"emailSignInUnavailable": "This instance doesn't offer signing in with email.",
|
|
||||||
"emailSignInNotAnAddress": "This isn't an email address.",
|
|
||||||
"emailSignInTooManyCodes": "Too many codes in the last hour. Try again later.",
|
|
||||||
"emailSignInWrongCode": "That code isn't right.",
|
|
||||||
"emailSignInTooManyTries": "Too many wrong codes. Ask for a new one.",
|
|
||||||
"emailSignInCodeExpired": "This code is no longer valid. Ask for a new one.",
|
|
||||||
"emailSignInOtherAddressLinked": "You already sign in with another address.",
|
|
||||||
"emailSignInFailed": "Couldn't reach the instance. Try again.",
|
|
||||||
"emailSignInMailFailed": "The instance couldn't send the mail. Try again later.",
|
|
||||||
"publicProfileTitle": "Public profile",
|
"publicProfileTitle": "Public profile",
|
||||||
"profileSheetTitle": "Profile",
|
"profileSheetTitle": "Profile",
|
||||||
"profileSheetNotFound": "Nobody on this instance has this handle.",
|
"profileSheetNotFound": "Nobody on this instance has this handle.",
|
||||||
|
|
|
||||||
|
|
@ -2872,12 +2872,6 @@ abstract class AppLocalizations {
|
||||||
/// **'Switch'**
|
/// **'Switch'**
|
||||||
String get profileSwitchConfirm;
|
String get profileSwitchConfirm;
|
||||||
|
|
||||||
/// No description provided for @profileContinueWithEmail.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Continue with email'**
|
|
||||||
String get profileContinueWithEmail;
|
|
||||||
|
|
||||||
/// No description provided for @profileSignInWays.
|
/// No description provided for @profileSignInWays.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -2908,156 +2902,18 @@ abstract class AppLocalizations {
|
||||||
/// **'Add GitHub'**
|
/// **'Add GitHub'**
|
||||||
String get profileAddGitHub;
|
String get profileAddGitHub;
|
||||||
|
|
||||||
/// No description provided for @profileAddEmail.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Add an email address'**
|
|
||||||
String get profileAddEmail;
|
|
||||||
|
|
||||||
/// No description provided for @profileGitHubTaken.
|
/// No description provided for @profileGitHubTaken.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'This GitHub account already signs in to another account.'**
|
/// **'This GitHub account already signs in to another account.'**
|
||||||
String get profileGitHubTaken;
|
String get profileGitHubTaken;
|
||||||
|
|
||||||
/// No description provided for @emailSignInTitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Continue with email'**
|
|
||||||
String get emailSignInTitle;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInIntro.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'We mail you a code to sign in. This instance keeps only a fingerprint of your address, never the address itself, and writes to it only when you ask for a code.'**
|
|
||||||
String get emailSignInIntro;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInAddress.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Email address'**
|
|
||||||
String get emailSignInAddress;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInSend.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Send code'**
|
|
||||||
String get emailSignInSend;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInCodeSent.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'We mailed a code to {address}. It\'s valid for 15 minutes.'**
|
|
||||||
String emailSignInCodeSent(String address);
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInResent.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'A new code is on its way.'**
|
|
||||||
String get emailSignInResent;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInCode.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Code'**
|
|
||||||
String get emailSignInCode;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInContinue.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Continue'**
|
|
||||||
String get emailSignInContinue;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInResend.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Send a new code'**
|
|
||||||
String get emailSignInResend;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInOtherAddress.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Use another address'**
|
|
||||||
String get emailSignInOtherAddress;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInSwitchTitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Switch to the account of {address}?'**
|
|
||||||
String emailSignInSwitchTitle(String address);
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInSwitchMessage.
|
/// No description provided for @emailSignInSwitchMessage.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'This address already has progress from another device. The studysets on this device can\'t be combined with it yet: if you switch, they are deleted.'**
|
/// **'This address already has progress from another device. The studysets on this device can\'t be combined with it yet: if you switch, they are deleted.'**
|
||||||
String get emailSignInSwitchMessage;
|
String get emailSignInSwitchMessage;
|
||||||
|
|
||||||
/// No description provided for @emailSignInTaken.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'{address} already signs in to another account. To switch to it, sign out first.'**
|
|
||||||
String emailSignInTaken(String address);
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInDone.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Signed in with {address}.'**
|
|
||||||
String emailSignInDone(String address);
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInUnavailable.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'This instance doesn\'t offer signing in with email.'**
|
|
||||||
String get emailSignInUnavailable;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInNotAnAddress.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'This isn\'t an email address.'**
|
|
||||||
String get emailSignInNotAnAddress;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInTooManyCodes.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Too many codes in the last hour. Try again later.'**
|
|
||||||
String get emailSignInTooManyCodes;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInWrongCode.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'That code isn\'t right.'**
|
|
||||||
String get emailSignInWrongCode;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInTooManyTries.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Too many wrong codes. Ask for a new one.'**
|
|
||||||
String get emailSignInTooManyTries;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInCodeExpired.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'This code is no longer valid. Ask for a new one.'**
|
|
||||||
String get emailSignInCodeExpired;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInOtherAddressLinked.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'You already sign in with another address.'**
|
|
||||||
String get emailSignInOtherAddressLinked;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInFailed.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Couldn\'t reach the instance. Try again.'**
|
|
||||||
String get emailSignInFailed;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInMailFailed.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'The instance couldn\'t send the mail. Try again later.'**
|
|
||||||
String get emailSignInMailFailed;
|
|
||||||
|
|
||||||
/// No description provided for @publicProfileTitle.
|
/// No description provided for @publicProfileTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -1741,9 +1741,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get profileSwitchConfirm => 'Wechseln';
|
String get profileSwitchConfirm => 'Wechseln';
|
||||||
|
|
||||||
@override
|
|
||||||
String get profileContinueWithEmail => 'Mit E-Mail fortfahren';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get profileSignInWays => 'Anmeldewege';
|
String get profileSignInWays => 'Anmeldewege';
|
||||||
|
|
||||||
|
|
@ -1759,99 +1756,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get profileAddGitHub => 'GitHub hinzufügen';
|
String get profileAddGitHub => 'GitHub hinzufügen';
|
||||||
|
|
||||||
@override
|
|
||||||
String get profileAddEmail => 'E-Mail-Adresse hinzufügen';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get profileGitHubTaken =>
|
String get profileGitHubTaken =>
|
||||||
'Mit diesem GitHub-Konto meldet sich schon ein anderes Konto an.';
|
'Mit diesem GitHub-Konto meldet sich schon ein anderes Konto an.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInTitle => 'Mit E-Mail fortfahren';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInIntro =>
|
|
||||||
'Wir schicken dir einen Code zum Anmelden. Diese Instanz behält nur einen Fingerabdruck deiner Adresse, nie die Adresse selbst, und schreibt ihr nur, wenn du einen Code anforderst.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInAddress => 'E-Mail-Adresse';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInSend => 'Code senden';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String emailSignInCodeSent(String address) {
|
|
||||||
return 'Wir haben einen Code an $address geschickt. Er gilt 15 Minuten.';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInResent => 'Ein neuer Code ist unterwegs.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInCode => 'Code';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInContinue => 'Weiter';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInResend => 'Neuen Code senden';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInOtherAddress => 'Andere Adresse verwenden';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String emailSignInSwitchTitle(String address) {
|
|
||||||
return 'Zum Konto von $address wechseln?';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get emailSignInSwitchMessage =>
|
String get emailSignInSwitchMessage =>
|
||||||
'Diese Adresse hat bereits Fortschritt von einem anderen Gerät. Die Lernsets auf diesem Gerät lassen sich noch nicht damit zusammenführen: Wenn du wechselst, werden sie gelöscht.';
|
'Diese Adresse hat bereits Fortschritt von einem anderen Gerät. Die Lernsets auf diesem Gerät lassen sich noch nicht damit zusammenführen: Wenn du wechselst, werden sie gelöscht.';
|
||||||
|
|
||||||
@override
|
|
||||||
String emailSignInTaken(String address) {
|
|
||||||
return 'Mit $address meldet sich schon ein anderes Konto an. Um zu ihm zu wechseln, melde dich zuerst ab.';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String emailSignInDone(String address) {
|
|
||||||
return 'Mit $address angemeldet.';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInUnavailable =>
|
|
||||||
'Diese Instanz bietet keine Anmeldung mit E-Mail an.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInNotAnAddress => 'Das ist keine E-Mail-Adresse.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInTooManyCodes =>
|
|
||||||
'Zu viele Codes in der letzten Stunde. Versuche es später noch einmal.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInWrongCode => 'Dieser Code stimmt nicht.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInTooManyTries =>
|
|
||||||
'Zu viele falsche Codes. Fordere einen neuen an.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInCodeExpired =>
|
|
||||||
'Dieser Code gilt nicht mehr. Fordere einen neuen an.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInOtherAddressLinked =>
|
|
||||||
'Du meldest dich schon mit einer anderen Adresse an.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInFailed =>
|
|
||||||
'Die Instanz war nicht erreichbar. Versuche es noch einmal.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInMailFailed =>
|
|
||||||
'Die Instanz konnte die Mail nicht senden. Versuche es später noch einmal.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get publicProfileTitle => 'Öffentliches Profil';
|
String get publicProfileTitle => 'Öffentliches Profil';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1725,9 +1725,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get profileSwitchConfirm => 'Switch';
|
String get profileSwitchConfirm => 'Switch';
|
||||||
|
|
||||||
@override
|
|
||||||
String get profileContinueWithEmail => 'Continue with email';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get profileSignInWays => 'Ways to sign in';
|
String get profileSignInWays => 'Ways to sign in';
|
||||||
|
|
||||||
|
|
@ -1743,98 +1740,14 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get profileAddGitHub => 'Add GitHub';
|
String get profileAddGitHub => 'Add GitHub';
|
||||||
|
|
||||||
@override
|
|
||||||
String get profileAddEmail => 'Add an email address';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get profileGitHubTaken =>
|
String get profileGitHubTaken =>
|
||||||
'This GitHub account already signs in to another account.';
|
'This GitHub account already signs in to another account.';
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInTitle => 'Continue with email';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInIntro =>
|
|
||||||
'We mail you a code to sign in. This instance keeps only a fingerprint of your address, never the address itself, and writes to it only when you ask for a code.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInAddress => 'Email address';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInSend => 'Send code';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String emailSignInCodeSent(String address) {
|
|
||||||
return 'We mailed a code to $address. It\'s valid for 15 minutes.';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInResent => 'A new code is on its way.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInCode => 'Code';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInContinue => 'Continue';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInResend => 'Send a new code';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInOtherAddress => 'Use another address';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String emailSignInSwitchTitle(String address) {
|
|
||||||
return 'Switch to the account of $address?';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get emailSignInSwitchMessage =>
|
String get emailSignInSwitchMessage =>
|
||||||
'This address already has progress from another device. The studysets on this device can\'t be combined with it yet: if you switch, they are deleted.';
|
'This address already has progress from another device. The studysets on this device can\'t be combined with it yet: if you switch, they are deleted.';
|
||||||
|
|
||||||
@override
|
|
||||||
String emailSignInTaken(String address) {
|
|
||||||
return '$address already signs in to another account. To switch to it, sign out first.';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String emailSignInDone(String address) {
|
|
||||||
return 'Signed in with $address.';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInUnavailable =>
|
|
||||||
'This instance doesn\'t offer signing in with email.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInNotAnAddress => 'This isn\'t an email address.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInTooManyCodes =>
|
|
||||||
'Too many codes in the last hour. Try again later.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInWrongCode => 'That code isn\'t right.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInTooManyTries =>
|
|
||||||
'Too many wrong codes. Ask for a new one.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInCodeExpired =>
|
|
||||||
'This code is no longer valid. Ask for a new one.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInOtherAddressLinked =>
|
|
||||||
'You already sign in with another address.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInFailed => 'Couldn\'t reach the instance. Try again.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInMailFailed =>
|
|
||||||
'The instance couldn\'t send the mail. Try again later.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get publicProfileTitle => 'Public profile';
|
String get publicProfileTitle => 'Public profile';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -189,10 +189,7 @@ GoRouter buildRouter({String initialLocation = RoutePaths.home}) => GoRouter(
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RoutePaths.profile,
|
path: RoutePaths.profile,
|
||||||
builder: (context, state) => ProfileScreen(
|
builder: (context, state) => const ProfileScreen(),
|
||||||
signsInWithEmail:
|
|
||||||
state.uri.queryParameters['signIn'] == 'email',
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,7 @@ class const PersonApp({
|
||||||
|
|
||||||
class _PersonAppState() extends State<PersonApp> {
|
class _PersonAppState() extends State<PersonApp> {
|
||||||
late final GoRouter _router = buildRouter(
|
late final GoRouter _router = buildRouter(
|
||||||
// Started with an email address: Profile asks for it at once.
|
initialLocation: widget.initialLocation,
|
||||||
initialLocation: switch (context.read<SessionBloc>().state) {
|
|
||||||
SessionStateActive(signsInWithEmail: true) =>
|
|
||||||
RoutePaths.profileEmailSignIn,
|
|
||||||
_ => widget.initialLocation,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,6 @@ abstract final class RoutePaths() {
|
||||||
/// The profile tab.
|
/// The profile tab.
|
||||||
static const profile = '/profile';
|
static const profile = '/profile';
|
||||||
|
|
||||||
/// Profile, asking for an email address to sign in with at once: as the
|
|
||||||
/// app opens after a start with one (decided 2026-10-16).
|
|
||||||
static const profileEmailSignIn = '$profile?signIn=email';
|
|
||||||
|
|
||||||
/// Settings, over the tabs.
|
/// Settings, over the tabs.
|
||||||
static const settings = '/settings';
|
static const settings = '/settings';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,11 @@ import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// The profile tab, in two tabs as Vaia has it (decided 2026-10-12): the
|
/// The profile tab, in two tabs as Vaia has it (decided 2026-10-12): the
|
||||||
/// profile (what the account means, signing in with GitHub or an email
|
/// profile (what the account means, keeping an anonymous one with GitHub or
|
||||||
/// address to keep an anonymous one, and the ways an identified person signs
|
/// a password, the ways an identified person signs in, and their password
|
||||||
/// in) and the person's statistics, which anonymous people have too.
|
/// and authenticators, ADR 0017) and the person's statistics, which
|
||||||
/// Settings are behind the gear.
|
/// anonymous people have too. Settings are behind the gear.
|
||||||
class const ProfileScreen({
|
class const ProfileScreen({super.key}) extends StatelessWidget {
|
||||||
/// Whether it asks for an email address to sign in with at once, as after
|
|
||||||
/// a start with one.
|
|
||||||
final bool signsInWithEmail = false,
|
|
||||||
super.key,
|
|
||||||
}) extends StatelessWidget {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
|
|
@ -42,46 +37,43 @@ class const ProfileScreen({
|
||||||
SignInWaysCubit(Dependencies.of(context).auth)..load(),
|
SignInWaysCubit(Dependencies.of(context).auth)..load(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: _AsksForEmail(
|
child: TopGradient(
|
||||||
asks: signsInWithEmail,
|
color: Brand.primary,
|
||||||
child: TopGradient(
|
background: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
color: Brand.primary,
|
child: DefaultTabController(
|
||||||
background: Theme.of(context).colorScheme.surfaceContainerLow,
|
length: 2,
|
||||||
child: DefaultTabController(
|
child: Scaffold(
|
||||||
length: 2,
|
backgroundColor: Colors.transparent,
|
||||||
child: Scaffold(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
appBar: AppBar(
|
systemOverlayStyle: TopGradient.overlayStyle(context),
|
||||||
backgroundColor: Colors.transparent,
|
title: Text(l10n.profileTitle),
|
||||||
systemOverlayStyle: TopGradient.overlayStyle(context),
|
actions: [
|
||||||
title: Text(l10n.profileTitle),
|
IconButton(
|
||||||
actions: [
|
icon: const SyncTroubleBadge(
|
||||||
IconButton(
|
child: Icon(LucideIcons.settings),
|
||||||
icon: const SyncTroubleBadge(
|
|
||||||
child: Icon(LucideIcons.settings),
|
|
||||||
),
|
|
||||||
tooltip: l10n.profileSettingsTooltip,
|
|
||||||
onPressed: () => context.push(RoutePaths.settings),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
tooltip: l10n.profileSettingsTooltip,
|
||||||
],
|
onPressed: () => context.push(RoutePaths.settings),
|
||||||
bottom: TabBar(
|
|
||||||
isScrollable: true,
|
|
||||||
tabAlignment: TabAlignment.start,
|
|
||||||
dividerColor: Colors.transparent,
|
|
||||||
tabs: [
|
|
||||||
Tab(text: l10n.profileTabProfile),
|
|
||||||
Tab(text: l10n.profileTabStatistics),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 4),
|
||||||
body: const TabBarView(
|
],
|
||||||
children: [
|
bottom: TabBar(
|
||||||
_CancelWhenLeft(child: _Account()),
|
isScrollable: true,
|
||||||
StatisticsView(),
|
tabAlignment: TabAlignment.start,
|
||||||
|
dividerColor: Colors.transparent,
|
||||||
|
tabs: [
|
||||||
|
Tab(text: l10n.profileTabProfile),
|
||||||
|
Tab(text: l10n.profileTabStatistics),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
body: const TabBarView(
|
||||||
|
children: [
|
||||||
|
_CancelWhenLeft(child: _Account()),
|
||||||
|
StatisticsView(),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -179,7 +171,9 @@ class const _Account() extends StatelessWidget {
|
||||||
if (isIdentified) const _SignInWays(),
|
if (isIdentified) const _SignInWays(),
|
||||||
if (!isIdentified) ...[
|
if (!isIdentified) ...[
|
||||||
const _GitHubSignIn(),
|
const _GitHubSignIn(),
|
||||||
const _EmailSignInButton(),
|
const SizedBox(height: 16),
|
||||||
|
// A password keeps the account too (ADR 0017).
|
||||||
|
const AccountSecuritySection(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
l10n.publicProfileAnonymousHint,
|
l10n.publicProfileAnonymousHint,
|
||||||
|
|
@ -212,58 +206,9 @@ bool _identifiedIn(SessionState state) => switch (state) {
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Signs in with an email address in its sheet; once the address is the
|
/// How an identified person signs in: GitHub, with a way to add it where
|
||||||
/// person's, says so and shows it among the ways they sign in.
|
/// the instance offers it, and an email address, with the password and
|
||||||
Future<void> _signInWithEmail(BuildContext context) async {
|
/// authenticators beneath.
|
||||||
final l10n = AppLocalizations.of(context);
|
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
|
||||||
final ways = context.read<SignInWaysCubit>();
|
|
||||||
final address = await showEmailSignInSheet(
|
|
||||||
context,
|
|
||||||
deviceHoldsContent: PersonDependencies.of(context).holdsContent,
|
|
||||||
);
|
|
||||||
if (address != null) {
|
|
||||||
messenger.showSnackBar(
|
|
||||||
SnackBar(content: Text(l10n.emailSignInDone(address))),
|
|
||||||
);
|
|
||||||
await ways.load();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The other way to keep an anonymous account, beside GitHub: an email
|
|
||||||
/// address, where the instance offers it.
|
|
||||||
class const _EmailSignInButton() extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final l10n = AppLocalizations.of(context);
|
|
||||||
final offersEmail = context.select<SignInWaysCubit, bool>(
|
|
||||||
(ways) => ways.state.offersEmail,
|
|
||||||
);
|
|
||||||
final gitHub = context.watch<GitHubSignInBloc>().state;
|
|
||||||
if (!offersEmail) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
final onPressed = gitHub is GitHubSignInStateBusy
|
|
||||||
? null
|
|
||||||
: () => _signInWithEmail(context);
|
|
||||||
final label = Text(l10n.profileContinueWithEmail);
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
top: gitHub is GitHubSignInStateUnavailable ? 16 : 8,
|
|
||||||
),
|
|
||||||
child: Align(
|
|
||||||
alignment: AlignmentDirectional.centerStart,
|
|
||||||
// The first way offered stands out; a second beside it doesn't.
|
|
||||||
child: gitHub is GitHubSignInStateUnavailable
|
|
||||||
? FilledButton(onPressed: onPressed, child: label)
|
|
||||||
: OutlinedButton(onPressed: onPressed, child: label),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How an identified person signs in, GitHub and an email address, each
|
|
||||||
/// with a way to add it where the instance offers it and they lack it.
|
|
||||||
class const _SignInWays() extends StatelessWidget {
|
class const _SignInWays() extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -313,17 +258,15 @@ class const _SignInWays() extends StatelessWidget {
|
||||||
Text(l10n.profileGitHubTaken, style: note),
|
Text(l10n.profileGitHubTaken, style: note),
|
||||||
if (gitHub case GitHubSignInStateReady(failed: true))
|
if (gitHub case GitHubSignInStateReady(failed: true))
|
||||||
Text(l10n.profileSignInFailed, style: note),
|
Text(l10n.profileSignInFailed, style: note),
|
||||||
if (ways.ways.email || ways.offersEmail)
|
if (ways.ways.email)
|
||||||
way(
|
way(
|
||||||
l10n.profileWayEmail,
|
l10n.profileWayEmail,
|
||||||
linkedAs: ways.ways.email
|
linkedAs: ways.emailAddress ?? l10n.profileWayLinked,
|
||||||
? ways.emailAddress ?? l10n.profileWayLinked
|
|
||||||
: null,
|
|
||||||
add: FilledButton.tonal(
|
|
||||||
onPressed: busy ? null : () => _signInWithEmail(context),
|
|
||||||
child: Text(l10n.profileAddEmail),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// The password and authenticators, and setting them up (ADR 0017),
|
||||||
|
// which brings the address.
|
||||||
|
const AccountSecuritySection(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -431,30 +374,3 @@ Future<bool> _confirmSwitch(BuildContext context) async {
|
||||||
);
|
);
|
||||||
return confirmed ?? false;
|
return confirmed ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Asks for an email address to sign in with once its screen shows, when
|
|
||||||
/// [asks] says so: after a start with one.
|
|
||||||
class const _AsksForEmail({
|
|
||||||
required final bool asks,
|
|
||||||
required final Widget child,
|
|
||||||
}) extends StatefulWidget {
|
|
||||||
@override
|
|
||||||
State<_AsksForEmail> createState() => _AsksForEmailState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AsksForEmailState() extends State<_AsksForEmail> {
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
if (widget.asks) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (mounted) {
|
|
||||||
unawaited(_signInWithEmail(context));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => widget.child;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
import 'package:halkyon_app/halkyon_app.dart';
|
|
||||||
import 'package:halkyon_app/testing.dart';
|
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
|
||||||
import 'package:material_ui/material_ui.dart';
|
|
||||||
|
|
||||||
import '../../../support/pump_app.dart';
|
|
||||||
import '../../../support/test_dependencies.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
late FakeAuthGateway auth;
|
|
||||||
late MemorySignInAddresses addresses;
|
|
||||||
|
|
||||||
setUp(() {
|
|
||||||
auth = FakeAuthGateway();
|
|
||||||
addresses = MemorySignInAddresses();
|
|
||||||
});
|
|
||||||
|
|
||||||
Future<void> openProfile(WidgetTester tester) async {
|
|
||||||
await pumpApp(
|
|
||||||
tester,
|
|
||||||
testDependencies(authGateway: auth, signInAddresses: addresses),
|
|
||||||
);
|
|
||||||
await openTab(tester, 'Profile');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The sheet's text field.
|
|
||||||
final inSheet = find.descendant(
|
|
||||||
of: find.byType(SheetScaffold),
|
|
||||||
matching: find.byType(TextField),
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Types [address] and sends a code to it, then types [code].
|
|
||||||
Future<void> signInWith(
|
|
||||||
WidgetTester tester,
|
|
||||||
String address, {
|
|
||||||
String code = 'CODE-1',
|
|
||||||
}) async {
|
|
||||||
await tester.enterText(inSheet, address);
|
|
||||||
await tester.tap(find.widgetWithText(FilledButton, 'Send code'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(
|
|
||||||
find.text("We mailed a code to $address. It's valid for 15 minutes."),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
await tester.enterText(inSheet, code);
|
|
||||||
await tester.tap(find.widgetWithText(FilledButton, 'Continue'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
}
|
|
||||||
|
|
||||||
testWidgets('an anonymous person continues with email: the address, then '
|
|
||||||
'the code mailed to it, and their progress is kept with their account, '
|
|
||||||
'which shows the address', (tester) async {
|
|
||||||
await openProfile(tester);
|
|
||||||
|
|
||||||
await tester.tap(find.text('Continue with email'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(
|
|
||||||
find.textContaining('keeps only a fingerprint of your address'),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
await signInWith(tester, 'anna@example.org');
|
|
||||||
|
|
||||||
expect(find.text('Signed in with anna@example.org.'), findsOneWidget);
|
|
||||||
expect(
|
|
||||||
find.text('Your progress is kept with your account.'),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
expect(find.text('Ways to sign in'), findsOneWidget);
|
|
||||||
expect(
|
|
||||||
find.widgetWithText(ListTile, 'anna@example.org'),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
expect(find.text('Add GitHub'), findsOneWidget);
|
|
||||||
expect(addresses.addresses, {'anonymous-1': 'anna@example.org'});
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('a wrong code says so, and a new one can be asked for', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
await openProfile(tester);
|
|
||||||
await tester.tap(find.text('Continue with email'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
await signInWith(tester, 'anna@example.org', code: 'CODE-9');
|
|
||||||
|
|
||||||
expect(find.text("That code isn't right."), findsOneWidget);
|
|
||||||
await tester.tap(find.text('Send a new code'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.text('A new code is on its way.'), findsOneWidget);
|
|
||||||
expect(auth.mailed, hasLength(2));
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('no email sign-in where the instance offers none', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
auth.offersEmail = false;
|
|
||||||
|
|
||||||
await openProfile(tester);
|
|
||||||
|
|
||||||
expect(find.text('Continue with email'), findsNothing);
|
|
||||||
expect(find.text('Sign in with GitHub'), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('someone who signs in with GitHub adds an email address', (
|
|
||||||
tester,
|
|
||||||
) async {
|
|
||||||
auth = FakeAuthGateway(
|
|
||||||
status: const SignedIn(authUserId: 'github-person', isIdentified: true),
|
|
||||||
)..gitHubPeople.add('github-person');
|
|
||||||
await openProfile(tester);
|
|
||||||
|
|
||||||
expect(find.text('Ways to sign in'), findsOneWidget);
|
|
||||||
expect(find.text('Add GitHub'), findsNothing);
|
|
||||||
await tester.tap(find.text('Add an email address'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await signInWith(tester, 'carla@example.org');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find.widgetWithText(ListTile, 'carla@example.org'),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
expect(find.text('Add an email address'), findsNothing);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets("someone identified whose address is another account's is "
|
|
||||||
'told so, and stays who they are', (tester) async {
|
|
||||||
auth =
|
|
||||||
FakeAuthGateway(
|
|
||||||
status: const SignedIn(
|
|
||||||
authUserId: 'github-person',
|
|
||||||
isIdentified: true,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
..gitHubPeople.add('github-person')
|
|
||||||
..addressOwners['ben@example.org'] = 'ben';
|
|
||||||
await openProfile(tester);
|
|
||||||
|
|
||||||
await tester.tap(find.text('Add an email address'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await signInWith(tester, 'ben@example.org');
|
|
||||||
|
|
||||||
expect(
|
|
||||||
find.text(
|
|
||||||
'ben@example.org already signs in to another account. To switch to '
|
|
||||||
'it, sign out first.',
|
|
||||||
),
|
|
||||||
findsOneWidget,
|
|
||||||
);
|
|
||||||
expect(auth.emailSwitches, 0);
|
|
||||||
expect(
|
|
||||||
auth.status,
|
|
||||||
const SignedIn(authUserId: 'github-person', isIdentified: true),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,127 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:halkyon_app/halkyon_app.dart';
|
||||||
|
import 'package:halkyon_app/testing.dart';
|
||||||
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
import '../../../support/pump_app.dart';
|
||||||
|
import '../../../support/test_dependencies.dart';
|
||||||
|
|
||||||
|
/// The profile's way to keep an account with a password and an
|
||||||
|
/// authenticator (ADR 0017), as halkyon_app's section shows it in Learn.
|
||||||
|
void main() {
|
||||||
|
late FakeAuthGateway auth;
|
||||||
|
late FakePasswordGateway passwords;
|
||||||
|
late MemoryAccountKeyStore keys;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
auth = FakeAuthGateway();
|
||||||
|
passwords = FakePasswordGateway(auth);
|
||||||
|
keys = MemoryAccountKeyStore();
|
||||||
|
});
|
||||||
|
|
||||||
|
Future<void> openProfile(WidgetTester tester) async {
|
||||||
|
tester.view.physicalSize = const Size(412, 2000);
|
||||||
|
tester.view.devicePixelRatio = 1;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
await pumpApp(
|
||||||
|
tester,
|
||||||
|
testDependencies(
|
||||||
|
authGateway: auth,
|
||||||
|
passwordGateway: passwords,
|
||||||
|
accountKeys: keys,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await openTab(tester, 'Profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
Finder field(String label) => find.widgetWithText(TextField, label);
|
||||||
|
|
||||||
|
Future<void> next(WidgetTester tester, String label) async {
|
||||||
|
final button = find.descendant(
|
||||||
|
of: find.byType(SheetScaffold),
|
||||||
|
matching: find.widgetWithText(FilledButton, label),
|
||||||
|
);
|
||||||
|
await tester.ensureVisible(button);
|
||||||
|
await tester.tap(button);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('an anonymous person keeps their account with a password: the '
|
||||||
|
'address, the phrase checked, the code, the authenticator', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await openProfile(tester);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Set up a password'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.enterText(field('Email address'), 'anna@example.org');
|
||||||
|
await next(tester, 'Continue');
|
||||||
|
await tester.enterText(field('New password'), 'correct horse');
|
||||||
|
await tester.enterText(field('New password again'), 'correct horse');
|
||||||
|
await next(tester, 'Continue');
|
||||||
|
await next(tester, "I've written it down");
|
||||||
|
final words = wordsOf(fakePhrase);
|
||||||
|
final labels = [
|
||||||
|
for (final element in find.byType(TextField).evaluate())
|
||||||
|
if ((element.widget as TextField).decoration?.labelText
|
||||||
|
case final label? when label.startsWith('Word '))
|
||||||
|
label,
|
||||||
|
];
|
||||||
|
for (final label in labels) {
|
||||||
|
await tester.enterText(
|
||||||
|
field(label),
|
||||||
|
words[int.parse(label.substring('Word '.length)) - 1],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await next(tester, 'Continue');
|
||||||
|
await tester.enterText(
|
||||||
|
field('Code from the email'),
|
||||||
|
passwords.codeFor('anna@example.org'),
|
||||||
|
);
|
||||||
|
await next(tester, 'Continue');
|
||||||
|
await tester.enterText(
|
||||||
|
field('Code from your authenticator app'),
|
||||||
|
fakeTotpCode,
|
||||||
|
);
|
||||||
|
await next(tester, 'Continue');
|
||||||
|
await next(tester, "I've kept them");
|
||||||
|
|
||||||
|
expect(
|
||||||
|
auth.status,
|
||||||
|
const SignedIn(authUserId: 'anonymous-1', isIdentified: true),
|
||||||
|
);
|
||||||
|
expect(find.text('Your progress is kept with your account.'), findsOne);
|
||||||
|
expect(keys.kept.keys, ['anonymous-1']);
|
||||||
|
expect(passwords.accounts['anna@example.org']!.finished, isTrue);
|
||||||
|
await tester.dragUntilVisible(
|
||||||
|
find.text('Change password'),
|
||||||
|
find.byType(ListView).last,
|
||||||
|
const Offset(0, -200),
|
||||||
|
);
|
||||||
|
expect(find.text('Change password'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('someone who signs in with GitHub is told when it ends, and '
|
||||||
|
'offered a password', (tester) async {
|
||||||
|
auth = FakeAuthGateway(
|
||||||
|
status: const SignedIn(authUserId: 'github-person', isIdentified: true),
|
||||||
|
)..gitHubPeople.add('github-person');
|
||||||
|
passwords = FakePasswordGateway(auth)
|
||||||
|
..gitHubPeople.add('github-person')
|
||||||
|
..gitHubEnds = DateTime.utc(2027, 1, 22);
|
||||||
|
await openProfile(tester);
|
||||||
|
|
||||||
|
expect(find.textContaining('Sign-in with GitHub ends on'), findsOneWidget);
|
||||||
|
expect(find.text('Set up a password'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('no password where the instance offers none', (tester) async {
|
||||||
|
passwords.offered = false;
|
||||||
|
|
||||||
|
await openProfile(tester);
|
||||||
|
|
||||||
|
expect(find.text('Set up a password'), findsNothing);
|
||||||
|
expect(find.text('Sign in with GitHub'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -121,7 +121,7 @@ void main() {
|
||||||
expect(find.text('Signed out'), findsOneWidget);
|
expect(find.text('Signed out'), findsOneWidget);
|
||||||
expect(find.text('Start on learn.example.org:'), findsOneWidget);
|
expect(find.text('Start on learn.example.org:'), findsOneWidget);
|
||||||
expect(find.text('Sign in with GitHub'), findsOneWidget);
|
expect(find.text('Sign in with GitHub'), findsOneWidget);
|
||||||
expect(find.text('Continue with email'), findsOneWidget);
|
expect(find.text('Create an account'), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.text('Start without an account'));
|
await tester.tap(find.text('Start without an account'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
@ -176,26 +176,6 @@ void main() {
|
||||||
expect(gateway.status, const SignedOut());
|
expect(gateway.status, const SignedOut());
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('from there, an email address starts without an account and '
|
|
||||||
'asks for the address on Profile', (tester) async {
|
|
||||||
await openSettings(tester);
|
|
||||||
await tester.tap(find.text('Sign out'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
await tester.tap(find.text('Sign out').last);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
await tester.tap(find.text('Continue with email'));
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
gateway.status,
|
|
||||||
const SignedIn(authUserId: 'anonymous-2', isIdentified: false),
|
|
||||||
);
|
|
||||||
// The email sign-in, over the Profile tab.
|
|
||||||
expect(find.text('Continue with email'), findsWidgets);
|
|
||||||
expect(find.byType(TextField), findsOneWidget);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWidgets('deleting the account asks first; Cancel keeps it', (
|
testWidgets('deleting the account asks first; Cancel keeps it', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,9 @@
|
||||||
/// halkyon_base (ADR 0014, decided 2026-10-19): builds up to 20 (v0.17.0) call
|
/// halkyon_base (ADR 0014, decided 2026-10-19): builds up to 20 (v0.17.0) call
|
||||||
/// endpoints that are gone and can't read the module's refusals, so the server
|
/// endpoints that are gone and can't read the module's refusals, so the server
|
||||||
/// with the module deploys only together with a release of build 21.
|
/// with the module deploys only together with a release of build 21.
|
||||||
const minimumAppBuild = 21;
|
///
|
||||||
|
/// 22 since codes by mail stopped signing in (ADR 0017, decided 2026-10-20):
|
||||||
|
/// builds up to 21 sign in with `emailCode.verifyCode` and
|
||||||
|
/// `emailSignIn.signIn`, which are gone; 22 signs in with a password and
|
||||||
|
/// TOTP.
|
||||||
|
const minimumAppBuild = 22;
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// The account: halkyon_app's section (whose it is, on which instance,
|
/// The account: halkyon_app's sections (whose it is, on which instance,
|
||||||
/// switching to the account on another, signing out and deleting it) in
|
/// switching to the account on another, signing out and deleting it; its
|
||||||
/// Todo's words where only Todo can say it; and About.
|
/// password and authenticators) in Todo's words where only Todo can say it;
|
||||||
|
/// and About.
|
||||||
Future<void> showAccountSheet(BuildContext context) => showAdaptiveSheet(
|
Future<void> showAccountSheet(BuildContext context) => showAdaptiveSheet(
|
||||||
context,
|
context,
|
||||||
builder: (context) => const _AccountSheet(),
|
builder: (context) => const _AccountSheet(),
|
||||||
|
|
@ -27,6 +28,12 @@ class const _AccountSheet() extends StatelessWidget {
|
||||||
signOutAnonymousMessage: l10n.accountSignOutAnonymousMessage,
|
signOutAnonymousMessage: l10n.accountSignOutAnonymousMessage,
|
||||||
deleteMessage: l10n.accountDeleteMessage,
|
deleteMessage: l10n.accountDeleteMessage,
|
||||||
),
|
),
|
||||||
|
// A password and an authenticator keep the account, and sign in
|
||||||
|
// on other devices and in the instance's other apps (ADR 0017).
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: AccountSecuritySection(),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(LucideIcons.info),
|
leading: const Icon(LucideIcons.info),
|
||||||
title: Text(l10n.aboutTitle),
|
title: Text(l10n.aboutTitle),
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,10 @@
|
||||||
/// - **The account and About**: `AccountTiles` for an app's settings,
|
/// - **The account and About**: `AccountTiles` for an app's settings,
|
||||||
/// `showAppAbout` with the source of what the instance runs, and
|
/// `showAppAbout` with the source of what the instance runs, and
|
||||||
/// `resolveAppLocale`, the language every Halkyon app picks.
|
/// `resolveAppLocale`, the language every Halkyon app picks.
|
||||||
/// - **Signing in**: anonymously, with GitHub or with a code by mail
|
/// - **Signing in**: anonymously, with GitHub (`AuthRepository` on
|
||||||
/// (`AuthRepository` on `ServerpodAuthGateway`), and the blocs and sheet
|
/// `ServerpodAuthGateway`), and with a password and TOTP (ADR 0017:
|
||||||
/// that switch a device to another account; with a password and TOTP
|
/// `PasswordRepository`, its sheets and `AccountSecuritySection`), the
|
||||||
/// (ADR 0017: `PasswordRepository`, its sheets and
|
/// account's keys on the device.
|
||||||
/// `AccountSecuritySection`), the account's keys on the device.
|
|
||||||
/// - **Sync**: `SyncService` on triggers, the changes still waiting, and
|
/// - **Sync**: `SyncService` on triggers, the changes still waiting, and
|
||||||
/// where it stands (`SyncStatusBloc`, `SyncStatusCard`, `AppMismatchBanner`,
|
/// where it stands (`SyncStatusBloc`, `SyncStatusCard`, `AppMismatchBanner`,
|
||||||
/// `SyncTroubleBadge`).
|
/// `SyncTroubleBadge`).
|
||||||
|
|
@ -31,7 +30,6 @@ export 'src/app/app_locale.dart';
|
||||||
export 'src/app/app_scope.dart';
|
export 'src/app/app_scope.dart';
|
||||||
export 'src/app/instance_gate.dart';
|
export 'src/app/instance_gate.dart';
|
||||||
export 'src/app/person_scope.dart';
|
export 'src/app/person_scope.dart';
|
||||||
export 'src/auth/bloc/email_sign_in_bloc.dart';
|
|
||||||
export 'src/auth/bloc/github_sign_in_bloc.dart';
|
export 'src/auth/bloc/github_sign_in_bloc.dart';
|
||||||
export 'src/auth/bloc/session_bloc.dart';
|
export 'src/auth/bloc/session_bloc.dart';
|
||||||
export 'src/auth/bloc/sign_in_ways_cubit.dart';
|
export 'src/auth/bloc/sign_in_ways_cubit.dart';
|
||||||
|
|
@ -42,13 +40,11 @@ export 'src/auth/data/serverpod_auth_gateway.dart';
|
||||||
export 'src/auth/data/sign_in_addresses.dart';
|
export 'src/auth/data/sign_in_addresses.dart';
|
||||||
export 'src/auth/data/signed_out_mark.dart';
|
export 'src/auth/data/signed_out_mark.dart';
|
||||||
export 'src/auth/model/auth_status.dart';
|
export 'src/auth/model/auth_status.dart';
|
||||||
export 'src/auth/model/email_sign_in.dart';
|
|
||||||
export 'src/auth/model/github_link_outcome.dart';
|
export 'src/auth/model/github_link_outcome.dart';
|
||||||
export 'src/auth/model/github_sign_in.dart';
|
export 'src/auth/model/github_sign_in.dart';
|
||||||
export 'src/auth/model/session_grant.dart';
|
export 'src/auth/model/session_grant.dart';
|
||||||
export 'src/auth/model/sign_in_ways.dart';
|
export 'src/auth/model/sign_in_ways.dart';
|
||||||
export 'src/auth/widget/account_tiles.dart';
|
export 'src/auth/widget/account_tiles.dart';
|
||||||
export 'src/auth/widget/email_sign_in_sheet.dart';
|
|
||||||
export 'src/auth/widget/session_gate.dart';
|
export 'src/auth/widget/session_gate.dart';
|
||||||
export 'src/auth/widget/start_screen.dart';
|
export 'src/auth/widget/start_screen.dart';
|
||||||
export 'src/base/base_client.dart';
|
export 'src/base/base_client.dart';
|
||||||
|
|
|
||||||
|
|
@ -42,36 +42,11 @@
|
||||||
"startSignedOut": "Abgemeldet",
|
"startSignedOut": "Abgemeldet",
|
||||||
"startOn": "Starte auf {instance}:",
|
"startOn": "Starte auf {instance}:",
|
||||||
"startWithGitHub": "Mit GitHub anmelden",
|
"startWithGitHub": "Mit GitHub anmelden",
|
||||||
"startWithEmail": "Mit E-Mail fortfahren",
|
|
||||||
"startWithoutAccount": "Ohne Konto starten",
|
"startWithoutAccount": "Ohne Konto starten",
|
||||||
"startAnonymous": "Dein Fortschritt wird auf diesem Gerät gespeichert. Melde dich an, um ihn zu behalten.",
|
"startAnonymous": "Dein Fortschritt wird auf diesem Gerät gespeichert. Melde dich an, um ihn zu behalten.",
|
||||||
"startGitHubFailed": "Anmeldung mit GitHub fehlgeschlagen.",
|
"startGitHubFailed": "Anmeldung mit GitHub fehlgeschlagen.",
|
||||||
"startNoGitHubAccount": "Noch kein Konto hier meldet sich mit diesem GitHub-Konto an.",
|
"startNoGitHubAccount": "Noch kein Konto hier meldet sich mit diesem GitHub-Konto an.",
|
||||||
"startOtherInstance": "Andere Instanz verwenden",
|
"startOtherInstance": "Andere Instanz verwenden",
|
||||||
"emailSignInTitle": "Mit E-Mail fortfahren",
|
|
||||||
"emailSignInIntro": "Wir schicken dir einen Code zum Anmelden. Diese Instanz behält nur einen Fingerabdruck deiner Adresse, nie die Adresse selbst, und schreibt ihr nur, wenn du einen Code anforderst.",
|
|
||||||
"emailSignInAddress": "E-Mail-Adresse",
|
|
||||||
"emailSignInSend": "Code senden",
|
|
||||||
"emailSignInCodeSent": "Wir haben einen Code an {address} geschickt. Er gilt 15 Minuten.",
|
|
||||||
"emailSignInResent": "Ein neuer Code ist unterwegs.",
|
|
||||||
"emailSignInCode": "Code",
|
|
||||||
"emailSignInContinue": "Weiter",
|
|
||||||
"emailSignInResend": "Neuen Code senden",
|
|
||||||
"emailSignInOtherAddress": "Andere Adresse verwenden",
|
|
||||||
"emailSignInSwitchTitle": "Zum Konto von {address} wechseln?",
|
|
||||||
"emailSignInSwitchConfirm": "Wechseln",
|
|
||||||
"emailSignInSwitchStay": "Anonym bleiben",
|
|
||||||
"emailSignInTaken": "Mit {address} meldet sich schon ein anderes Konto an. Um zu ihm zu wechseln, melde dich zuerst ab.",
|
|
||||||
"emailSignInUnavailable": "Diese Instanz bietet keine Anmeldung mit E-Mail an.",
|
|
||||||
"emailSignInNotAnAddress": "Das ist keine E-Mail-Adresse.",
|
|
||||||
"emailSignInTooManyCodes": "Zu viele Codes in der letzten Stunde. Versuche es später noch einmal.",
|
|
||||||
"emailSignInWrongCode": "Dieser Code stimmt nicht.",
|
|
||||||
"emailSignInTooManyTries": "Zu viele falsche Codes. Fordere einen neuen an.",
|
|
||||||
"emailSignInCodeExpired": "Dieser Code gilt nicht mehr. Fordere einen neuen an.",
|
|
||||||
"emailSignInOtherAddressLinked": "Du meldest dich schon mit einer anderen Adresse an.",
|
|
||||||
"emailSignInMailFailed": "Die Instanz konnte die Mail nicht senden. Versuche es später noch einmal.",
|
|
||||||
"emailSignInUsePassword": "Dieses Konto meldet sich mit seinem Passwort und einer Authenticator-App an.",
|
|
||||||
"emailSignInFailed": "Die Instanz war nicht erreichbar. Versuche es noch einmal.",
|
|
||||||
"syncTitle": "Synchronisierung",
|
"syncTitle": "Synchronisierung",
|
||||||
"syncNow": "Jetzt synchronisieren",
|
"syncNow": "Jetzt synchronisieren",
|
||||||
"syncStateSyncing": "Wird synchronisiert…",
|
"syncStateSyncing": "Wird synchronisiert…",
|
||||||
|
|
|
||||||
|
|
@ -142,57 +142,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"startWithGitHub": "Sign in with GitHub",
|
"startWithGitHub": "Sign in with GitHub",
|
||||||
"startWithEmail": "Continue with email",
|
|
||||||
"startWithoutAccount": "Start without an account",
|
"startWithoutAccount": "Start without an account",
|
||||||
"startAnonymous": "Progress is saved on this device. Sign in to keep it.",
|
"startAnonymous": "Progress is saved on this device. Sign in to keep it.",
|
||||||
"startGitHubFailed": "Couldn't sign in with GitHub.",
|
"startGitHubFailed": "Couldn't sign in with GitHub.",
|
||||||
"startNoGitHubAccount": "No account here signs in with this GitHub account yet.",
|
"startNoGitHubAccount": "No account here signs in with this GitHub account yet.",
|
||||||
"startOtherInstance": "Use another instance",
|
"startOtherInstance": "Use another instance",
|
||||||
"emailSignInTitle": "Continue with email",
|
|
||||||
"emailSignInIntro": "We mail you a code to sign in. This instance keeps only a fingerprint of your address, never the address itself, and writes to it only when you ask for a code.",
|
|
||||||
"emailSignInAddress": "Email address",
|
|
||||||
"emailSignInSend": "Send code",
|
|
||||||
"emailSignInCodeSent": "We mailed a code to {address}. It's valid for 15 minutes.",
|
|
||||||
"@emailSignInCodeSent": {
|
|
||||||
"placeholders": {
|
|
||||||
"address": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"emailSignInResent": "A new code is on its way.",
|
|
||||||
"emailSignInCode": "Code",
|
|
||||||
"emailSignInContinue": "Continue",
|
|
||||||
"emailSignInResend": "Send a new code",
|
|
||||||
"emailSignInOtherAddress": "Use another address",
|
|
||||||
"emailSignInSwitchTitle": "Switch to the account of {address}?",
|
|
||||||
"@emailSignInSwitchTitle": {
|
|
||||||
"placeholders": {
|
|
||||||
"address": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"emailSignInSwitchConfirm": "Switch",
|
|
||||||
"emailSignInSwitchStay": "Stay anonymous",
|
|
||||||
"emailSignInTaken": "{address} already signs in to another account. To switch to it, sign out first.",
|
|
||||||
"@emailSignInTaken": {
|
|
||||||
"placeholders": {
|
|
||||||
"address": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"emailSignInUnavailable": "This instance doesn't offer signing in with email.",
|
|
||||||
"emailSignInNotAnAddress": "This isn't an email address.",
|
|
||||||
"emailSignInTooManyCodes": "Too many codes in the last hour. Try again later.",
|
|
||||||
"emailSignInWrongCode": "That code isn't right.",
|
|
||||||
"emailSignInTooManyTries": "Too many wrong codes. Ask for a new one.",
|
|
||||||
"emailSignInCodeExpired": "This code is no longer valid. Ask for a new one.",
|
|
||||||
"emailSignInOtherAddressLinked": "You already sign in with another address.",
|
|
||||||
"emailSignInMailFailed": "The instance couldn't send the mail. Try again later.",
|
|
||||||
"emailSignInUsePassword": "This account signs in with its password and an authenticator app.",
|
|
||||||
"emailSignInFailed": "Couldn't reach the instance. Try again.",
|
|
||||||
"syncTitle": "Sync",
|
"syncTitle": "Sync",
|
||||||
"syncNow": "Sync now",
|
"syncNow": "Sync now",
|
||||||
"syncStateSyncing": "Syncing…",
|
"syncStateSyncing": "Syncing…",
|
||||||
|
|
|
||||||
|
|
@ -355,12 +355,6 @@ abstract class HalkyonAppLocalizations {
|
||||||
/// **'Sign in with GitHub'**
|
/// **'Sign in with GitHub'**
|
||||||
String get startWithGitHub;
|
String get startWithGitHub;
|
||||||
|
|
||||||
/// No description provided for @startWithEmail.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Continue with email'**
|
|
||||||
String get startWithEmail;
|
|
||||||
|
|
||||||
/// No description provided for @startWithoutAccount.
|
/// No description provided for @startWithoutAccount.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -391,150 +385,6 @@ abstract class HalkyonAppLocalizations {
|
||||||
/// **'Use another instance'**
|
/// **'Use another instance'**
|
||||||
String get startOtherInstance;
|
String get startOtherInstance;
|
||||||
|
|
||||||
/// No description provided for @emailSignInTitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Continue with email'**
|
|
||||||
String get emailSignInTitle;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInIntro.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'We mail you a code to sign in. This instance keeps only a fingerprint of your address, never the address itself, and writes to it only when you ask for a code.'**
|
|
||||||
String get emailSignInIntro;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInAddress.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Email address'**
|
|
||||||
String get emailSignInAddress;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInSend.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Send code'**
|
|
||||||
String get emailSignInSend;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInCodeSent.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'We mailed a code to {address}. It\'s valid for 15 minutes.'**
|
|
||||||
String emailSignInCodeSent(String address);
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInResent.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'A new code is on its way.'**
|
|
||||||
String get emailSignInResent;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInCode.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Code'**
|
|
||||||
String get emailSignInCode;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInContinue.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Continue'**
|
|
||||||
String get emailSignInContinue;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInResend.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Send a new code'**
|
|
||||||
String get emailSignInResend;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInOtherAddress.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Use another address'**
|
|
||||||
String get emailSignInOtherAddress;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInSwitchTitle.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Switch to the account of {address}?'**
|
|
||||||
String emailSignInSwitchTitle(String address);
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInSwitchConfirm.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Switch'**
|
|
||||||
String get emailSignInSwitchConfirm;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInSwitchStay.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Stay anonymous'**
|
|
||||||
String get emailSignInSwitchStay;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInTaken.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'{address} already signs in to another account. To switch to it, sign out first.'**
|
|
||||||
String emailSignInTaken(String address);
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInUnavailable.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'This instance doesn\'t offer signing in with email.'**
|
|
||||||
String get emailSignInUnavailable;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInNotAnAddress.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'This isn\'t an email address.'**
|
|
||||||
String get emailSignInNotAnAddress;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInTooManyCodes.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Too many codes in the last hour. Try again later.'**
|
|
||||||
String get emailSignInTooManyCodes;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInWrongCode.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'That code isn\'t right.'**
|
|
||||||
String get emailSignInWrongCode;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInTooManyTries.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Too many wrong codes. Ask for a new one.'**
|
|
||||||
String get emailSignInTooManyTries;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInCodeExpired.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'This code is no longer valid. Ask for a new one.'**
|
|
||||||
String get emailSignInCodeExpired;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInOtherAddressLinked.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'You already sign in with another address.'**
|
|
||||||
String get emailSignInOtherAddressLinked;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInMailFailed.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'The instance couldn\'t send the mail. Try again later.'**
|
|
||||||
String get emailSignInMailFailed;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInUsePassword.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'This account signs in with its password and an authenticator app.'**
|
|
||||||
String get emailSignInUsePassword;
|
|
||||||
|
|
||||||
/// No description provided for @emailSignInFailed.
|
|
||||||
///
|
|
||||||
/// In en, this message translates to:
|
|
||||||
/// **'Couldn\'t reach the instance. Try again.'**
|
|
||||||
String get emailSignInFailed;
|
|
||||||
|
|
||||||
/// No description provided for @syncTitle.
|
/// No description provided for @syncTitle.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -165,9 +165,6 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get startWithGitHub => 'Mit GitHub anmelden';
|
String get startWithGitHub => 'Mit GitHub anmelden';
|
||||||
|
|
||||||
@override
|
|
||||||
String get startWithEmail => 'Mit E-Mail fortfahren';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get startWithoutAccount => 'Ohne Konto starten';
|
String get startWithoutAccount => 'Ohne Konto starten';
|
||||||
|
|
||||||
|
|
@ -185,93 +182,6 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get startOtherInstance => 'Andere Instanz verwenden';
|
String get startOtherInstance => 'Andere Instanz verwenden';
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInTitle => 'Mit E-Mail fortfahren';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInIntro =>
|
|
||||||
'Wir schicken dir einen Code zum Anmelden. Diese Instanz behält nur einen Fingerabdruck deiner Adresse, nie die Adresse selbst, und schreibt ihr nur, wenn du einen Code anforderst.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInAddress => 'E-Mail-Adresse';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInSend => 'Code senden';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String emailSignInCodeSent(String address) {
|
|
||||||
return 'Wir haben einen Code an $address geschickt. Er gilt 15 Minuten.';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInResent => 'Ein neuer Code ist unterwegs.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInCode => 'Code';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInContinue => 'Weiter';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInResend => 'Neuen Code senden';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInOtherAddress => 'Andere Adresse verwenden';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String emailSignInSwitchTitle(String address) {
|
|
||||||
return 'Zum Konto von $address wechseln?';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInSwitchConfirm => 'Wechseln';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInSwitchStay => 'Anonym bleiben';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String emailSignInTaken(String address) {
|
|
||||||
return 'Mit $address meldet sich schon ein anderes Konto an. Um zu ihm zu wechseln, melde dich zuerst ab.';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInUnavailable =>
|
|
||||||
'Diese Instanz bietet keine Anmeldung mit E-Mail an.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInNotAnAddress => 'Das ist keine E-Mail-Adresse.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInTooManyCodes =>
|
|
||||||
'Zu viele Codes in der letzten Stunde. Versuche es später noch einmal.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInWrongCode => 'Dieser Code stimmt nicht.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInTooManyTries =>
|
|
||||||
'Zu viele falsche Codes. Fordere einen neuen an.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInCodeExpired =>
|
|
||||||
'Dieser Code gilt nicht mehr. Fordere einen neuen an.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInOtherAddressLinked =>
|
|
||||||
'Du meldest dich schon mit einer anderen Adresse an.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInMailFailed =>
|
|
||||||
'Die Instanz konnte die Mail nicht senden. Versuche es später noch einmal.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInUsePassword =>
|
|
||||||
'Dieses Konto meldet sich mit seinem Passwort und einer Authenticator-App an.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInFailed =>
|
|
||||||
'Die Instanz war nicht erreichbar. Versuche es noch einmal.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get syncTitle => 'Synchronisierung';
|
String get syncTitle => 'Synchronisierung';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -163,9 +163,6 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get startWithGitHub => 'Sign in with GitHub';
|
String get startWithGitHub => 'Sign in with GitHub';
|
||||||
|
|
||||||
@override
|
|
||||||
String get startWithEmail => 'Continue with email';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get startWithoutAccount => 'Start without an account';
|
String get startWithoutAccount => 'Start without an account';
|
||||||
|
|
||||||
|
|
@ -183,92 +180,6 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get startOtherInstance => 'Use another instance';
|
String get startOtherInstance => 'Use another instance';
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInTitle => 'Continue with email';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInIntro =>
|
|
||||||
'We mail you a code to sign in. This instance keeps only a fingerprint of your address, never the address itself, and writes to it only when you ask for a code.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInAddress => 'Email address';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInSend => 'Send code';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String emailSignInCodeSent(String address) {
|
|
||||||
return 'We mailed a code to $address. It\'s valid for 15 minutes.';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInResent => 'A new code is on its way.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInCode => 'Code';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInContinue => 'Continue';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInResend => 'Send a new code';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInOtherAddress => 'Use another address';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String emailSignInSwitchTitle(String address) {
|
|
||||||
return 'Switch to the account of $address?';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInSwitchConfirm => 'Switch';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInSwitchStay => 'Stay anonymous';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String emailSignInTaken(String address) {
|
|
||||||
return '$address already signs in to another account. To switch to it, sign out first.';
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInUnavailable =>
|
|
||||||
'This instance doesn\'t offer signing in with email.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInNotAnAddress => 'This isn\'t an email address.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInTooManyCodes =>
|
|
||||||
'Too many codes in the last hour. Try again later.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInWrongCode => 'That code isn\'t right.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInTooManyTries =>
|
|
||||||
'Too many wrong codes. Ask for a new one.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInCodeExpired =>
|
|
||||||
'This code is no longer valid. Ask for a new one.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInOtherAddressLinked =>
|
|
||||||
'You already sign in with another address.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInMailFailed =>
|
|
||||||
'The instance couldn\'t send the mail. Try again later.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInUsePassword =>
|
|
||||||
'This account signs in with its password and an authenticator app.';
|
|
||||||
|
|
||||||
@override
|
|
||||||
String get emailSignInFailed => 'Couldn\'t reach the instance. Try again.';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get syncTitle => 'Sync';
|
String get syncTitle => 'Sync';
|
||||||
|
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue