halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
import 'package:flutter/widgets.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';
|
feat(app): sign in with a password and TOTP on the device
ADR 0017, step 3: halkyon_app's side, which Learn and Todo take.
- The account's cryptography on the device (`SodiumAccountCrypto`):
halkyon_keys and SRP's client half, each step on an isolate of its
own. sodium hands out results as views of native memory its
finalizers free once the isolate that made them ends, so everything
crossing back is copied first: a view read freed memory and crashed
the process.
- `PasswordGateway` on the module's endpoints (steps without a session
through a client holding none; an authenticator set up with a session
granted but not yet the device's), and `PasswordRepository` running
the flows: signing in (M2 checked, the keys opened before anything is
trusted), signing up, a password for someone signed in, an account
never finished, the authenticators, recovery codes, changing the
password, a new recovery phrase, and getting back in.
- The unlocked keys in the system's keyring per instance and person
(`SecureAccountKeyStore`), gone once another person signs in or
nobody, and with the instance's removal.
- The screens: the start screen's Sign in and Create an account (and
when GitHub sign-in ends), the sign-in sheet (codes, recovery codes,
an account never finished, a lost authenticator), the account's setup
(the recovery phrase shown once, three random words typed back before
anything reaches the instance; the authenticator's QR code, drawn
with pub.dev's qr; the recovery codes), getting back in, and the
account section (`AccountSecuritySection`).
- `FileSaver` moves from Learn into halkyon_app, for the phrase and the
codes in Todo too; `AppDependencies` names it and `passwords`.
- halkyon_keys rebuilds account keys from the secret keys a device kept;
the key proof's message is in halkyon_base_client, checked against
the register's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 15:42:45 +02:00
|
|
|
import 'package:halkyon_app/src/files/file_saver.dart';
|
|
|
|
|
import 'package:halkyon_app/src/password/data/password_repository.dart';
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
|
|
|
|
|
/// What can be closed once it is no longer needed: an app's dependencies on
|
|
|
|
|
/// an instance, or a person's.
|
|
|
|
|
abstract interface class Disposable() {
|
|
|
|
|
/// Closes it; nothing of it is used afterwards.
|
|
|
|
|
Future<void> dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// What this package needs of an app's dependencies on one instance (its
|
|
|
|
|
/// composition root), which `InstanceGate` opens for the instance in use and
|
|
|
|
|
/// [AppScope] hands down. An app's own class implements it beside its
|
|
|
|
|
/// features' repositories.
|
|
|
|
|
abstract interface class AppDependencies<P extends Disposable>()
|
|
|
|
|
implements Disposable {
|
|
|
|
|
/// The instance's name as people know it (its host), for messages about
|
|
|
|
|
/// it.
|
|
|
|
|
String get serverName;
|
|
|
|
|
|
|
|
|
|
/// Who is signed in, and signing in and out.
|
|
|
|
|
AuthRepository get auth;
|
|
|
|
|
|
|
|
|
|
/// Whether the person here signed out, so the app asks how to start.
|
|
|
|
|
SignedOutMark get signedOut;
|
|
|
|
|
|
feat(app): sign in with a password and TOTP on the device
ADR 0017, step 3: halkyon_app's side, which Learn and Todo take.
- The account's cryptography on the device (`SodiumAccountCrypto`):
halkyon_keys and SRP's client half, each step on an isolate of its
own. sodium hands out results as views of native memory its
finalizers free once the isolate that made them ends, so everything
crossing back is copied first: a view read freed memory and crashed
the process.
- `PasswordGateway` on the module's endpoints (steps without a session
through a client holding none; an authenticator set up with a session
granted but not yet the device's), and `PasswordRepository` running
the flows: signing in (M2 checked, the keys opened before anything is
trusted), signing up, a password for someone signed in, an account
never finished, the authenticators, recovery codes, changing the
password, a new recovery phrase, and getting back in.
- The unlocked keys in the system's keyring per instance and person
(`SecureAccountKeyStore`), gone once another person signs in or
nobody, and with the instance's removal.
- The screens: the start screen's Sign in and Create an account (and
when GitHub sign-in ends), the sign-in sheet (codes, recovery codes,
an account never finished, a lost authenticator), the account's setup
(the recovery phrase shown once, three random words typed back before
anything reaches the instance; the authenticator's QR code, drawn
with pub.dev's qr; the recovery codes), getting back in, and the
account section (`AccountSecuritySection`).
- `FileSaver` moves from Learn into halkyon_app, for the phrase and the
codes in Todo too; `AppDependencies` names it and `passwords`.
- halkyon_keys rebuilds account keys from the secret keys a device kept;
the key proof's message is in halkyon_base_client, checked against
the register's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 15:42:45 +02:00
|
|
|
/// Signing in with a password and TOTP, signing up, getting back in, and
|
|
|
|
|
/// what keeps an account (ADR 0017).
|
|
|
|
|
PasswordRepository get passwords;
|
|
|
|
|
|
|
|
|
|
/// Keeps a file the app made where the person chooses (a recovery phrase,
|
|
|
|
|
/// recovery codes, an app's exports).
|
|
|
|
|
FileSaver get fileSaver;
|
|
|
|
|
|
refactor(halkyon_app): the account section, About, the language and countries' names, shared
What Learn and Todo each held now comes from halkyon_app:
- AccountTiles: whose account on which instance, switching, signing
out (asking first for an anonymous account, as it deletes it) and
deleting it (asking first), the app's own words passed in.
- showAppAbout: the app's name, version, icon and legalese as it gives
them, the source of what the instance runs (AppDependencies gains
sourceCodeUrl), the app's own parts, the licence list.
- resolveAppLocale: the first preferred language the app speaks, never
by country alone, British English otherwise.
- AssetCountryNames, the default of InstanceGate and InstanceScope:
Unicode CLDR 48.2's territory names in English and German
(tool/country_names.py, 7.6 KB), Hong Kong, Macao, Myanmar and
Palestine by their short names as Learn's world map has them, so
Todo says "Germany (EU)" where it said "DE (EU)".
Learn's texts read as before in every language; its About bloc and
gateway, its country names from the world map and the keys only the
moved code used are gone, as are Todo's copies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 23:51:52 +02:00
|
|
|
/// Where the source of what the instance runs is, as it names it (its
|
|
|
|
|
/// basics, halkyon_base's): both licences require offering it to the
|
|
|
|
|
/// people who use the instance, which About does (`showAppAbout`).
|
|
|
|
|
Future<String> sourceCodeUrl();
|
|
|
|
|
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
/// Opens the dependencies of [person], once the previous person's have
|
|
|
|
|
/// closed; `PersonScope` disposes them when the person goes.
|
|
|
|
|
Future<P> openPerson(SignedIn person);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Hands an app's [dependencies] on the instance in use down the widget
|
|
|
|
|
/// tree; `InstanceGate` puts it above the app.
|
|
|
|
|
class const AppScope({
|
|
|
|
|
/// The app's dependencies.
|
|
|
|
|
required final AppDependencies<Disposable> dependencies,
|
|
|
|
|
required super.child,
|
|
|
|
|
super.key,
|
|
|
|
|
}) extends InheritedWidget {
|
|
|
|
|
/// The dependencies above [context], as the app's own type [D]
|
|
|
|
|
/// (`AppScope.of<Dependencies>(context)`), or as [AppDependencies] where
|
|
|
|
|
/// that is all that is needed.
|
|
|
|
|
///
|
|
|
|
|
/// Doesn't register [context] for rebuilds: the dependencies of one app run
|
|
|
|
|
/// never change.
|
|
|
|
|
static D of<D extends AppDependencies<Disposable>>(BuildContext context) {
|
|
|
|
|
final scope = context.getInheritedWidgetOfExactType<AppScope>();
|
|
|
|
|
assert(scope != null, 'No AppScope above this widget.');
|
|
|
|
|
return scope!.dependencies as D;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
bool updateShouldNotify(AppScope oldWidget) =>
|
|
|
|
|
dependencies != oldWidget.dependencies;
|
|
|
|
|
}
|