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.",
"profileSwitchStay": "Anonym bleiben",
"profileSwitchConfirm": "Wechseln",
"profileContinueWithEmail": "Mit E-Mail fortfahren",
"profileSignInWays": "Anmeldewege",
"profileWayGitHub": "GitHub",
"profileWayEmail": "E-Mail-Adresse",
"profileWayLinked": "Meldet dich an",
"profileAddGitHub": "GitHub hinzufügen",
"profileAddEmail": "E-Mail-Adresse hinzufügen",
"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.",
"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",
"profileSheetTitle": "Profil",
"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.",
"profileSwitchStay": "Stay anonymous",
"profileSwitchConfirm": "Switch",
"profileContinueWithEmail": "Continue with email",
"profileSignInWays": "Ways to sign in",
"profileWayGitHub": "GitHub",
"profileWayEmail": "Email address",
"profileWayLinked": "Signs you in",
"profileAddGitHub": "Add GitHub",
"profileAddEmail": "Add an email address",
"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.",
"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",
"profileSheetTitle": "Profile",
"profileSheetNotFound": "Nobody on this instance has this handle.",

View file

@ -2872,12 +2872,6 @@ abstract class AppLocalizations {
/// **'Switch'**
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.
///
/// In en, this message translates to:
@ -2908,156 +2902,18 @@ abstract class AppLocalizations {
/// **'Add GitHub'**
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.
///
/// In en, this message translates to:
/// **'This GitHub account already signs in to another account.'**
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.
///
/// 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.'**
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.
///
/// In en, this message translates to:

View file

@ -1741,9 +1741,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get profileSwitchConfirm => 'Wechseln';
@override
String get profileContinueWithEmail => 'Mit E-Mail fortfahren';
@override
String get profileSignInWays => 'Anmeldewege';
@ -1759,99 +1756,14 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get profileAddGitHub => 'GitHub hinzufügen';
@override
String get profileAddEmail => 'E-Mail-Adresse hinzufügen';
@override
String get profileGitHubTaken =>
'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
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.';
@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
String get publicProfileTitle => 'Öffentliches Profil';

View file

@ -1725,9 +1725,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get profileSwitchConfirm => 'Switch';
@override
String get profileContinueWithEmail => 'Continue with email';
@override
String get profileSignInWays => 'Ways to sign in';
@ -1743,98 +1740,14 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get profileAddGitHub => 'Add GitHub';
@override
String get profileAddEmail => 'Add an email address';
@override
String get profileGitHubTaken =>
'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
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.';
@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
String get publicProfileTitle => 'Public profile';

View file

@ -189,10 +189,7 @@ GoRouter buildRouter({String initialLocation = RoutePaths.home}) => GoRouter(
routes: [
GoRoute(
path: RoutePaths.profile,
builder: (context, state) => ProfileScreen(
signsInWithEmail:
state.uri.queryParameters['signIn'] == 'email',
),
builder: (context, state) => const ProfileScreen(),
),
],
),

View file

@ -43,12 +43,7 @@ class const PersonApp({
class _PersonAppState() extends State<PersonApp> {
late final GoRouter _router = buildRouter(
// Started with an email address: Profile asks for it at once.
initialLocation: switch (context.read<SessionBloc>().state) {
SessionStateActive(signsInWithEmail: true) =>
RoutePaths.profileEmailSignIn,
_ => widget.initialLocation,
},
initialLocation: widget.initialLocation,
);
@override

View file

@ -13,10 +13,6 @@ abstract final class RoutePaths() {
/// The profile tab.
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.
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';
/// 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
/// address to keep an anonymous one, and the ways an identified person signs
/// in) and the person's statistics, which anonymous people have too.
/// Settings are behind the gear.
class const ProfileScreen({
/// 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 {
/// profile (what the account means, keeping an anonymous one with GitHub or
/// a password, the ways an identified person signs in, and their password
/// and authenticators, ADR 0017) and the person's statistics, which
/// anonymous people have too. Settings are behind the gear.
class const ProfileScreen({super.key}) extends StatelessWidget {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
@ -42,8 +37,6 @@ class const ProfileScreen({
SignInWaysCubit(Dependencies.of(context).auth)..load(),
),
],
child: _AsksForEmail(
asks: signsInWithEmail,
child: TopGradient(
color: Brand.primary,
background: Theme.of(context).colorScheme.surfaceContainerLow,
@ -84,7 +77,6 @@ class const ProfileScreen({
),
),
),
),
);
}
}
@ -179,7 +171,9 @@ class const _Account() extends StatelessWidget {
if (isIdentified) const _SignInWays(),
if (!isIdentified) ...[
const _GitHubSignIn(),
const _EmailSignInButton(),
const SizedBox(height: 16),
// A password keeps the account too (ADR 0017).
const AccountSecuritySection(),
const SizedBox(height: 16),
Text(
l10n.publicProfileAnonymousHint,
@ -212,58 +206,9 @@ bool _identifiedIn(SessionState state) => switch (state) {
_ => false,
};
/// Signs in with an email address in its sheet; once the address is the
/// person's, says so and shows it among the ways they sign in.
Future<void> _signInWithEmail(BuildContext context) async {
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.
/// How an identified person signs in: GitHub, with a way to add it where
/// the instance offers it, and an email address, with the password and
/// authenticators beneath.
class const _SignInWays() extends StatelessWidget {
@override
Widget build(BuildContext context) {
@ -313,17 +258,15 @@ class const _SignInWays() extends StatelessWidget {
Text(l10n.profileGitHubTaken, style: note),
if (gitHub case GitHubSignInStateReady(failed: true))
Text(l10n.profileSignInFailed, style: note),
if (ways.ways.email || ways.offersEmail)
if (ways.ways.email)
way(
l10n.profileWayEmail,
linkedAs: ways.ways.email
? ways.emailAddress ?? l10n.profileWayLinked
: null,
add: FilledButton.tonal(
onPressed: busy ? null : () => _signInWithEmail(context),
child: Text(l10n.profileAddEmail),
),
linkedAs: ways.emailAddress ?? l10n.profileWayLinked,
),
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;
}
/// 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('Start on learn.example.org:'), 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.pumpAndSettle();
@ -176,26 +176,6 @@ void main() {
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', (
tester,
) async {

View file

@ -12,4 +12,9 @@
/// 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
/// 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:material_ui/material_ui.dart';
/// The account: halkyon_app's section (whose it is, on which instance,
/// switching to the account on another, signing out and deleting it) in
/// Todo's words where only Todo can say it; and About.
/// The account: halkyon_app's sections (whose it is, on which instance,
/// switching to the account on another, signing out and deleting it; its
/// password and authenticators) in Todo's words where only Todo can say it;
/// and About.
Future<void> showAccountSheet(BuildContext context) => showAdaptiveSheet(
context,
builder: (context) => const _AccountSheet(),
@ -27,6 +28,12 @@ class const _AccountSheet() extends StatelessWidget {
signOutAnonymousMessage: l10n.accountSignOutAnonymousMessage,
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(
leading: const Icon(LucideIcons.info),
title: Text(l10n.aboutTitle),

View file

@ -11,11 +11,10 @@
/// - **The account and About**: `AccountTiles` for an app's settings,
/// `showAppAbout` with the source of what the instance runs, and
/// `resolveAppLocale`, the language every Halkyon app picks.
/// - **Signing in**: anonymously, with GitHub or with a code by mail
/// (`AuthRepository` on `ServerpodAuthGateway`), and the blocs and sheet
/// that switch a device to another account; with a password and TOTP
/// (ADR 0017: `PasswordRepository`, its sheets and
/// `AccountSecuritySection`), the account's keys on the device.
/// - **Signing in**: anonymously, with GitHub (`AuthRepository` on
/// `ServerpodAuthGateway`), and with a password and TOTP (ADR 0017:
/// `PasswordRepository`, its sheets and `AccountSecuritySection`), the
/// account's keys on the device.
/// - **Sync**: `SyncService` on triggers, the changes still waiting, and
/// where it stands (`SyncStatusBloc`, `SyncStatusCard`, `AppMismatchBanner`,
/// `SyncTroubleBadge`).
@ -31,7 +30,6 @@ export 'src/app/app_locale.dart';
export 'src/app/app_scope.dart';
export 'src/app/instance_gate.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/session_bloc.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/signed_out_mark.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_sign_in.dart';
export 'src/auth/model/session_grant.dart';
export 'src/auth/model/sign_in_ways.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/start_screen.dart';
export 'src/base/base_client.dart';

View file

@ -42,36 +42,11 @@
"startSignedOut": "Abgemeldet",
"startOn": "Starte auf {instance}:",
"startWithGitHub": "Mit GitHub anmelden",
"startWithEmail": "Mit E-Mail fortfahren",
"startWithoutAccount": "Ohne Konto starten",
"startAnonymous": "Dein Fortschritt wird auf diesem Gerät gespeichert. Melde dich an, um ihn zu behalten.",
"startGitHubFailed": "Anmeldung mit GitHub fehlgeschlagen.",
"startNoGitHubAccount": "Noch kein Konto hier meldet sich mit diesem GitHub-Konto an.",
"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",
"syncNow": "Jetzt synchronisieren",
"syncStateSyncing": "Wird synchronisiert…",

View file

@ -142,57 +142,11 @@
}
},
"startWithGitHub": "Sign in with GitHub",
"startWithEmail": "Continue with email",
"startWithoutAccount": "Start without an account",
"startAnonymous": "Progress is saved on this device. Sign in to keep it.",
"startGitHubFailed": "Couldn't sign in with GitHub.",
"startNoGitHubAccount": "No account here signs in with this GitHub account yet.",
"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",
"syncNow": "Sync now",
"syncStateSyncing": "Syncing…",

View file

@ -355,12 +355,6 @@ abstract class HalkyonAppLocalizations {
/// **'Sign in with GitHub'**
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.
///
/// In en, this message translates to:
@ -391,150 +385,6 @@ abstract class HalkyonAppLocalizations {
/// **'Use another instance'**
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.
///
/// In en, this message translates to:

View file

@ -165,9 +165,6 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
@override
String get startWithGitHub => 'Mit GitHub anmelden';
@override
String get startWithEmail => 'Mit E-Mail fortfahren';
@override
String get startWithoutAccount => 'Ohne Konto starten';
@ -185,93 +182,6 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
@override
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
String get syncTitle => 'Synchronisierung';

View file

@ -163,9 +163,6 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
@override
String get startWithGitHub => 'Sign in with GitHub';
@override
String get startWithEmail => 'Continue with email';
@override
String get startWithoutAccount => 'Start without an account';
@ -183,92 +180,6 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
@override
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
String get syncTitle => 'Sync';

Some files were not shown because too many files have changed in this diff Show more