feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
import 'dart:async';
|
|
|
|
|
|
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
|
import 'package:halkyon_app/halkyon_app.dart';
|
|
|
|
|
import 'package:halkyon_app/testing.dart';
|
|
|
|
|
import 'package:halkyon_oidc_flutter/halkyon_oidc_flutter.dart';
|
|
|
|
|
|
|
|
|
|
/// A state at [step], [busy] or not, with [problem] or none.
|
|
|
|
|
Matcher at(SimpleLoginStep step, {bool busy = false, Exception? problem}) =>
|
|
|
|
|
isA<SimpleLoginState>()
|
|
|
|
|
.having((state) => state.step, 'step', step)
|
|
|
|
|
.having((state) => state.busy, 'busy', busy)
|
|
|
|
|
.having((state) => state.problem, 'problem', problem);
|
|
|
|
|
|
|
|
|
|
const password = 'correct horse battery';
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
late FakeAuthGateway auth;
|
|
|
|
|
late FakeSimpleLoginGateway gateway;
|
|
|
|
|
late FakeOidcBrowser browser;
|
|
|
|
|
late FakeAccountCrypto crypto;
|
|
|
|
|
late MemoryAccountKeyStore keys;
|
|
|
|
|
late SimpleLoginRepository simpleLogin;
|
|
|
|
|
|
|
|
|
|
setUp(() {
|
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>
2026-10-24 14:50:55 +02:00
|
|
|
auth = FakeAuthGateway(status: const SignedOut());
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
gateway = FakeSimpleLoginGateway(auth);
|
|
|
|
|
browser = FakeOidcBrowser();
|
|
|
|
|
crypto = FakeAccountCrypto();
|
|
|
|
|
keys = MemoryAccountKeyStore();
|
|
|
|
|
simpleLogin = SimpleLoginRepository(
|
|
|
|
|
gateway,
|
|
|
|
|
browser,
|
|
|
|
|
crypto,
|
|
|
|
|
keys,
|
|
|
|
|
auth,
|
|
|
|
|
callbackScheme: 'app.halkyon.test',
|
|
|
|
|
loopbackPort: 43823,
|
|
|
|
|
platform: TargetPlatform.android,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
SimpleLoginCubit cubitFor({bool link = false}) {
|
|
|
|
|
final cubit = SimpleLoginCubit(simpleLogin, link: link);
|
|
|
|
|
addTearDown(cubit.close);
|
|
|
|
|
return cubit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The words the check asks for, typed back right.
|
|
|
|
|
List<String> wordsAsked(SimpleLoginState state) => [
|
|
|
|
|
for (final position in state.checked) wordsOf(state.phrase)[position],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/// Makes the keys with [password] and types the phrase back.
|
|
|
|
|
Future<void> makeKeys(SimpleLoginCubit cubit) async {
|
|
|
|
|
await cubit.passwordEntered(password, password);
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.phrase));
|
|
|
|
|
expect(cubit.state.phrase, fakePhrase);
|
|
|
|
|
cubit.phraseWrittenDown();
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.phraseCheck));
|
|
|
|
|
await cubit.phraseChecked(wordsAsked(cubit.state));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
group('someone new', () {
|
|
|
|
|
test('creates an account: the suggested handle checked, the password '
|
|
|
|
|
'twice, the phrase shown and three words typed back, signed in '
|
|
|
|
|
'only at the end', () async {
|
|
|
|
|
final cubit = cubitFor();
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.browser));
|
|
|
|
|
|
|
|
|
|
await cubit.started();
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.newcomer));
|
|
|
|
|
expect(cubit.state.handle, 'anna_mueller');
|
|
|
|
|
|
|
|
|
|
cubit.createChosen();
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.handle));
|
|
|
|
|
expect(cubit.state.handleFree, isTrue);
|
|
|
|
|
|
|
|
|
|
await cubit.handleEntered(' @anna_mueller ');
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.password));
|
|
|
|
|
expect(cubit.state.handle, 'anna_mueller');
|
|
|
|
|
expect(
|
|
|
|
|
gateway.calls,
|
|
|
|
|
isNot(contains('checkHandle')),
|
|
|
|
|
reason: 'the instance suggests no handle anybody has',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await cubit.passwordEntered(password, 'correct horse batery');
|
|
|
|
|
expect(cubit.state.passwordsDiffer, isTrue);
|
|
|
|
|
await cubit.passwordEntered('too short', 'too short');
|
|
|
|
|
expect(
|
|
|
|
|
cubit.state,
|
|
|
|
|
at(
|
|
|
|
|
SimpleLoginStep.password,
|
|
|
|
|
problem: const PasswordFailed(PasswordProblem.passwordTooShort),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await cubit.passwordEntered(password, password);
|
|
|
|
|
cubit.phraseWrittenDown();
|
|
|
|
|
await cubit.phraseChecked(['wrong', 'words', 'typed']);
|
|
|
|
|
expect(cubit.state.phraseWrong, isTrue);
|
|
|
|
|
cubit.phraseShownAgain();
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.phrase));
|
|
|
|
|
expect(gateway.calls, isNot(contains('signUp')));
|
|
|
|
|
expect(auth.status, const SignedOut());
|
|
|
|
|
|
|
|
|
|
cubit.phraseWrittenDown();
|
|
|
|
|
await cubit.phraseChecked(wordsAsked(cubit.state));
|
|
|
|
|
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
|
|
|
expect(
|
|
|
|
|
auth.status,
|
|
|
|
|
const SignedIn(authUserId: 'person-1', isIdentified: true),
|
|
|
|
|
);
|
|
|
|
|
expect(gateway.accounts['person-1']!.handle, 'anna_mueller');
|
|
|
|
|
expect(keys.kept.keys.toList(), ['person-1']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('learns on the handle that one typed is taken, and goes on with '
|
|
|
|
|
'one free', () async {
|
|
|
|
|
gateway.addAccount('person-9', subject: 'other', handle: 'anna');
|
|
|
|
|
final cubit = cubitFor();
|
|
|
|
|
await cubit.started();
|
|
|
|
|
cubit.createChosen();
|
|
|
|
|
|
|
|
|
|
await cubit.handleEntered('anna');
|
|
|
|
|
expect(
|
|
|
|
|
cubit.state,
|
|
|
|
|
at(
|
|
|
|
|
SimpleLoginStep.handle,
|
|
|
|
|
problem: const SimpleLoginFailed(SimpleLoginProblem.handleTaken),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(cubit.state.handleFree, isFalse);
|
|
|
|
|
|
|
|
|
|
await cubit.handleEntered('admin');
|
|
|
|
|
expect(
|
|
|
|
|
cubit.state.problem,
|
|
|
|
|
const SimpleLoginFailed(SimpleLoginProblem.handleReserved),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await cubit.handleEntered('anna_m');
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.password));
|
|
|
|
|
expect(cubit.state.handleFree, isTrue);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('goes back to the handle when it was taken meanwhile, then on with '
|
|
|
|
|
'the keys made, once changed', () async {
|
|
|
|
|
final cubit = cubitFor();
|
|
|
|
|
await cubit.started();
|
|
|
|
|
cubit.createChosen();
|
|
|
|
|
await cubit.handleEntered('anna_mueller');
|
|
|
|
|
gateway.addAccount('person-9', subject: 'other', handle: 'anna_mueller');
|
|
|
|
|
|
|
|
|
|
await makeKeys(cubit);
|
|
|
|
|
expect(
|
|
|
|
|
cubit.state,
|
|
|
|
|
at(
|
|
|
|
|
SimpleLoginStep.handle,
|
|
|
|
|
problem: const SimpleLoginFailed(SimpleLoginProblem.handleTaken),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await cubit.handleEntered('anna_m');
|
|
|
|
|
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
|
|
|
final person = auth.usedSessions.single.authUserId;
|
|
|
|
|
expect(gateway.accounts[person]!.handle, 'anna_m');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('links the account they have again, by its handle and recovery '
|
|
|
|
|
'phrase, refusing a phrase that does not fit', () async {
|
|
|
|
|
final material = await crypto.newKeys(password);
|
|
|
|
|
gateway.addAccount(
|
|
|
|
|
'person-9',
|
|
|
|
|
subject: 'simplelogin-lost',
|
|
|
|
|
handle: 'anna',
|
|
|
|
|
keys: material.setup,
|
|
|
|
|
);
|
|
|
|
|
final cubit = cubitFor();
|
|
|
|
|
await cubit.started();
|
|
|
|
|
|
|
|
|
|
cubit.linkChosen();
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.mend));
|
|
|
|
|
await cubit.mendEntered('anna', otherFakePhrase);
|
|
|
|
|
expect(
|
|
|
|
|
cubit.state,
|
|
|
|
|
at(
|
|
|
|
|
SimpleLoginStep.mend,
|
|
|
|
|
problem: const PasswordFailed(PasswordProblem.phraseDoesNotFit),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
cubit.choiceShownAgain();
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.newcomer));
|
|
|
|
|
cubit.linkChosen();
|
|
|
|
|
|
|
|
|
|
await cubit.mendEntered('@anna', fakePhrase);
|
|
|
|
|
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
|
|
|
expect(gateway.linked, {'simplelogin-anna': 'person-9'});
|
|
|
|
|
expect(keys.kept, {'person-9': material.keys});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('begins again once their sign-in ran out', () async {
|
|
|
|
|
final cubit = cubitFor();
|
|
|
|
|
await cubit.started();
|
|
|
|
|
cubit.linkChosen();
|
|
|
|
|
gateway.failing = const SimpleLoginFailed(SimpleLoginProblem.startAgain);
|
|
|
|
|
|
|
|
|
|
await cubit.mendEntered('anna', fakePhrase);
|
|
|
|
|
expect(
|
|
|
|
|
cubit.state,
|
|
|
|
|
at(
|
|
|
|
|
SimpleLoginStep.browser,
|
|
|
|
|
problem: const SimpleLoginFailed(SimpleLoginProblem.startAgain),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
gateway.failing = null;
|
|
|
|
|
await cubit.started();
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.newcomer));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
group('an account', () {
|
|
|
|
|
test('opens its keys with the password, refusing a wrong one', () async {
|
|
|
|
|
final material = await crypto.newKeys(password);
|
|
|
|
|
gateway.addAccount(
|
|
|
|
|
'person-9',
|
|
|
|
|
subject: 'simplelogin-anna',
|
|
|
|
|
handle: 'anna',
|
|
|
|
|
keys: material.setup,
|
|
|
|
|
);
|
|
|
|
|
final cubit = cubitFor();
|
|
|
|
|
await cubit.started();
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.unlock));
|
|
|
|
|
expect(cubit.state.signedInAs, 'anna');
|
|
|
|
|
|
|
|
|
|
await cubit.unlockEntered('wrong horse battery');
|
|
|
|
|
expect(
|
|
|
|
|
cubit.state,
|
|
|
|
|
at(
|
|
|
|
|
SimpleLoginStep.unlock,
|
|
|
|
|
problem: const PasswordFailed(PasswordProblem.wrongCredentials),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(auth.status, const SignedOut());
|
|
|
|
|
|
|
|
|
|
await cubit.unlockEntered(password);
|
|
|
|
|
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
|
|
|
expect(
|
|
|
|
|
auth.status,
|
|
|
|
|
const SignedIn(authUserId: 'person-9', isIdentified: true),
|
|
|
|
|
);
|
|
|
|
|
expect(keys.kept, {'person-9': material.keys});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('of before, without keys, makes them with the handle it '
|
|
|
|
|
'lacks', () async {
|
|
|
|
|
gateway.addAccount('person-9', subject: 'simplelogin-anna');
|
|
|
|
|
final cubit = cubitFor();
|
|
|
|
|
await cubit.started();
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.handle));
|
|
|
|
|
|
|
|
|
|
await cubit.handleEntered('anna');
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.password));
|
|
|
|
|
expect(gateway.calls, isNot(contains('checkHandle')));
|
|
|
|
|
await makeKeys(cubit);
|
|
|
|
|
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
|
|
|
expect(gateway.accounts['person-9']!.handle, 'anna');
|
|
|
|
|
expect(gateway.accounts['person-9']!.keys, isNotNull);
|
|
|
|
|
expect(auth.usedSessions.single.authUserId, 'person-9');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
group("SimpleLogin added to the account of the device's person", () {
|
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>
2026-10-24 14:50:55 +02:00
|
|
|
test('a person without keys keeps those their device holds, and their '
|
|
|
|
|
'session, once the keys are set', () async {
|
|
|
|
|
await auth.switchToGitHub();
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
final deviceKeys = crypto.keysNumbered(1);
|
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>
2026-10-24 14:50:55 +02:00
|
|
|
keys.kept['github-person'] = deviceKeys;
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
final cubit = cubitFor(link: true);
|
|
|
|
|
|
|
|
|
|
await cubit.started();
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.handle));
|
|
|
|
|
await cubit.handleEntered('anna');
|
|
|
|
|
await makeKeys(cubit);
|
|
|
|
|
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
|
|
|
expect(
|
|
|
|
|
auth.status,
|
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>
2026-10-24 14:50:55 +02:00
|
|
|
const SignedIn(authUserId: 'github-person', isIdentified: true),
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
);
|
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>
2026-10-24 14:50:55 +02:00
|
|
|
expect(keys.kept, {'github-person': deviceKeys});
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
expect(auth.usedSessions, isEmpty);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('an account with keys is done at once', () async {
|
|
|
|
|
final material = await crypto.newKeys(password);
|
|
|
|
|
auth = FakeAuthGateway(
|
|
|
|
|
status: const SignedIn(authUserId: 'person-9', isIdentified: true),
|
|
|
|
|
);
|
|
|
|
|
gateway = FakeSimpleLoginGateway(auth);
|
|
|
|
|
gateway.accounts['person-9'] = FakeSimpleLoginAccount(
|
|
|
|
|
handle: 'anna',
|
|
|
|
|
keys: material.setup,
|
|
|
|
|
);
|
|
|
|
|
simpleLogin = SimpleLoginRepository(
|
|
|
|
|
gateway,
|
|
|
|
|
browser,
|
|
|
|
|
crypto,
|
|
|
|
|
keys,
|
|
|
|
|
auth,
|
|
|
|
|
callbackScheme: 'app.halkyon.test',
|
|
|
|
|
loopbackPort: 43823,
|
|
|
|
|
platform: TargetPlatform.android,
|
|
|
|
|
);
|
|
|
|
|
final cubit = cubitFor(link: true);
|
|
|
|
|
|
|
|
|
|
await cubit.started();
|
|
|
|
|
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.done));
|
|
|
|
|
expect(gateway.linked, {'simplelogin-anna': 'person-9'});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
group('the browser', () {
|
|
|
|
|
test('closed, the sign-in ends with nothing done', () async {
|
|
|
|
|
browser.comesBack = () => null;
|
|
|
|
|
final cubit = cubitFor();
|
|
|
|
|
|
|
|
|
|
await cubit.started();
|
|
|
|
|
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.closed));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("refused on SimpleLogin's page, says so and can begin "
|
|
|
|
|
'again', () async {
|
|
|
|
|
browser.comesBack = () => const OidcDenied('access_denied');
|
|
|
|
|
final cubit = cubitFor();
|
|
|
|
|
|
|
|
|
|
await cubit.started();
|
|
|
|
|
expect(
|
|
|
|
|
cubit.state,
|
|
|
|
|
at(
|
|
|
|
|
SimpleLoginStep.browser,
|
|
|
|
|
problem: const SimpleLoginFailed(SimpleLoginProblem.denied),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
browser.comesBack = null;
|
|
|
|
|
await cubit.started();
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.newcomer));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('is no longer waited for once the sheet closes', () async {
|
|
|
|
|
final waiting = Completer<OidcCallback?>();
|
|
|
|
|
browser.comesBack = () => waiting.future;
|
|
|
|
|
final cubit = SimpleLoginCubit(simpleLogin);
|
|
|
|
|
|
|
|
|
|
unawaited(cubit.started());
|
|
|
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
|
expect(cubit.state, at(SimpleLoginStep.browser, busy: true));
|
|
|
|
|
await cubit.close();
|
|
|
|
|
|
|
|
|
|
expect(browser.cancelled, 1);
|
|
|
|
|
waiting.complete(null);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('is no longer waited for once cancelled', () async {
|
|
|
|
|
final cubit = cubitFor();
|
|
|
|
|
|
|
|
|
|
await cubit.cancelled();
|
|
|
|
|
|
|
|
|
|
expect(browser.cancelled, 1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|