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:
Tizian 「ティツィアーン」 2026-10-20 19:31:49 +02:00
commit ae57cbe864
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
68 changed files with 429 additions and 3835 deletions

View file

@ -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.",

View file

@ -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.",

View file

@ -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:

View file

@ -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';

View file

@ -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';

View file

@ -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',
),
), ),
], ],
), ),

View file

@ -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

View file

@ -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';

View file

@ -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;
}

View file

@ -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),
);
});
}

View file

@ -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);
});
}

View file

@ -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 {

View file

@ -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;

View file

@ -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),

View file

@ -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';

View file

@ -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…",

View file

@ -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…",

View file

@ -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:

View file

@ -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';

View file

@ -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