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
23 changed files with 829 additions and 77 deletions
|
|
@ -143,11 +143,18 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
|
|||
keys on the device before any proof is sent; an account opens its keys
|
||||
with its password; an account of before gets its keys, and its handle
|
||||
where it has none. With `link`, SimpleLogin joins the account of the
|
||||
device's person instead, an anonymous person's device keys kept (no
|
||||
screen offers it yet). The screens: the start screen's Continue with
|
||||
SimpleLogin, first where the instance offers it, and
|
||||
`showSimpleLoginSheet` (`SimpleLoginCubit`), which waits for the browser
|
||||
with Cancel and stops waiting on the loopback once it closes.
|
||||
device's person instead, an anonymous person's device keys kept. The
|
||||
screens: the start screen's Continue with SimpleLogin, first where the
|
||||
instance offers it, and `showSimpleLoginSheet` (`SimpleLoginCubit`),
|
||||
which waits for the browser with Cancel and stops waiting on the loopback
|
||||
once it closes. Adding it to an account (ADR 0020, D21, D23): where the
|
||||
person stands (`SimpleLoginRepository.standing`, read anew after every
|
||||
change, `SimpleLoginStandingCubit`: nothing to show, linked, or
|
||||
missing), shown by `SimpleLoginSection` (each app places it above
|
||||
`AccountSecuritySection`: a heading and Add SimpleLogin; once added a row
|
||||
among the ways in), which reads it once where a
|
||||
`SimpleLoginStandingScope` is above it. Where the instance offers
|
||||
SimpleLogin, GitHub is added to nobody (`GitHubOffer.linking`).
|
||||
- **Files** (`FileSaver`, `PlatformFileSaver`): keeping a file the app made
|
||||
where the person chooses (a recovery phrase, recovery codes, an app's
|
||||
exports): on Linux and macOS the own user's alone (`writePrivately`). On
|
||||
|
|
|
|||
|
|
@ -93,12 +93,16 @@ export 'src/password/widget/secret_screen.dart';
|
|||
export 'src/person/person_folders.dart';
|
||||
export 'src/privacy/content_cipher.dart';
|
||||
export 'src/simplelogin/bloc/simplelogin_cubit.dart';
|
||||
export 'src/simplelogin/bloc/simplelogin_standing_cubit.dart';
|
||||
export 'src/simplelogin/data/serverpod_simplelogin_gateway.dart';
|
||||
export 'src/simplelogin/data/simplelogin_gateway.dart';
|
||||
export 'src/simplelogin/data/simplelogin_repository.dart';
|
||||
export 'src/simplelogin/model/simplelogin_arrival.dart';
|
||||
export 'src/simplelogin/model/simplelogin_failed.dart';
|
||||
export 'src/simplelogin/model/simplelogin_standing.dart';
|
||||
export 'src/simplelogin/widget/simplelogin_section.dart';
|
||||
export 'src/simplelogin/widget/simplelogin_sheet.dart';
|
||||
export 'src/simplelogin/widget/simplelogin_standing_scope.dart';
|
||||
export 'src/simplelogin/widget/simplelogin_texts.dart';
|
||||
export 'src/sync/app_compatibility.dart';
|
||||
export 'src/sync/bloc/sync_status_bloc.dart';
|
||||
|
|
|
|||
|
|
@ -278,5 +278,9 @@
|
|||
"simpleLoginHandleInvalid": "Ein Handle besteht aus 1 bis 30 Buchstaben (a bis z), Ziffern oder Unterstrichen.",
|
||||
"simpleLoginHandleReserved": "Dieses Handle ist der Instanz vorbehalten.",
|
||||
"simpleLoginHandleTaken": "Dieses Handle hat schon jemand anderes.",
|
||||
"simpleLoginTooManyHandleChecks": "Es wurden zu viele Handles ausprobiert. Versuch es in einer Stunde noch einmal."
|
||||
"simpleLoginTooManyHandleChecks": "Es wurden zu viele Handles ausprobiert. Versuch es in einer Stunde noch einmal.",
|
||||
"simpleLoginSectionTitle": "SimpleLogin",
|
||||
"simpleLoginSignsYouIn": "Meldet dich an",
|
||||
"simpleLoginAddHint": "Melde dich mit SimpleLogin an, oder mit deinem Proton-Konto.",
|
||||
"simpleLoginAdded": "SimpleLogin hinzugefügt."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -604,5 +604,9 @@
|
|||
"simpleLoginHandleInvalid": "A handle is 1 to 30 letters (a to z), digits or underscores.",
|
||||
"simpleLoginHandleReserved": "This handle is kept for the instance.",
|
||||
"simpleLoginHandleTaken": "Someone else has this handle.",
|
||||
"simpleLoginTooManyHandleChecks": "Too many handles were tried. Try again in an hour."
|
||||
"simpleLoginTooManyHandleChecks": "Too many handles were tried. Try again in an hour.",
|
||||
"simpleLoginSectionTitle": "SimpleLogin",
|
||||
"simpleLoginSignsYouIn": "Signs you in",
|
||||
"simpleLoginAddHint": "Sign in with SimpleLogin, or with your Proton account.",
|
||||
"simpleLoginAdded": "SimpleLogin added."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1776,6 +1776,30 @@ abstract class HalkyonAppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Too many handles were tried. Try again in an hour.'**
|
||||
String get simpleLoginTooManyHandleChecks;
|
||||
|
||||
/// No description provided for @simpleLoginSectionTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'SimpleLogin'**
|
||||
String get simpleLoginSectionTitle;
|
||||
|
||||
/// No description provided for @simpleLoginSignsYouIn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Signs you in'**
|
||||
String get simpleLoginSignsYouIn;
|
||||
|
||||
/// No description provided for @simpleLoginAddHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sign in with SimpleLogin, or with your Proton account.'**
|
||||
String get simpleLoginAddHint;
|
||||
|
||||
/// No description provided for @simpleLoginAdded.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'SimpleLogin added.'**
|
||||
String get simpleLoginAdded;
|
||||
}
|
||||
|
||||
class _HalkyonAppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -1051,4 +1051,17 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
@override
|
||||
String get simpleLoginTooManyHandleChecks =>
|
||||
'Es wurden zu viele Handles ausprobiert. Versuch es in einer Stunde noch einmal.';
|
||||
|
||||
@override
|
||||
String get simpleLoginSectionTitle => 'SimpleLogin';
|
||||
|
||||
@override
|
||||
String get simpleLoginSignsYouIn => 'Meldet dich an';
|
||||
|
||||
@override
|
||||
String get simpleLoginAddHint =>
|
||||
'Melde dich mit SimpleLogin an, oder mit deinem Proton-Konto.';
|
||||
|
||||
@override
|
||||
String get simpleLoginAdded => 'SimpleLogin hinzugefügt.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1032,6 +1032,19 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
@override
|
||||
String get simpleLoginTooManyHandleChecks =>
|
||||
'Too many handles were tried. Try again in an hour.';
|
||||
|
||||
@override
|
||||
String get simpleLoginSectionTitle => 'SimpleLogin';
|
||||
|
||||
@override
|
||||
String get simpleLoginSignsYouIn => 'Signs you in';
|
||||
|
||||
@override
|
||||
String get simpleLoginAddHint =>
|
||||
'Sign in with SimpleLogin, or with your Proton account.';
|
||||
|
||||
@override
|
||||
String get simpleLoginAdded => 'SimpleLogin added.';
|
||||
}
|
||||
|
||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ class ServerpodAuthGateway(
|
|||
final signIn = basics.gitHubClientId != null;
|
||||
return GitHubOffer(
|
||||
signIn: signIn,
|
||||
linking: signIn && !basics.passwordSignIn,
|
||||
linking: signIn && !basics.passwordSignIn && !basics.simpleLogin,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,8 +35,8 @@ final class const GitHubOffer({
|
|||
required final bool signIn,
|
||||
|
||||
/// Whether GitHub is added to people as a way in: not where they sign in
|
||||
/// with a password (ADR 0017, D2), where only those who have it already
|
||||
/// sign in with it.
|
||||
/// with a password (ADR 0017, D2) or with SimpleLogin (ADR 0020, D23),
|
||||
/// where only those who have it already sign in with it.
|
||||
required final bool linking,
|
||||
}) with Equatable {
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/src/password/bloc/unexpected_failure.dart';
|
||||
import 'package:halkyon_app/src/simplelogin/data/simplelogin_repository.dart';
|
||||
import 'package:halkyon_app/src/simplelogin/model/simplelogin_failed.dart';
|
||||
import 'package:halkyon_app/src/simplelogin/model/simplelogin_standing.dart';
|
||||
|
||||
/// Reads where the device's person stands with SimpleLogin (ADR 0020,
|
||||
/// D21), and again after every change ([SimpleLoginRepository.changes]),
|
||||
/// for the account section and what an app says beside it. Nothing is
|
||||
/// shown until the instance has answered ([SimpleLoginChecking]); while it
|
||||
/// can't be reached, what it last said stays.
|
||||
class SimpleLoginStandingCubit(final SimpleLoginRepository _simpleLogin)
|
||||
extends Cubit<SimpleLoginStanding> {
|
||||
this : super(const SimpleLoginChecking()) {
|
||||
_changes = _simpleLogin.changes.listen((_) => load());
|
||||
}
|
||||
|
||||
late final StreamSubscription<void> _changes;
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
// Not awaited: a broadcast stream's cancel completes in the root zone,
|
||||
// which a widget test's fake clock never reaches.
|
||||
unawaited(_changes.cancel());
|
||||
return super.close();
|
||||
}
|
||||
|
||||
/// Reads it anew.
|
||||
Future<void> load() async {
|
||||
try {
|
||||
final standing = await _simpleLogin.standing();
|
||||
if (!isClosed) {
|
||||
emit(standing);
|
||||
}
|
||||
} on Object catch (error) {
|
||||
if (error is! SimpleLoginFailed) {
|
||||
logUnexpected(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,10 @@ class ServerpodSimpleLoginGateway(
|
|||
Future<bool> offered() =>
|
||||
_failing(() async => (await _base.instance.basics()).simpleLogin);
|
||||
|
||||
@override
|
||||
Future<bool> hasSimpleLogin() =>
|
||||
_failing(() async => (await _base.account.signInWays()).simpleLogin);
|
||||
|
||||
@override
|
||||
Future<OidcStart> begin(
|
||||
protocol.SimpleLoginReturn back, {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ abstract interface class SimpleLoginGateway() {
|
|||
/// Whether the instance offers SimpleLogin.
|
||||
Future<bool> offered();
|
||||
|
||||
/// Whether the device's person's account signs in with SimpleLogin.
|
||||
Future<bool> hasSimpleLogin();
|
||||
|
||||
/// Begins a sign-in that comes [back] by the loopback or the app's scheme:
|
||||
/// the attempt, SimpleLogin's page and the state. With [link], as the
|
||||
/// device's person, to add SimpleLogin to their account.
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:halkyon_app/src/auth/data/auth_gateway.dart';
|
||||
import 'package:halkyon_app/src/auth/model/auth_status.dart';
|
||||
|
|
@ -8,6 +10,7 @@ import 'package:halkyon_app/src/password/model/unlocked_account_keys.dart';
|
|||
import 'package:halkyon_app/src/simplelogin/data/simplelogin_gateway.dart';
|
||||
import 'package:halkyon_app/src/simplelogin/model/simplelogin_arrival.dart';
|
||||
import 'package:halkyon_app/src/simplelogin/model/simplelogin_failed.dart';
|
||||
import 'package:halkyon_app/src/simplelogin/model/simplelogin_standing.dart';
|
||||
import 'package:halkyon_base_client/halkyon_base_client.dart' as protocol;
|
||||
import 'package:halkyon_oidc_flutter/halkyon_oidc_flutter.dart';
|
||||
|
||||
|
|
@ -53,9 +56,54 @@ class SimpleLoginRepository(
|
|||
/// The platform the app runs on; tests name one.
|
||||
final TargetPlatform? _platform,
|
||||
}) {
|
||||
this {
|
||||
_status = _auth.statusChanges.listen((_) => _changed());
|
||||
}
|
||||
|
||||
late final StreamSubscription<AuthStatus> _status;
|
||||
|
||||
final _changes = StreamController<void>.broadcast();
|
||||
|
||||
/// Every change to where the device's person stands with SimpleLogin
|
||||
/// ([standing]): SimpleLogin added to their account, or another person
|
||||
/// signed in, or nobody, for what shows it to read anew.
|
||||
Stream<void> get changes => _changes.stream;
|
||||
|
||||
void _changed() {
|
||||
if (!_changes.isClosed) {
|
||||
_changes.add(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stops following who is signed in; the owner calls it as it closes.
|
||||
Future<void> dispose() async {
|
||||
// Not awaited: a broadcast stream's cancel completes in the root zone,
|
||||
// which a widget test's fake clock never reaches.
|
||||
unawaited(_status.cancel());
|
||||
unawaited(_changes.close());
|
||||
}
|
||||
|
||||
/// Whether the instance offers SimpleLogin.
|
||||
Future<bool> offered() => _gateway.offered();
|
||||
|
||||
/// Where the device's person stands with SimpleLogin (D21): nothing to
|
||||
/// show where the instance offers none or nobody is signed in; otherwise
|
||||
/// whether their account has it.
|
||||
Future<SimpleLoginStanding> standing() async {
|
||||
final status = _auth.status;
|
||||
if (status is! SignedIn) {
|
||||
return const SimpleLoginUnavailable();
|
||||
}
|
||||
if (!await _gateway.offered()) {
|
||||
return const SimpleLoginUnavailable();
|
||||
}
|
||||
// An anonymous person has no way in to ask about: SimpleLogin, once
|
||||
// added, makes them identified.
|
||||
return status.isIdentified && await _gateway.hasSimpleLogin()
|
||||
? const SimpleLoginLinked()
|
||||
: const SimpleLoginMissing();
|
||||
}
|
||||
|
||||
/// Signs in with SimpleLogin in the browser: someone new, or an account
|
||||
/// signed in to; with [link], SimpleLogin added to the device's person's
|
||||
/// account. Null where the person closed the browser, or [cancel] ended
|
||||
|
|
@ -90,7 +138,7 @@ class SimpleLoginRepository(
|
|||
OidcCallbackProblem.failed => SimpleLoginProblem.failed,
|
||||
});
|
||||
}
|
||||
return switch (back) {
|
||||
final arrival = switch (back) {
|
||||
null => null,
|
||||
OidcDenied() => throw const SimpleLoginFailed(SimpleLoginProblem.denied),
|
||||
OidcCode(:final code) => await _gateway.complete(
|
||||
|
|
@ -99,6 +147,11 @@ class SimpleLoginRepository(
|
|||
link: link,
|
||||
),
|
||||
};
|
||||
if (link && arrival != null) {
|
||||
// Added already: an account with its keys is done here.
|
||||
_changed();
|
||||
}
|
||||
return arrival;
|
||||
}
|
||||
|
||||
/// Stops waiting for the browser on the loopback: [signIn] returns null.
|
||||
|
|
@ -235,5 +288,6 @@ class SimpleLoginRepository(
|
|||
}
|
||||
await _auth.markIdentified();
|
||||
}
|
||||
_changed();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Where the device's person stands with SimpleLogin on their instance
|
||||
/// (ADR 0020, D21): what the account section shows.
|
||||
sealed class const SimpleLoginStanding() with Equatable;
|
||||
|
||||
/// Not known yet: the instance hasn't answered. Nothing is shown.
|
||||
final class const SimpleLoginChecking() extends SimpleLoginStanding {
|
||||
@override
|
||||
List<Object?> get props => const [];
|
||||
}
|
||||
|
||||
/// Nothing to show: the instance offers no SimpleLogin, or nobody is signed
|
||||
/// in to add it to.
|
||||
final class const SimpleLoginUnavailable() extends SimpleLoginStanding {
|
||||
@override
|
||||
List<Object?> get props => const [];
|
||||
}
|
||||
|
||||
/// The person's account signs in with SimpleLogin.
|
||||
final class const SimpleLoginLinked() extends SimpleLoginStanding {
|
||||
@override
|
||||
List<Object?> get props => const [];
|
||||
}
|
||||
|
||||
/// The person's account doesn't sign in with SimpleLogin yet: they add it
|
||||
/// to keep the account, an anonymous person with it alone (D23).
|
||||
final class const SimpleLoginMissing() extends SimpleLoginStanding {
|
||||
@override
|
||||
List<Object?> get props => const [];
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/l10n/halkyon_app_localizations.dart';
|
||||
import 'package:halkyon_app/src/app/app_scope.dart';
|
||||
import 'package:halkyon_app/src/instance/instance_scope.dart';
|
||||
import 'package:halkyon_app/src/simplelogin/bloc/simplelogin_standing_cubit.dart';
|
||||
import 'package:halkyon_app/src/simplelogin/model/simplelogin_standing.dart';
|
||||
import 'package:halkyon_app/src/simplelogin/widget/simplelogin_sheet.dart';
|
||||
import 'package:halkyon_app/src/simplelogin/widget/simplelogin_standing_scope.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// The SimpleLogin part of an app's account section (ADR 0020, D21), which
|
||||
/// each app places above `AccountSecuritySection`: for someone without
|
||||
/// SimpleLogin a heading and Add SimpleLogin; once added, one more way in,
|
||||
/// a row without heading lined up with the others. Nothing where the
|
||||
/// instance offers no SimpleLogin or nobody is signed in. [gapAbove] is
|
||||
/// the space above its heading, none above the row.
|
||||
class const SimpleLoginSection({
|
||||
final double gapAbove = 0,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => SimpleLoginStandingScope.around(
|
||||
context,
|
||||
_SimpleLoginSection(gapAbove: gapAbove),
|
||||
);
|
||||
}
|
||||
|
||||
class const _SimpleLoginSection({required final double gapAbove})
|
||||
extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = HalkyonAppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
return switch (context.watch<SimpleLoginStandingCubit>().state) {
|
||||
SimpleLoginChecking() ||
|
||||
SimpleLoginUnavailable() => const SizedBox.shrink(),
|
||||
SimpleLoginLinked() => ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(l10n.simpleLoginSectionTitle),
|
||||
subtitle: Text(l10n.simpleLoginSignsYouIn),
|
||||
trailing: Icon(
|
||||
LucideIcons.circleCheck,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
),
|
||||
SimpleLoginMissing() => Padding(
|
||||
padding: EdgeInsets.only(top: gapAbove),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
l10n.simpleLoginSectionTitle,
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(LucideIcons.link),
|
||||
title: Text(l10n.simpleLoginAddTitle),
|
||||
subtitle: Text(l10n.simpleLoginAddHint),
|
||||
onTap: () => addSimpleLogin(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// The instance's name as people know it, for the texts about it.
|
||||
String simpleLoginInstanceName(BuildContext context) =>
|
||||
InstanceScope.maybeOf(context)?.choice.current?.name ??
|
||||
AppScope.of(context).serverName;
|
||||
|
||||
/// Adds SimpleLogin to the device's person's account through its sheet
|
||||
/// (`showSimpleLoginSheet`, `link`), saying so once it is added; where they
|
||||
/// stand is read anew everywhere ([SimpleLoginStandingCubit]).
|
||||
Future<void> addSimpleLogin(BuildContext context) async {
|
||||
final messenger = ScaffoldMessenger.maybeOf(context);
|
||||
final added = HalkyonAppLocalizations.of(context).simpleLoginAdded;
|
||||
if (await showSimpleLoginSheet(
|
||||
context,
|
||||
instance: simpleLoginInstanceName(context),
|
||||
link: true,
|
||||
)) {
|
||||
messenger?.showSnackBar(SnackBar(content: Text(added)));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/src/app/app_scope.dart';
|
||||
import 'package:halkyon_app/src/simplelogin/bloc/simplelogin_standing_cubit.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Reads where the device's person stands with SimpleLogin once for all
|
||||
/// below it that show it: `SimpleLoginSection` and an app's own words
|
||||
/// (`context.watch<SimpleLoginStandingCubit>()`). Where none is above it,
|
||||
/// the section reads it on its own ([around]).
|
||||
class const SimpleLoginStandingScope({
|
||||
required final Widget child,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
/// [child] under the scope above [context], or under one of its own.
|
||||
static Widget around(BuildContext context, Widget child) =>
|
||||
context.getInheritedWidgetOfExactType<_Scoped>() != null
|
||||
? child
|
||||
: SimpleLoginStandingScope(child: child);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BlocProvider(
|
||||
create: (context) =>
|
||||
SimpleLoginStandingCubit(AppScope.of(context).simpleLogin)..load(),
|
||||
child: _Scoped(child: child),
|
||||
);
|
||||
}
|
||||
|
||||
/// Marks a [SimpleLoginStandingScope] for those below it.
|
||||
class const _Scoped({required super.child}) extends InheritedWidget {
|
||||
@override
|
||||
bool updateShouldNotify(_Scoped oldWidget) => false;
|
||||
}
|
||||
|
|
@ -44,7 +44,7 @@ class FakeAuthGateway({var AuthStatus _status = const SignedOut()})
|
|||
bool offersGitHub = true;
|
||||
|
||||
/// Whether the instance adds GitHub to people as a way in: not where they
|
||||
/// sign in with a password (ADR 0017).
|
||||
/// sign in with a password (ADR 0017) or with SimpleLogin (ADR 0020).
|
||||
bool linksGitHub = true;
|
||||
|
||||
/// How the next GitHub links end.
|
||||
|
|
|
|||
|
|
@ -114,6 +114,12 @@ class FakeSimpleLoginGateway(final AuthGateway _auth)
|
|||
return isOffered;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> hasSimpleLogin() async {
|
||||
await _call('hasSimpleLogin');
|
||||
return linked.containsValue(_owner(null));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<OidcStart> begin(
|
||||
protocol.SimpleLoginReturn back, {
|
||||
|
|
|
|||
|
|
@ -188,6 +188,7 @@ class TestAppDependencies({
|
|||
Future<void> dispose() async {
|
||||
disposed = true;
|
||||
await passwords.dispose();
|
||||
await simpleLogin.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,10 +21,12 @@ final class _MemoryStorage() implements ClientAuthSuccessStorage {
|
|||
|
||||
/// An instance's halkyon_base as the network shows it: anonymous sign-ins
|
||||
/// get `anonymous-token`, its basics offer GitHub (and passwords while
|
||||
/// [passwords]), and a sign-up makes Ben, signed in with `ben-token`.
|
||||
/// [passwords], SimpleLogin while [simpleLogin]), and a sign-up makes Ben,
|
||||
/// signed in with `ben-token`.
|
||||
final class _Instance() {
|
||||
/// Whether its basics offer password sign-in.
|
||||
/// Whether its basics offer password sign-in, and SimpleLogin.
|
||||
bool passwords = false;
|
||||
bool simpleLogin = false;
|
||||
|
||||
static final anonymous = UuidValue.withValidation(
|
||||
'0191f0c4-1111-7000-8000-000000000001',
|
||||
|
|
@ -54,6 +56,7 @@ final class _Instance() {
|
|||
sourceCodeUrl: 'https://code.example.org/learn',
|
||||
minimumAppBuild: 1,
|
||||
passwordSignIn: passwords,
|
||||
simpleLogin: simpleLogin,
|
||||
).toJson(),
|
||||
),
|
||||
200,
|
||||
|
|
@ -121,7 +124,8 @@ void main() {
|
|||
);
|
||||
|
||||
test('tells what the instance offers from its basics: GitHub sign-in, '
|
||||
'and linking GitHub to people only where passwords are not', () async {
|
||||
'and linking GitHub to people only where neither passwords nor '
|
||||
'SimpleLogin are (ADR 0020, D23)', () async {
|
||||
expect(
|
||||
await gateway.gitHubOffer(),
|
||||
const GitHubOffer(signIn: true, linking: true),
|
||||
|
|
@ -139,6 +143,14 @@ void main() {
|
|||
hasLength(2),
|
||||
reason: 'one read of the basics each',
|
||||
);
|
||||
|
||||
instance
|
||||
..passwords = false
|
||||
..simpleLogin = true;
|
||||
expect(
|
||||
await gateway.gitHubOffer(),
|
||||
const GitHubOffer(signIn: true, linking: false),
|
||||
);
|
||||
});
|
||||
|
||||
test("puts the instance's refusals of GitHub as the app's problems", () {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue