fix(app): signing out or deleting the account keeps the person's app until it is done
Both went through the launch screen (SessionStateLoading), which closed the person's app before asking the instance; a failure then opened it again with no word of why. The person's state stays now, saying what is under way (`busy`), and a failure keeps them signed in with why (`problem`). An instance out of reach still signs out on the device (Serverpod's signOutDevice), as signing out of a borrowed device must work offline, so only the device's own storage fails a sign-out; a deletion fails whenever the instance isn't reached or refuses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
3994257044
commit
662c30b117
3 changed files with 133 additions and 19 deletions
|
|
@ -126,28 +126,49 @@ class SessionBloc(final AuthRepository _auth, final SignedOutMark _mark)
|
||||||
Future<void> _onSignOutRequested(
|
Future<void> _onSignOutRequested(
|
||||||
SessionEventSignOutRequested event,
|
SessionEventSignOutRequested event,
|
||||||
Emitter<SessionState> emit,
|
Emitter<SessionState> emit,
|
||||||
) async {
|
) => _onAccountAction(
|
||||||
emit(const SessionStateLoading());
|
SessionAction.signOut,
|
||||||
try {
|
// The instance out of reach signs out on this device all the same
|
||||||
// The status change to signed out then asks how to start.
|
// (Serverpod's `signOutDevice`), as signing out of a borrowed device
|
||||||
await _auth.signOut();
|
// must work offline; only the device's own storage fails it.
|
||||||
} on Object catch (error, st) {
|
_auth.signOut,
|
||||||
addError(error, st);
|
SessionProblem.signOutFailed,
|
||||||
emit(_stateOf(_auth.status));
|
emit,
|
||||||
}
|
);
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onDeleteRequested(
|
Future<void> _onDeleteRequested(
|
||||||
SessionEventDeleteRequested event,
|
SessionEventDeleteRequested event,
|
||||||
Emitter<SessionState> emit,
|
Emitter<SessionState> emit,
|
||||||
|
) => _onAccountAction(
|
||||||
|
SessionAction.delete,
|
||||||
|
_auth.deleteAccount,
|
||||||
|
SessionProblem.deleteFailed,
|
||||||
|
emit,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Runs [action] for the signed-in person's account, their app in place
|
||||||
|
/// meanwhile, [busy] saying what is under way: never through the launch
|
||||||
|
/// screen ([SessionStateLoading]), which would close the app and, should
|
||||||
|
/// [action] fail, open it again with no word of why. Done, the status
|
||||||
|
/// change to signed out asks how to start; failed, the person stays,
|
||||||
|
/// told so by [problem].
|
||||||
|
Future<void> _onAccountAction(
|
||||||
|
SessionAction busy,
|
||||||
|
Future<void> Function() action,
|
||||||
|
SessionProblem problem,
|
||||||
|
Emitter<SessionState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(const SessionStateLoading());
|
if (_auth.status case final SignedIn person) {
|
||||||
|
emit(SessionStateActive(person, busy: busy));
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
// The status change to signed out then asks how to start.
|
await action();
|
||||||
await _auth.deleteAccount();
|
|
||||||
} on Object catch (error, st) {
|
} on Object catch (error, st) {
|
||||||
addError(error, st);
|
addError(error, st);
|
||||||
emit(_stateOf(_auth.status));
|
emit(switch (_auth.status) {
|
||||||
|
final SignedIn person => SessionStateActive(person, problem: problem),
|
||||||
|
SignedOut() => const SessionStateSignedOut(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -219,10 +240,40 @@ final class const SessionStateFailed() extends SessionState {
|
||||||
/// A person is signed in.
|
/// A person is signed in.
|
||||||
final class const SessionStateActive(
|
final class const SessionStateActive(
|
||||||
/// Who is signed in.
|
/// Who is signed in.
|
||||||
final SignedIn person,
|
final SignedIn person, {
|
||||||
) extends SessionState {
|
|
||||||
|
/// What is under way for the account (signing out, deleting it), while
|
||||||
|
/// the person's app stays; null for nothing.
|
||||||
|
final SessionAction? busy,
|
||||||
|
|
||||||
|
/// Why the last of those failed, the person still signed in; null for
|
||||||
|
/// none.
|
||||||
|
final SessionProblem? problem,
|
||||||
|
}) extends SessionState {
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [person];
|
List<Object?> get props => [person, busy, problem];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the person asked for their account, under way while their app
|
||||||
|
/// stays.
|
||||||
|
enum SessionAction() {
|
||||||
|
/// Signing out.
|
||||||
|
signOut,
|
||||||
|
|
||||||
|
/// Deleting the account.
|
||||||
|
delete,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Why signing out or deleting the account failed, the person still signed
|
||||||
|
/// in.
|
||||||
|
enum SessionProblem() {
|
||||||
|
/// Signing out failed on this device: the instance not reached signs out
|
||||||
|
/// here all the same, so only the device's own storage fails it.
|
||||||
|
signOutFailed,
|
||||||
|
|
||||||
|
/// Deleting the account failed, the instance not reached or refusing:
|
||||||
|
/// nothing deleted.
|
||||||
|
deleteFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [person] signed in with GitHub just now: before their app, a password
|
/// [person] signed in with GitHub just now: before their app, a password
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ class FakeAuthGateway({var AuthStatus _status = const SignedOut()})
|
||||||
/// How often signing out was attempted.
|
/// How often signing out was attempted.
|
||||||
int signOuts = 0;
|
int signOuts = 0;
|
||||||
|
|
||||||
|
/// Thrown by the next sign-outs while set, which then leave the person
|
||||||
|
/// signed in.
|
||||||
|
Exception? signOutError;
|
||||||
|
|
||||||
/// How often the signed-in person's account was deleted.
|
/// How often the signed-in person's account was deleted.
|
||||||
int accountDeletions = 0;
|
int accountDeletions = 0;
|
||||||
|
|
||||||
|
|
@ -86,6 +90,9 @@ class FakeAuthGateway({var AuthStatus _status = const SignedOut()})
|
||||||
@override
|
@override
|
||||||
Future<void> signOut() async {
|
Future<void> signOut() async {
|
||||||
signOuts++;
|
signOuts++;
|
||||||
|
if (signOutError case final error?) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
_set(const SignedOut());
|
_set(const SignedOut());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -69,10 +69,12 @@ void main() {
|
||||||
|
|
||||||
bloc.add(const SessionEventSignOutRequested());
|
bloc.add(const SessionEventSignOutRequested());
|
||||||
|
|
||||||
|
// The person's app stays while signing out, never the launch screen in
|
||||||
|
// between.
|
||||||
await expectLater(
|
await expectLater(
|
||||||
bloc.stream,
|
bloc.stream,
|
||||||
emitsInOrder([
|
emitsInOrder([
|
||||||
const SessionStateLoading(),
|
const SessionStateActive(stored, busy: SessionAction.signOut),
|
||||||
const SessionStateSignedOut(),
|
const SessionStateSignedOut(),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
@ -208,11 +210,65 @@ void main() {
|
||||||
await expectLater(
|
await expectLater(
|
||||||
bloc.stream,
|
bloc.stream,
|
||||||
emitsInOrder([
|
emitsInOrder([
|
||||||
const SessionStateLoading(),
|
const SessionStateActive(stored, busy: SessionAction.delete),
|
||||||
const SessionStateSignedOut(),
|
const SessionStateSignedOut(),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
expect(gateway.accountDeletions, 1);
|
expect(gateway.accountDeletions, 1);
|
||||||
expect(gateway.signIns, 0);
|
expect(gateway.signIns, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a deletion that fails keeps the person signed in, their app in '
|
||||||
|
'place, and says so; asked again, it tries again', () async {
|
||||||
|
const stored = SignedIn(authUserId: 'github-1', isIdentified: true);
|
||||||
|
start(status: stored);
|
||||||
|
bloc.add(const SessionEventStarted());
|
||||||
|
await expectLater(bloc.stream, emits(const SessionStateActive(stored)));
|
||||||
|
gateway.deletionError = Exception('No connection');
|
||||||
|
|
||||||
|
bloc.add(const SessionEventDeleteRequested());
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
bloc.stream,
|
||||||
|
emitsInOrder([
|
||||||
|
const SessionStateActive(stored, busy: SessionAction.delete),
|
||||||
|
const SessionStateActive(stored, problem: SessionProblem.deleteFailed),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(gateway.status, stored);
|
||||||
|
expect(mark.signedOut, isFalse);
|
||||||
|
|
||||||
|
gateway.deletionError = null;
|
||||||
|
bloc.add(const SessionEventDeleteRequested());
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
bloc.stream,
|
||||||
|
emitsInOrder([
|
||||||
|
const SessionStateActive(stored, busy: SessionAction.delete),
|
||||||
|
const SessionStateSignedOut(),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(gateway.accountDeletions, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a sign-out that fails keeps the person signed in, their app in '
|
||||||
|
'place, and says so', () async {
|
||||||
|
const stored = SignedIn(authUserId: 'github-1', isIdentified: true);
|
||||||
|
start(status: stored);
|
||||||
|
bloc.add(const SessionEventStarted());
|
||||||
|
await expectLater(bloc.stream, emits(const SessionStateActive(stored)));
|
||||||
|
gateway.signOutError = Exception('The keyring is locked');
|
||||||
|
|
||||||
|
bloc.add(const SessionEventSignOutRequested());
|
||||||
|
|
||||||
|
await expectLater(
|
||||||
|
bloc.stream,
|
||||||
|
emitsInOrder([
|
||||||
|
const SessionStateActive(stored, busy: SessionAction.signOut),
|
||||||
|
const SessionStateActive(stored, problem: SessionProblem.signOutFailed),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(gateway.status, stored);
|
||||||
|
expect(mark.signedOut, isFalse);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue