feat(app)!: no anonymous accounts on the device (ADR 0020, D4, D25)
Every account has a way in, so the apps make no anonymous account any more. The start screen shows whenever nobody is signed in, a first start included, and offers SimpleLogin, a password and GitHub for those who have it, with no start without an account. An instance out of reach is said so, with Try again, and one offering no way to create an account too. An anonymous account's session left from before is deleted as the app starts (D25), falling back to signing out on the device. What only anonymous accounts needed goes: signing in anonymously, the signed-out mark, adding GitHub to an account and switching to a GitHub account from the Profile tab (with its Cancel, the only one), an anonymous device's own keys, the warning that signing out deletes the account, and the texts about keeping progress. Settings says on which instance the account is signed in. Learn's publishing, reports and suggestions and its learner count no longer ask whether the person has an identity: everyone signed in has one. The server still offers anonymous sign-in until the next commit, so older apps keep working until v0.20.0 raises the minimum build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
fad12b4411
commit
5b9df73756
1 changed files with 981 additions and 3339 deletions
|
|
@ -3,21 +3,19 @@ import 'dart:async';
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/src/auth/data/auth_repository.dart';
|
||||
import 'package:halkyon_app/src/auth/data/signed_out_mark.dart';
|
||||
import 'package:halkyon_app/src/auth/model/auth_status.dart';
|
||||
import 'package:halkyon_app/src/auth/model/github_sign_in.dart';
|
||||
|
||||
/// Keeps a person signed in: follows who is signed in, signs in anonymously
|
||||
/// at a first start, and signs out on request. Once signed out (by the
|
||||
/// person, by deleting the account, or by the server ending the session),
|
||||
/// it asks how to start rather than making an account by itself, also at
|
||||
/// every start of the app until someone starts.
|
||||
class SessionBloc(final AuthRepository _auth, final SignedOutMark _mark)
|
||||
/// Keeps a person signed in: follows who is signed in, and signs out on
|
||||
/// request. Nobody signed in (at a first start, after signing out or
|
||||
/// deleting the account, or once the server ended the session), it asks
|
||||
/// how to start: every account has a way in, and nothing is made until the
|
||||
/// person chooses (ADR 0020, D4). An anonymous account's session left from
|
||||
/// before they went ends as the app starts, the account deleted (D25).
|
||||
class SessionBloc(final AuthRepository _auth)
|
||||
extends Bloc<SessionEvent, SessionState> {
|
||||
this : super(const SessionStateLoading()) {
|
||||
on<SessionEventStarted>(_onStarted);
|
||||
on<SessionEventSignInRequested>(_onSignInRequested);
|
||||
on<SessionEventStartedAnonymously>(_onStartedAnonymously);
|
||||
on<SessionEventGitHubRequested>(_onGitHubRequested);
|
||||
on<SessionEventPasswordOfferDone>(
|
||||
(event, emit) => emit(_stateOf(_auth.status)),
|
||||
|
|
@ -33,55 +31,20 @@ class SessionBloc(final AuthRepository _auth, final SignedOutMark _mark)
|
|||
SessionEventStarted event,
|
||||
Emitter<SessionState> emit,
|
||||
) async {
|
||||
if (_auth.status is SignedOut && await _mark.read()) {
|
||||
emit(const SessionStateSignedOut());
|
||||
} else {
|
||||
add(const SessionEventSignInRequested());
|
||||
if (_auth.isAnonymous) {
|
||||
try {
|
||||
await _auth.endAnonymousSession();
|
||||
} on Object catch (error, st) {
|
||||
// Only the device's own storage fails it: the session stays, but
|
||||
// nothing works in it any more ([_stateOf]).
|
||||
addError(error, st);
|
||||
}
|
||||
}
|
||||
await emit.forEach<AuthStatus>(
|
||||
_auth.statusChanges,
|
||||
onData: (status) {
|
||||
if (status is SignedOut) {
|
||||
// Signed out, here or by the server (a revoked session): ask how
|
||||
// to start, now and at the next starts.
|
||||
unawaited(_mark.write(signedOut: true));
|
||||
return const SessionStateSignedOut();
|
||||
}
|
||||
unawaited(_mark.write(signedOut: false));
|
||||
return _stateOf(status);
|
||||
},
|
||||
);
|
||||
emit(_stateOf(_auth.status));
|
||||
await emit.forEach<AuthStatus>(_auth.statusChanges, onData: _stateOf);
|
||||
}
|
||||
|
||||
Future<void> _onSignInRequested(
|
||||
SessionEventSignInRequested event,
|
||||
Emitter<SessionState> emit,
|
||||
) async {
|
||||
// A session stored on the device wins: never a second anonymous account.
|
||||
if (_auth.status case final SignedIn person) {
|
||||
emit(SessionStateActive(person));
|
||||
return;
|
||||
}
|
||||
emit(const SessionStateLoading());
|
||||
try {
|
||||
await _auth.ensureSignedIn();
|
||||
emit(_stateOf(_auth.status));
|
||||
} on Object catch (error, st) {
|
||||
addError(error, st);
|
||||
emit(const SessionStateFailed());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onStartedAnonymously(
|
||||
SessionEventStartedAnonymously event,
|
||||
Emitter<SessionState> emit,
|
||||
) async {
|
||||
await _mark.write(signedOut: false);
|
||||
add(const SessionEventSignInRequested());
|
||||
}
|
||||
|
||||
/// Signing in with GitHub needs no account here first: the GitHub
|
||||
/// account's person, if there is one.
|
||||
/// Signing in with GitHub: the GitHub account's person, if there is one.
|
||||
Future<void> _onGitHubRequested(
|
||||
SessionEventGitHubRequested event,
|
||||
Emitter<SessionState> emit,
|
||||
|
|
@ -173,6 +136,9 @@ class SessionBloc(final AuthRepository _auth, final SignedOutMark _mark)
|
|||
}
|
||||
|
||||
SessionState _stateOf(AuthStatus status) => switch (status) {
|
||||
// Nothing works in an anonymous account any more (D25): only one that
|
||||
// began before the app started could be signed in, and it has ended.
|
||||
SignedIn(isIdentified: false) => const SessionStateSignedOut(),
|
||||
SignedIn() when _offerPassword => _offered(status),
|
||||
SignedIn() => SessionStateActive(status),
|
||||
SignedOut() => const SessionStateSignedOut(),
|
||||
|
|
@ -190,16 +156,10 @@ sealed class const SessionEvent() with Equatable {
|
|||
List<Object?> get props => const [];
|
||||
}
|
||||
|
||||
/// The app starts: follow who is signed in, and sign in anonymously if nobody
|
||||
/// is.
|
||||
/// The app starts: follow who is signed in, and end an anonymous account's
|
||||
/// session left from before (ADR 0020, D25).
|
||||
final class const SessionEventStarted() extends SessionEvent;
|
||||
|
||||
/// Sign in (again): after a failed attempt, or once nobody is signed in.
|
||||
final class const SessionEventSignInRequested() extends SessionEvent;
|
||||
|
||||
/// Start without an account, once signed out: a new anonymous one.
|
||||
final class const SessionEventStartedAnonymously() extends SessionEvent;
|
||||
|
||||
/// Sign in with GitHub, once signed out.
|
||||
final class const SessionEventGitHubRequested() extends SessionEvent;
|
||||
|
||||
|
|
@ -224,19 +184,12 @@ final class const SessionEventDeleteRequested() extends SessionEvent;
|
|||
/// Whether the app has a signed-in person to work for.
|
||||
sealed class const SessionState() with Equatable;
|
||||
|
||||
/// Signing in, or finding out who is signed in.
|
||||
/// Finding out who is signed in.
|
||||
final class const SessionStateLoading() extends SessionState {
|
||||
@override
|
||||
List<Object?> get props => const [];
|
||||
}
|
||||
|
||||
/// Signing in failed, for example without a connection; the app offers to try
|
||||
/// again.
|
||||
final class const SessionStateFailed() extends SessionState {
|
||||
@override
|
||||
List<Object?> get props => const [];
|
||||
}
|
||||
|
||||
/// A person is signed in.
|
||||
final class const SessionStateActive(
|
||||
/// Who is signed in.
|
||||
|
|
@ -287,7 +240,7 @@ final class const SessionStateGitHubSignedIn(
|
|||
List<Object?> get props => [person];
|
||||
}
|
||||
|
||||
/// Nobody is signed in since signing out: the app asks how to start.
|
||||
/// Nobody is signed in: the app asks how to start.
|
||||
final class const SessionStateSignedOut({
|
||||
/// Whether a way to start is under way.
|
||||
final bool busy = false,
|
||||
|
|
@ -301,8 +254,8 @@ final class const SessionStateSignedOut({
|
|||
|
||||
/// Why a way to start failed.
|
||||
enum SignedOutProblem() {
|
||||
/// No account has the GitHub account yet: start without one, then add
|
||||
/// GitHub, or, where passwords are offered, create an account.
|
||||
/// No account has the GitHub account: GitHub makes none (ADR 0020, D25),
|
||||
/// so the person creates one where the instance offers a way.
|
||||
noGitHubAccount,
|
||||
|
||||
/// The GitHub account's person signs in with their address and password
|
||||
|
|
|
|||
Loading…
Reference in a new issue