feat(app): adding SimpleLogin to an account (ADR 0020, D21, D23)
halkyon_app reads where the device's person stands with SimpleLogin (nothing to show, linked, or missing) and shows it in SimpleLoginSection, which each app places above the password part: Add SimpleLogin, once added one more way in. GitHub is added to nobody where SimpleLogin is offered, and Learn's Profile offers an anonymous person SimpleLogin alone there (D23). No cut-off day, reminder or banner (D24): with the developer the only person on learn.halkyon.eu, the cut-off is the operator taking GitHub's OAuth app out of the deploy's settings once that account has SimpleLogin. ADR 0020 records it and drops step 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
3a93dc4fd8
commit
fad12b4411
9 changed files with 829 additions and 77 deletions
|
|
@ -470,6 +470,7 @@
|
|||
"profileSettingsTooltip": "Einstellungen",
|
||||
"profileIdentified": "Dein Fortschritt wird in deinem Konto gespeichert.",
|
||||
"profileAnonymous": "Dein Fortschritt wird auf diesem Gerät gespeichert. Melde dich an, um ihn zu behalten.",
|
||||
"profileAnonymousSimpleLogin": "Dein Fortschritt wird auf diesem Gerät gespeichert. Füge SimpleLogin hinzu, um ihn zu behalten.",
|
||||
"profileSignInWithGitHub": "Mit GitHub anmelden",
|
||||
"profileSignInFailed": "Anmeldung mit GitHub fehlgeschlagen.",
|
||||
"profileContinueInBrowser": "Fahre in deinem Browser fort.",
|
||||
|
|
|
|||
|
|
@ -535,6 +535,7 @@
|
|||
"profileSettingsTooltip": "Settings",
|
||||
"profileIdentified": "Your progress is kept with your account.",
|
||||
"profileAnonymous": "Progress is saved on this device. Sign in to keep it.",
|
||||
"profileAnonymousSimpleLogin": "Progress is saved on this device. Add SimpleLogin to keep it.",
|
||||
"profileSignInWithGitHub": "Sign in with GitHub",
|
||||
"profileSignInFailed": "Couldn't sign in with GitHub.",
|
||||
"profileContinueInBrowser": "Continue in your browser.",
|
||||
|
|
|
|||
|
|
@ -2830,6 +2830,12 @@ abstract class AppLocalizations {
|
|||
/// **'Progress is saved on this device. Sign in to keep it.'**
|
||||
String get profileAnonymous;
|
||||
|
||||
/// No description provided for @profileAnonymousSimpleLogin.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Progress is saved on this device. Add SimpleLogin to keep it.'**
|
||||
String get profileAnonymousSimpleLogin;
|
||||
|
||||
/// No description provided for @profileSignInWithGitHub.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1719,6 +1719,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String get profileAnonymous =>
|
||||
'Dein Fortschritt wird auf diesem Gerät gespeichert. Melde dich an, um ihn zu behalten.';
|
||||
|
||||
@override
|
||||
String get profileAnonymousSimpleLogin =>
|
||||
'Dein Fortschritt wird auf diesem Gerät gespeichert. Füge SimpleLogin hinzu, um ihn zu behalten.';
|
||||
|
||||
@override
|
||||
String get profileSignInWithGitHub => 'Mit GitHub anmelden';
|
||||
|
||||
|
|
|
|||
|
|
@ -1703,6 +1703,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String get profileAnonymous =>
|
||||
'Progress is saved on this device. Sign in to keep it.';
|
||||
|
||||
@override
|
||||
String get profileAnonymousSimpleLogin =>
|
||||
'Progress is saved on this device. Add SimpleLogin to keep it.';
|
||||
|
||||
@override
|
||||
String get profileSignInWithGitHub => 'Sign in with GitHub';
|
||||
|
||||
|
|
|
|||
|
|
@ -342,6 +342,7 @@ class Dependencies({
|
|||
onDispose: () async {
|
||||
await deviceDictionaries.dispose();
|
||||
await passwords.dispose();
|
||||
await simpleLogin.dispose();
|
||||
await authGateway.dispose();
|
||||
client.close();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -155,34 +155,40 @@ class const _Account() extends StatelessWidget {
|
|||
),
|
||||
children: [
|
||||
SectionCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
l10n.settingsAccount,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
isIdentified
|
||||
? l10n.profileIdentified
|
||||
: l10n.profileAnonymous,
|
||||
),
|
||||
if (isIdentified) const _SignInWays(),
|
||||
if (!isIdentified) ...[
|
||||
const _GitHubSignIn(),
|
||||
const SizedBox(height: 16),
|
||||
// A password keeps the account too (ADR 0017).
|
||||
const AccountSecuritySection(),
|
||||
const SizedBox(height: 16),
|
||||
// Where the person stands with SimpleLogin (ADR 0020, D21,
|
||||
// D23), read once for the card's parts that show it.
|
||||
child: SimpleLoginStandingScope(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
l10n.publicProfileAnonymousHint,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
l10n.settingsAccount,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_AccountLine(identified: isIdentified),
|
||||
if (isIdentified) const _SignInWays(),
|
||||
if (!isIdentified) ...[
|
||||
const _GitHubSignIn(),
|
||||
// SimpleLogin keeps the account, where the instance
|
||||
// offers it.
|
||||
const SimpleLoginSection(gapAbove: 16),
|
||||
const SizedBox(height: 16),
|
||||
// A password keeps the account too (ADR 0017).
|
||||
const AccountSecuritySection(),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.publicProfileAnonymousHint,
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isIdentified) ...[
|
||||
|
|
@ -201,6 +207,26 @@ class const _Account() extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// What the account card says first: that the account keeps the progress,
|
||||
/// or that only this device does, with what keeps it (SimpleLogin, where
|
||||
/// the instance offers it).
|
||||
class const _AccountLine({required final bool identified})
|
||||
extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
if (identified) {
|
||||
return Text(l10n.profileIdentified);
|
||||
}
|
||||
final simpleLogin = context.watch<SimpleLoginStandingCubit>().state;
|
||||
return Text(
|
||||
simpleLogin is SimpleLoginMissing
|
||||
? l10n.profileAnonymousSimpleLogin
|
||||
: l10n.profileAnonymous,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _identifiedIn(SessionState state) => switch (state) {
|
||||
SessionStateActive(:final person) => person.isIdentified,
|
||||
_ => false,
|
||||
|
|
@ -274,6 +300,8 @@ class const _SignInWays() extends StatelessWidget {
|
|||
l10n.profileWayEmail,
|
||||
linkedAs: ways.emailAddress ?? l10n.profileWayLinked,
|
||||
),
|
||||
// SimpleLogin (ADR 0020, D21): once added, one more way in here.
|
||||
const SimpleLoginSection(gapAbove: 16),
|
||||
const SizedBox(height: 16),
|
||||
// The password and authenticators, and setting them up (ADR 0017),
|
||||
// which brings the address.
|
||||
|
|
@ -286,31 +314,38 @@ class const _SignInWays() extends StatelessWidget {
|
|||
/// The way to keep an anonymous account: signing in with GitHub, where the
|
||||
/// instance offers it. Where people sign in with a password it reaches only
|
||||
/// a GitHub account someone has (ADR 0017), and the password beneath keeps
|
||||
/// the account instead.
|
||||
/// the account instead. Not where the instance offers SimpleLogin, which
|
||||
/// alone keeps it there (ADR 0020, D23), nor until the instance has said.
|
||||
class const _GitHubSignIn() extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
BlocBuilder<GitHubSignInBloc, GitHubSignInState>(
|
||||
builder: (context, state) => switch (state) {
|
||||
GitHubSignInStateChecking() ||
|
||||
GitHubSignInStateUnavailable() ||
|
||||
GitHubSignInStateLinked() => const SizedBox.shrink(),
|
||||
GitHubSignInStateReady(:final failed, :final problem) => _button(
|
||||
context,
|
||||
enabled: true,
|
||||
failed: failed,
|
||||
problem: problem,
|
||||
),
|
||||
// Only for someone identified, who sees their ways in instead.
|
||||
GitHubSignInStateTaken() => _button(context, enabled: true),
|
||||
GitHubSignInStateBusy(cancellable: true) => const Padding(
|
||||
padding: EdgeInsets.only(top: 16),
|
||||
child: _GitHubWaiting(),
|
||||
),
|
||||
GitHubSignInStateBusy() ||
|
||||
GitHubSignInStateConfirmSwitch() => _button(context, enabled: false),
|
||||
},
|
||||
);
|
||||
Widget build(BuildContext context) {
|
||||
final simpleLogin = context.watch<SimpleLoginStandingCubit>().state;
|
||||
if (simpleLogin is SimpleLoginChecking ||
|
||||
simpleLogin is SimpleLoginMissing) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return BlocBuilder<GitHubSignInBloc, GitHubSignInState>(
|
||||
builder: (context, state) => switch (state) {
|
||||
GitHubSignInStateChecking() ||
|
||||
GitHubSignInStateUnavailable() ||
|
||||
GitHubSignInStateLinked() => const SizedBox.shrink(),
|
||||
GitHubSignInStateReady(:final failed, :final problem) => _button(
|
||||
context,
|
||||
enabled: true,
|
||||
failed: failed,
|
||||
problem: problem,
|
||||
),
|
||||
// Only for someone identified, who sees their ways in instead.
|
||||
GitHubSignInStateTaken() => _button(context, enabled: true),
|
||||
GitHubSignInStateBusy(cancellable: true) => const Padding(
|
||||
padding: EdgeInsets.only(top: 16),
|
||||
child: _GitHubWaiting(),
|
||||
),
|
||||
GitHubSignInStateBusy() ||
|
||||
GitHubSignInStateConfirmSwitch() => _button(context, enabled: false),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _button(
|
||||
BuildContext context, {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_app/testing.dart';
|
||||
|
||||
import '../../../support/pump_app.dart';
|
||||
import '../../../support/test_dependencies.dart';
|
||||
|
||||
// Where the instance offers SimpleLogin (ADR 0020, D21, D23): it alone keeps
|
||||
// an anonymous account.
|
||||
void main() {
|
||||
late FakeAuthGateway auth;
|
||||
late FakeSimpleLoginGateway simpleLogin;
|
||||
|
||||
setUp(() {
|
||||
auth = FakeAuthGateway();
|
||||
simpleLogin = FakeSimpleLoginGateway(auth);
|
||||
});
|
||||
|
||||
Future<void> start(WidgetTester tester) => pumpApp(
|
||||
tester,
|
||||
testDependencies(authGateway: auth, simpleLoginGateway: simpleLogin),
|
||||
);
|
||||
|
||||
testWidgets('an anonymous person is offered SimpleLogin alone, not GitHub '
|
||||
'(D23)', (tester) async {
|
||||
await start(tester);
|
||||
await openTab(tester, 'Profile');
|
||||
|
||||
expect(
|
||||
find.text(
|
||||
'Progress is saved on this device. Add SimpleLogin to keep it.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Sign in with GitHub'), findsNothing);
|
||||
expect(find.text('Add SimpleLogin'), findsWidgets);
|
||||
});
|
||||
|
||||
testWidgets('without SimpleLogin, GitHub keeps the account as before', (
|
||||
tester,
|
||||
) async {
|
||||
simpleLogin.isOffered = false;
|
||||
await start(tester);
|
||||
await openTab(tester, 'Profile');
|
||||
|
||||
expect(
|
||||
find.text('Progress is saved on this device. Sign in to keep it.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text('Sign in with GitHub'), findsOneWidget);
|
||||
expect(find.textContaining('SimpleLogin'), findsNothing);
|
||||
});
|
||||
}
|
||||
|
|
@ -175,6 +175,8 @@ Dependencies testDependencies({
|
|||
MediaPlayers? mediaPlayers,
|
||||
PasswordGateway? passwordGateway,
|
||||
AccountKeyStore? accountKeys,
|
||||
// On the same `authGateway`; none offers SimpleLogin unless given.
|
||||
FakeSimpleLoginGateway? simpleLoginGateway,
|
||||
}) {
|
||||
final device = deviceDictionaries ?? noDeviceDictionaries();
|
||||
final auth = authGateway ?? FakeAuthGateway();
|
||||
|
|
@ -192,10 +194,10 @@ Dependencies testDependencies({
|
|||
auth,
|
||||
addresses,
|
||||
),
|
||||
// Offered by no instance in Learn's tests: halkyon_app's tests test its
|
||||
// sheet.
|
||||
// Offered by no instance in Learn's tests but where one is given:
|
||||
// halkyon_app's tests test its sheet.
|
||||
simpleLogin: SimpleLoginRepository(
|
||||
FakeSimpleLoginGateway(auth)..isOffered = false,
|
||||
simpleLoginGateway ?? (FakeSimpleLoginGateway(auth)..isOffered = false),
|
||||
FakeOidcBrowser(),
|
||||
crypto,
|
||||
keys,
|
||||
|
|
|
|||
Loading…
Reference in a new issue