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
62 changed files with 981 additions and 3339 deletions
|
|
@ -37,9 +37,14 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
|
|||
`SubdivisionNames`).
|
||||
- **Gates** (`lib/src/app/`): `InstanceGate` opens the app's dependencies on
|
||||
the instance in use (`AppDependencies`, handed down by `AppScope`), anew on
|
||||
a switch, the previous ones closed first; `SessionGate` keeps a person
|
||||
signed in, signing in anonymously at a first start and asking how to start
|
||||
once signed out (`StartScreen`); `PersonScope` opens each person's own
|
||||
a switch, the previous ones closed first; `SessionGate` shows the app of
|
||||
whoever is signed in and asks how to start while nobody is
|
||||
(`StartScreen`: SimpleLogin, a password, GitHub for those who have it,
|
||||
another instance; an instance out of reach is said so, with Try again,
|
||||
and one offering no way to create an account too), at a first start as
|
||||
after signing out; an anonymous account's session, left from before
|
||||
there were none, is deleted as the app starts (ADR 0020, D25), falling
|
||||
back to signing out on the device; `PersonScope` opens each person's own
|
||||
dependencies and closes them when the person goes. `resolveAppLocale`
|
||||
picks the app's language as every Halkyon app does: the first preferred
|
||||
language it speaks, never by country alone, British English otherwise.
|
||||
|
|
@ -48,9 +53,9 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
|
|||
instance runs is (`InstanceSourceCode`, from `AppDependencies.sourceCodeUrl`;
|
||||
both licences require offering it), the app's own parts (its credits), and
|
||||
the licence list.
|
||||
- **The account** (`AccountTiles`): whose account on which instance,
|
||||
switching to the account on another, signing out (which of an anonymous
|
||||
account deletes it, so asks first) and deleting it (after asking), for an
|
||||
- **The account** (`AccountTiles`): on which instance the account is signed
|
||||
in, switching to the account on another, signing out and deleting it
|
||||
(after asking), for an
|
||||
app's settings, in the app's words where only it can say them. While one
|
||||
runs, the person's app stays and its tile shows a spinner; one that fails
|
||||
keeps them signed in and says why once, in a snack bar (in a sheet, give
|
||||
|
|
@ -58,26 +63,20 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
|
|||
the same (Serverpod's `signOutDevice`), so only the device's own storage
|
||||
fails a sign-out.
|
||||
- **Signing in** (`lib/src/auth/`): `AuthRepository` on an `AuthGateway`;
|
||||
`ServerpodAuthGateway` signs in anonymously and with GitHub (the system
|
||||
browser and a loopback address on Linux and Windows, halkyon_oidc_flutter's
|
||||
`LoopbackWebAuth`),
|
||||
`ServerpodAuthGateway` signs in with GitHub those who have it, as it is
|
||||
added to nobody any more (ADR 0020, D25; the system browser and a loopback
|
||||
address on Linux and Windows, halkyon_oidc_flutter's `LoopbackWebAuth`),
|
||||
through the instance's `halkyon_base`, and puts a session granted elsewhere
|
||||
in place (`useSession`); `SessionBloc`, `GitHubSignInBloc` and
|
||||
`SignInWaysCubit`; `SignedOutMark`, `SignInAddresses`, `PersonFolders`.
|
||||
Where the instance offers passwords, GitHub is added to nobody (ADR 0017):
|
||||
`gitHubOffer` says so from its basics, `GitHubSignInStateReady.linking`
|
||||
hands it to the screens, which then offer no Add, and the start screen
|
||||
offers an account to create in place of the app's sentence on adding
|
||||
GitHub; `gitHubProblemText` says why a GitHub sign-in was refused.
|
||||
in place (`useSession`); `SessionBloc` and `SignInWaysCubit`;
|
||||
`SignInAddresses`, `PersonFolders`. The start screen says why a GitHub
|
||||
sign-in was refused, and for a GitHub account nobody here has, the way to
|
||||
create an account in its place.
|
||||
- **Content sealed with data keys** (`lib/src/privacy/`, ADR 0019):
|
||||
`ContentCipher` (`SodiumContentCipher`, on the isolate it is used on, as an
|
||||
app seals and opens rows one by one; `FakeContentCipher` in tests) makes a
|
||||
store's data keys, sealed to the account and signed by it, opens them, and
|
||||
seals and opens a row's content for the row it is stored in (its table, id
|
||||
and column), the payload holding the signature before the ciphertext. An
|
||||
anonymous person's device makes key pairs of its own
|
||||
(`AccountCrypto.newDeviceKeys`), which `makePassword` keeps as the account's
|
||||
once they set a password.
|
||||
and column), the payload holding the signature before the ciphertext.
|
||||
- **A password and TOTP** (`lib/src/password/`, ADR 0017): `PasswordRepository`
|
||||
on a `PasswordGateway` (`ServerpodPasswordGateway`), an `AccountCrypto`
|
||||
(`SodiumAccountCrypto`: halkyon_keys and SRP's client half, each step on an
|
||||
|
|
@ -125,9 +124,8 @@ Written as the apps are: Dart 3.13's primary constructors, Material from
|
|||
code or opened; and anything a step didn't expect (bytes of the wrong
|
||||
length from an instance, libsodium's or the keyring's own errors) fails it
|
||||
rather than leaving its sheet busy. `register_interop_test.dart` holds the
|
||||
real cryptography to the register's own checks. Anonymous people get no
|
||||
keys until something is encrypted (step 4). Every build of an app needs a C
|
||||
compiler and make: sodium's build hook compiles libsodium.
|
||||
real cryptography to the register's own checks. Every build of an app
|
||||
needs a C compiler and make: sodium's build hook compiles libsodium.
|
||||
- **Signing in with SimpleLogin** (`lib/src/simplelogin/`, ADR 0020):
|
||||
`SimpleLoginRepository` on a `SimpleLoginGateway` (`ServerpodSimpleLoginGateway`,
|
||||
halkyon_base's `simpleLogin` endpoint) and halkyon_oidc_flutter's
|
||||
|
|
@ -143,18 +141,17 @@ 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. The
|
||||
device's person instead. 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
|
||||
once it closes. Adding it to an account (ADR 0020, D21): 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`).
|
||||
`SimpleLoginStandingScope` is above it.
|
||||
- **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
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@
|
|||
/// - **The account and About**: `AccountTiles` for an app's settings,
|
||||
/// `showAppAbout` with the source of what the instance runs, and
|
||||
/// `resolveAppLocale`, the language every Halkyon app picks.
|
||||
/// - **Signing in**: anonymously, with GitHub (`AuthRepository` on
|
||||
/// `ServerpodAuthGateway`), and with a password and TOTP (ADR 0017:
|
||||
/// `PasswordRepository`, its sheets and `AccountSecuritySection`), the
|
||||
/// account's keys on the device.
|
||||
/// - **Signing in**: with SimpleLogin (ADR 0020: `SimpleLoginRepository`,
|
||||
/// its sheet and `SimpleLoginSection`), with a password and TOTP (ADR
|
||||
/// 0017: `PasswordRepository`, its sheets and `AccountSecuritySection`),
|
||||
/// and with GitHub while it lasts (`AuthRepository` on
|
||||
/// `ServerpodAuthGateway`), the account's keys on the device. Every
|
||||
/// account has a way in: there are no anonymous ones (ADR 0020, D4).
|
||||
/// - **Sync**: `SyncService` on triggers, the changes still waiting, and
|
||||
/// where it stands (`SyncStatusBloc`, `SyncStatusCard`, `AppMismatchBanner`,
|
||||
/// `SyncTroubleBadge`).
|
||||
|
|
@ -30,21 +32,17 @@ export 'src/app/app_locale.dart';
|
|||
export 'src/app/app_scope.dart';
|
||||
export 'src/app/instance_gate.dart';
|
||||
export 'src/app/person_scope.dart';
|
||||
export 'src/auth/bloc/github_sign_in_bloc.dart';
|
||||
export 'src/auth/bloc/session_bloc.dart';
|
||||
export 'src/auth/bloc/sign_in_ways_cubit.dart';
|
||||
export 'src/auth/data/auth_gateway.dart';
|
||||
export 'src/auth/data/auth_repository.dart';
|
||||
export 'src/auth/data/serverpod_auth_gateway.dart';
|
||||
export 'src/auth/data/sign_in_addresses.dart';
|
||||
export 'src/auth/data/signed_out_mark.dart';
|
||||
export 'src/auth/model/auth_status.dart';
|
||||
export 'src/auth/model/github_link_outcome.dart';
|
||||
export 'src/auth/model/github_sign_in.dart';
|
||||
export 'src/auth/model/session_grant.dart';
|
||||
export 'src/auth/model/sign_in_ways.dart';
|
||||
export 'src/auth/widget/account_tiles.dart';
|
||||
export 'src/auth/widget/github_texts.dart';
|
||||
export 'src/auth/widget/session_gate.dart';
|
||||
export 'src/auth/widget/start_screen.dart';
|
||||
export 'src/base/base_client.dart';
|
||||
|
|
|
|||
|
|
@ -27,16 +27,13 @@
|
|||
"accountAdd": "Konto hinzufügen",
|
||||
"accountAddSubtitle": "Wähle die Instanz für das Konto, das du hinzufügst.",
|
||||
"accountSignedIn": "Angemeldet",
|
||||
"accountAnonymous": "Anonym",
|
||||
"accountRemoveTooltip": "Konto entfernen",
|
||||
"accountRemoveTitle": "Konto entfernen?",
|
||||
"accountRemoveMessage": "Entfernt das Konto auf {name} von diesem Gerät, mit allem, was das Gerät davon hat. Was es synchronisiert hat, bleibt auf {name}: Melde dich dort wieder an, um es wiederzuhaben. In ein anonymes Konto kann man sich nicht wieder anmelden.",
|
||||
"accountRemoveMessage": "Entfernt das Konto auf {name} von diesem Gerät, mit allem, was das Gerät davon hat. Was es synchronisiert hat, bleibt auf {name}: Melde dich dort wieder an, um es wiederzuhaben.",
|
||||
"accountRemoveConfirm": "Entfernen",
|
||||
"accountSignedInOn": "Angemeldet auf {instance}",
|
||||
"accountAnonymousOn": "Anonym auf {instance}",
|
||||
"accountSwitch": "Konto wechseln",
|
||||
"accountSignOut": "Abmelden",
|
||||
"accountSignOutTitle": "Abmelden?",
|
||||
"accountDelete": "Konto löschen",
|
||||
"accountDeleteTitle": "Dein Konto löschen?",
|
||||
"accountSignOutFailed": "Abmelden auf diesem Gerät hat nicht geklappt. Versuch es noch einmal.",
|
||||
|
|
@ -48,16 +45,14 @@
|
|||
"startSignedOut": "Abgemeldet",
|
||||
"startOn": "Starte auf {instance}:",
|
||||
"startWithGitHub": "Mit GitHub anmelden",
|
||||
"startWithoutAccount": "Ohne Konto starten",
|
||||
"startAnonymous": "Dein Fortschritt wird auf diesem Gerät gespeichert. Melde dich an, um ihn zu behalten.",
|
||||
"startGitHubFailed": "Anmeldung mit GitHub fehlgeschlagen.",
|
||||
"startNoGitHubAccount": "Noch kein Konto hier meldet sich mit diesem GitHub-Konto an.",
|
||||
"startNoGitHubAccount": "Kein Konto hier meldet sich mit diesem GitHub-Konto an.",
|
||||
"gitHubCreateAccountInstead": "Erstelle stattdessen ein Konto mit deiner E-Mail-Adresse und einem Passwort.",
|
||||
"gitHubNoAccountSetUpPassword": "Noch kein Konto hier meldet sich mit diesem GitHub-Konto an. Richte stattdessen ein Passwort ein, um deinen Fortschritt zu behalten.",
|
||||
"gitHubSimpleLoginInstead": "Mit SimpleLogin kannst du stattdessen eins erstellen.",
|
||||
"gitHubUsePassword": "Dieses Konto meldet sich jetzt mit seiner E-Mail-Adresse und seinem Passwort an, nicht mehr mit GitHub.",
|
||||
"gitHubDeprecated": "GitHub lässt sich nicht mehr als Anmeldeweg hinzufügen: Deine E-Mail-Adresse und dein Passwort sind es.",
|
||||
"gitHubEnded": "Die Anmeldung mit GitHub ist auf dieser Instanz beendet.",
|
||||
"startOtherInstance": "Andere Instanz verwenden",
|
||||
"startNoWay": "{instance} bietet keinen Weg, ein Konto zu erstellen.",
|
||||
"syncTitle": "Synchronisierung",
|
||||
"syncNow": "Jetzt synchronisieren",
|
||||
"syncStateSyncing": "Wird synchronisiert…",
|
||||
|
|
|
|||
|
|
@ -116,10 +116,9 @@
|
|||
"accountAdd": "Add account",
|
||||
"accountAddSubtitle": "Choose the instance for the account to add.",
|
||||
"accountSignedIn": "Signed in",
|
||||
"accountAnonymous": "Anonymous",
|
||||
"accountRemoveTooltip": "Remove account",
|
||||
"accountRemoveTitle": "Remove account?",
|
||||
"accountRemoveMessage": "Removes the account on {name} from this device, with all the device holds of it. What it synced stays on {name}: sign in there again to have it. An anonymous account can't be signed into again.",
|
||||
"accountRemoveMessage": "Removes the account on {name} from this device, with all the device holds of it. What it synced stays on {name}: sign in there again to have it.",
|
||||
"@accountRemoveMessage": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
|
|
@ -136,17 +135,8 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"accountAnonymousOn": "Anonymous on {instance}",
|
||||
"@accountAnonymousOn": {
|
||||
"placeholders": {
|
||||
"instance": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"accountSwitch": "Switch account",
|
||||
"accountSignOut": "Sign out",
|
||||
"accountSignOutTitle": "Sign out?",
|
||||
"accountDelete": "Delete account",
|
||||
"accountDeleteTitle": "Delete your account?",
|
||||
"accountSignOutFailed": "Couldn't sign out on this device. Try again.",
|
||||
|
|
@ -165,22 +155,28 @@
|
|||
}
|
||||
},
|
||||
"startWithGitHub": "Sign in with GitHub",
|
||||
"startWithoutAccount": "Start without an account",
|
||||
"startAnonymous": "Progress is saved on this device. Sign in to keep it.",
|
||||
"startGitHubFailed": "Couldn't sign in with GitHub.",
|
||||
"startNoGitHubAccount": "No account here signs in with this GitHub account yet.",
|
||||
"startNoGitHubAccount": "No account here signs in with this GitHub account.",
|
||||
"gitHubCreateAccountInstead": "Create an account with your email address and a password instead.",
|
||||
"@gitHubCreateAccountInstead": {
|
||||
"description": "Said after a GitHub account nobody on the instance signs in with, where people sign in with a password and GitHub is added to nobody (ADR 0017)."
|
||||
},
|
||||
"gitHubNoAccountSetUpPassword": "No account here signs in with this GitHub account yet. To keep your progress, set up a password instead.",
|
||||
"@gitHubNoAccountSetUpPassword": {
|
||||
"description": "An anonymous person signed in with a GitHub account nobody here has, where GitHub is added to nobody: the password set up below keeps their account."
|
||||
"gitHubSimpleLoginInstead": "Continue with SimpleLogin to create one instead.",
|
||||
"@gitHubSimpleLoginInstead": {
|
||||
"description": "Said after a GitHub account nobody on the instance signs in with, where the instance offers SimpleLogin (ADR 0020): GitHub makes no account."
|
||||
},
|
||||
"gitHubUsePassword": "This account signs in with its email address and password now, not with GitHub.",
|
||||
"gitHubDeprecated": "GitHub can't be added as a way to sign in any more: your email address and password are.",
|
||||
"gitHubEnded": "Sign-in with GitHub has ended on this instance.",
|
||||
"startOtherInstance": "Use another instance",
|
||||
"startNoWay": "{instance} offers no way to create an account.",
|
||||
"@startNoWay": {
|
||||
"description": "On the start screen, where the instance offers neither SimpleLogin nor a password (ADR 0020, D4): nobody can make an account there.",
|
||||
"placeholders": {
|
||||
"instance": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"syncTitle": "Sync",
|
||||
"syncNow": "Sync now",
|
||||
"syncStateSyncing": "Syncing…",
|
||||
|
|
|
|||
|
|
@ -265,12 +265,6 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'Signed in'**
|
||||
String get accountSignedIn;
|
||||
|
||||
/// No description provided for @accountAnonymous.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Anonymous'**
|
||||
String get accountAnonymous;
|
||||
|
||||
/// No description provided for @accountRemoveTooltip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -286,7 +280,7 @@ abstract class HalkyonAppLocalizations {
|
|||
/// No description provided for @accountRemoveMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Removes the account on {name} from this device, with all the device holds of it. What it synced stays on {name}: sign in there again to have it. An anonymous account can\'t be signed into again.'**
|
||||
/// **'Removes the account on {name} from this device, with all the device holds of it. What it synced stays on {name}: sign in there again to have it.'**
|
||||
String accountRemoveMessage(String name);
|
||||
|
||||
/// No description provided for @accountRemoveConfirm.
|
||||
|
|
@ -301,12 +295,6 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'Signed in on {instance}'**
|
||||
String accountSignedInOn(String instance);
|
||||
|
||||
/// No description provided for @accountAnonymousOn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Anonymous on {instance}'**
|
||||
String accountAnonymousOn(String instance);
|
||||
|
||||
/// No description provided for @accountSwitch.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -319,12 +307,6 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'Sign out'**
|
||||
String get accountSignOut;
|
||||
|
||||
/// No description provided for @accountSignOutTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sign out?'**
|
||||
String get accountSignOutTitle;
|
||||
|
||||
/// No description provided for @accountDelete.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -391,18 +373,6 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'Sign in with GitHub'**
|
||||
String get startWithGitHub;
|
||||
|
||||
/// No description provided for @startWithoutAccount.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Start without an account'**
|
||||
String get startWithoutAccount;
|
||||
|
||||
/// No description provided for @startAnonymous.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Progress is saved on this device. Sign in to keep it.'**
|
||||
String get startAnonymous;
|
||||
|
||||
/// No description provided for @startGitHubFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -412,7 +382,7 @@ abstract class HalkyonAppLocalizations {
|
|||
/// No description provided for @startNoGitHubAccount.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No account here signs in with this GitHub account yet.'**
|
||||
/// **'No account here signs in with this GitHub account.'**
|
||||
String get startNoGitHubAccount;
|
||||
|
||||
/// Said after a GitHub account nobody on the instance signs in with, where people sign in with a password and GitHub is added to nobody (ADR 0017).
|
||||
|
|
@ -421,11 +391,11 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'Create an account with your email address and a password instead.'**
|
||||
String get gitHubCreateAccountInstead;
|
||||
|
||||
/// An anonymous person signed in with a GitHub account nobody here has, where GitHub is added to nobody: the password set up below keeps their account.
|
||||
/// Said after a GitHub account nobody on the instance signs in with, where the instance offers SimpleLogin (ADR 0020): GitHub makes no account.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No account here signs in with this GitHub account yet. To keep your progress, set up a password instead.'**
|
||||
String get gitHubNoAccountSetUpPassword;
|
||||
/// **'Continue with SimpleLogin to create one instead.'**
|
||||
String get gitHubSimpleLoginInstead;
|
||||
|
||||
/// No description provided for @gitHubUsePassword.
|
||||
///
|
||||
|
|
@ -433,12 +403,6 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'This account signs in with its email address and password now, not with GitHub.'**
|
||||
String get gitHubUsePassword;
|
||||
|
||||
/// No description provided for @gitHubDeprecated.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'GitHub can\'t be added as a way to sign in any more: your email address and password are.'**
|
||||
String get gitHubDeprecated;
|
||||
|
||||
/// No description provided for @gitHubEnded.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -451,6 +415,12 @@ abstract class HalkyonAppLocalizations {
|
|||
/// **'Use another instance'**
|
||||
String get startOtherInstance;
|
||||
|
||||
/// On the start screen, where the instance offers neither SimpleLogin nor a password (ADR 0020, D4): nobody can make an account there.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{instance} offers no way to create an account.'**
|
||||
String startNoWay(String instance);
|
||||
|
||||
/// No description provided for @syncTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -113,9 +113,6 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
@override
|
||||
String get accountSignedIn => 'Angemeldet';
|
||||
|
||||
@override
|
||||
String get accountAnonymous => 'Anonym';
|
||||
|
||||
@override
|
||||
String get accountRemoveTooltip => 'Konto entfernen';
|
||||
|
||||
|
|
@ -124,7 +121,7 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
|
||||
@override
|
||||
String accountRemoveMessage(String name) {
|
||||
return 'Entfernt das Konto auf $name von diesem Gerät, mit allem, was das Gerät davon hat. Was es synchronisiert hat, bleibt auf $name: Melde dich dort wieder an, um es wiederzuhaben. In ein anonymes Konto kann man sich nicht wieder anmelden.';
|
||||
return 'Entfernt das Konto auf $name von diesem Gerät, mit allem, was das Gerät davon hat. Was es synchronisiert hat, bleibt auf $name: Melde dich dort wieder an, um es wiederzuhaben.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -135,20 +132,12 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
return 'Angemeldet auf $instance';
|
||||
}
|
||||
|
||||
@override
|
||||
String accountAnonymousOn(String instance) {
|
||||
return 'Anonym auf $instance';
|
||||
}
|
||||
|
||||
@override
|
||||
String get accountSwitch => 'Konto wechseln';
|
||||
|
||||
@override
|
||||
String get accountSignOut => 'Abmelden';
|
||||
|
||||
@override
|
||||
String get accountSignOutTitle => 'Abmelden?';
|
||||
|
||||
@override
|
||||
String get accountDelete => 'Konto löschen';
|
||||
|
||||
|
|
@ -189,36 +178,25 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
@override
|
||||
String get startWithGitHub => 'Mit GitHub anmelden';
|
||||
|
||||
@override
|
||||
String get startWithoutAccount => 'Ohne Konto starten';
|
||||
|
||||
@override
|
||||
String get startAnonymous =>
|
||||
'Dein Fortschritt wird auf diesem Gerät gespeichert. Melde dich an, um ihn zu behalten.';
|
||||
|
||||
@override
|
||||
String get startGitHubFailed => 'Anmeldung mit GitHub fehlgeschlagen.';
|
||||
|
||||
@override
|
||||
String get startNoGitHubAccount =>
|
||||
'Noch kein Konto hier meldet sich mit diesem GitHub-Konto an.';
|
||||
'Kein Konto hier meldet sich mit diesem GitHub-Konto an.';
|
||||
|
||||
@override
|
||||
String get gitHubCreateAccountInstead =>
|
||||
'Erstelle stattdessen ein Konto mit deiner E-Mail-Adresse und einem Passwort.';
|
||||
|
||||
@override
|
||||
String get gitHubNoAccountSetUpPassword =>
|
||||
'Noch kein Konto hier meldet sich mit diesem GitHub-Konto an. Richte stattdessen ein Passwort ein, um deinen Fortschritt zu behalten.';
|
||||
String get gitHubSimpleLoginInstead =>
|
||||
'Mit SimpleLogin kannst du stattdessen eins erstellen.';
|
||||
|
||||
@override
|
||||
String get gitHubUsePassword =>
|
||||
'Dieses Konto meldet sich jetzt mit seiner E-Mail-Adresse und seinem Passwort an, nicht mehr mit GitHub.';
|
||||
|
||||
@override
|
||||
String get gitHubDeprecated =>
|
||||
'GitHub lässt sich nicht mehr als Anmeldeweg hinzufügen: Deine E-Mail-Adresse und dein Passwort sind es.';
|
||||
|
||||
@override
|
||||
String get gitHubEnded =>
|
||||
'Die Anmeldung mit GitHub ist auf dieser Instanz beendet.';
|
||||
|
|
@ -226,6 +204,11 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
|
|||
@override
|
||||
String get startOtherInstance => 'Andere Instanz verwenden';
|
||||
|
||||
@override
|
||||
String startNoWay(String instance) {
|
||||
return '$instance bietet keinen Weg, ein Konto zu erstellen.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get syncTitle => 'Synchronisierung';
|
||||
|
||||
|
|
|
|||
|
|
@ -113,9 +113,6 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
@override
|
||||
String get accountSignedIn => 'Signed in';
|
||||
|
||||
@override
|
||||
String get accountAnonymous => 'Anonymous';
|
||||
|
||||
@override
|
||||
String get accountRemoveTooltip => 'Remove account';
|
||||
|
||||
|
|
@ -124,7 +121,7 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
|
||||
@override
|
||||
String accountRemoveMessage(String name) {
|
||||
return 'Removes the account on $name from this device, with all the device holds of it. What it synced stays on $name: sign in there again to have it. An anonymous account can\'t be signed into again.';
|
||||
return 'Removes the account on $name from this device, with all the device holds of it. What it synced stays on $name: sign in there again to have it.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -135,20 +132,12 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
return 'Signed in on $instance';
|
||||
}
|
||||
|
||||
@override
|
||||
String accountAnonymousOn(String instance) {
|
||||
return 'Anonymous on $instance';
|
||||
}
|
||||
|
||||
@override
|
||||
String get accountSwitch => 'Switch account';
|
||||
|
||||
@override
|
||||
String get accountSignOut => 'Sign out';
|
||||
|
||||
@override
|
||||
String get accountSignOutTitle => 'Sign out?';
|
||||
|
||||
@override
|
||||
String get accountDelete => 'Delete account';
|
||||
|
||||
|
|
@ -187,42 +176,36 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
|
|||
@override
|
||||
String get startWithGitHub => 'Sign in with GitHub';
|
||||
|
||||
@override
|
||||
String get startWithoutAccount => 'Start without an account';
|
||||
|
||||
@override
|
||||
String get startAnonymous =>
|
||||
'Progress is saved on this device. Sign in to keep it.';
|
||||
|
||||
@override
|
||||
String get startGitHubFailed => 'Couldn\'t sign in with GitHub.';
|
||||
|
||||
@override
|
||||
String get startNoGitHubAccount =>
|
||||
'No account here signs in with this GitHub account yet.';
|
||||
'No account here signs in with this GitHub account.';
|
||||
|
||||
@override
|
||||
String get gitHubCreateAccountInstead =>
|
||||
'Create an account with your email address and a password instead.';
|
||||
|
||||
@override
|
||||
String get gitHubNoAccountSetUpPassword =>
|
||||
'No account here signs in with this GitHub account yet. To keep your progress, set up a password instead.';
|
||||
String get gitHubSimpleLoginInstead =>
|
||||
'Continue with SimpleLogin to create one instead.';
|
||||
|
||||
@override
|
||||
String get gitHubUsePassword =>
|
||||
'This account signs in with its email address and password now, not with GitHub.';
|
||||
|
||||
@override
|
||||
String get gitHubDeprecated =>
|
||||
'GitHub can\'t be added as a way to sign in any more: your email address and password are.';
|
||||
|
||||
@override
|
||||
String get gitHubEnded => 'Sign-in with GitHub has ended on this instance.';
|
||||
|
||||
@override
|
||||
String get startOtherInstance => 'Use another instance';
|
||||
|
||||
@override
|
||||
String startNoWay(String instance) {
|
||||
return '$instance offers no way to create an account.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get syncTitle => 'Sync';
|
||||
|
||||
|
|
|
|||
|
|
@ -30,18 +30,6 @@ final class const AppFrame({
|
|||
/// on.").
|
||||
required final String Function(BuildContext context) firstStartSubtitle,
|
||||
|
||||
/// What switching this device to another account leaves behind, as the
|
||||
/// email sign-in says before it switches ("The studysets on this device
|
||||
/// can't be combined with it yet: if you switch, they are deleted.").
|
||||
required final String Function(BuildContext context) switchLeavesBehind,
|
||||
|
||||
/// What the start screen says after a GitHub account nobody here signs in
|
||||
/// with: how to have it anyway, naming where the app adds a way to sign in
|
||||
/// ("Start without an account, then add GitHub in Profile."). A sentence of
|
||||
/// its own, as the place takes the grammar of the app's language ("im
|
||||
/// Profil").
|
||||
required final String Function(BuildContext context) thenAddGitHub,
|
||||
|
||||
/// The app's theme in a [Brightness].
|
||||
required final ThemeData Function(Brightness brightness) theme,
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
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';
|
||||
import 'package:halkyon_app/src/files/file_saver.dart';
|
||||
import 'package:halkyon_app/src/password/data/password_repository.dart';
|
||||
|
|
@ -26,9 +25,6 @@ abstract interface class AppDependencies<P extends Disposable>()
|
|||
/// 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;
|
||||
|
||||
/// Signing in with a password and TOTP, signing up, getting back in, and
|
||||
/// what keeps an account (ADR 0017).
|
||||
PasswordRepository get passwords;
|
||||
|
|
|
|||
|
|
@ -1,210 +0,0 @@
|
|||
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/model/github_link_outcome.dart';
|
||||
import 'package:halkyon_app/src/auth/model/github_sign_in.dart';
|
||||
|
||||
/// Signs an anonymous person in with GitHub, keeping everything they have,
|
||||
/// or adds GitHub as a way in for someone who signs in with an email
|
||||
/// address.
|
||||
///
|
||||
/// If the GitHub account already belongs to another person (typically their
|
||||
/// account from another device), this device switches to that person: right
|
||||
/// away when it holds nothing (`deviceHoldsContent`), otherwise only once
|
||||
/// the person confirms that this device's content stays behind. Someone
|
||||
/// identified is never switched, as their own account would be left: they
|
||||
/// are told the GitHub account is another's.
|
||||
///
|
||||
/// Where people sign in with a password, GitHub is added to nobody (ADR
|
||||
/// 0017, D2): [GitHubSignInStateReady.linking] says so, and an anonymous
|
||||
/// person reaches only the GitHub account someone has already, by the same
|
||||
/// switch.
|
||||
class GitHubSignInBloc(
|
||||
final AuthRepository _auth, {
|
||||
|
||||
/// Whether this device holds content of the person signed in that a
|
||||
/// switch would leave behind, as the app counts it (Halkyon Learn: its
|
||||
/// studysets, those waiting in Recently deleted too).
|
||||
required final Future<bool> Function() _deviceHoldsContent,
|
||||
}) extends Bloc<GitHubSignInEvent, GitHubSignInState> {
|
||||
this : super(const GitHubSignInStateChecking()) {
|
||||
on<GitHubSignInEventStarted>(_onStarted);
|
||||
on<GitHubSignInEventRequested>(_onRequested);
|
||||
on<GitHubSignInEventSwitchConfirmed>(_onSwitchConfirmed);
|
||||
on<GitHubSignInEventSwitchDeclined>(
|
||||
(event, emit) => emit(GitHubSignInStateReady(linking: _linking)),
|
||||
);
|
||||
// The sign-in in progress then ends as cancelled.
|
||||
on<GitHubSignInEventCancelled>((event, emit) => _auth.cancelGitHub());
|
||||
}
|
||||
|
||||
/// Closing the account dialog cancels a sign-in still waiting for GitHub's
|
||||
/// page: nothing finishes without the dialog to show it, such as the
|
||||
/// question whether to switch.
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (state is GitHubSignInStateBusy) {
|
||||
await _auth.cancelGitHub();
|
||||
}
|
||||
await super.close();
|
||||
}
|
||||
|
||||
/// Whether this instance adds GitHub to people as a way in, as it said
|
||||
/// when the dialog started.
|
||||
bool _linking = true;
|
||||
|
||||
Future<void> _onStarted(
|
||||
GitHubSignInEventStarted event,
|
||||
Emitter<GitHubSignInState> emit,
|
||||
) async {
|
||||
try {
|
||||
final offer = await _auth.gitHubOffer();
|
||||
_linking = offer.linking;
|
||||
emit(
|
||||
offer.signIn
|
||||
? GitHubSignInStateReady(linking: _linking)
|
||||
: const GitHubSignInStateUnavailable(),
|
||||
);
|
||||
} on Exception catch (error, st) {
|
||||
// Without the instance, signing in couldn't work either.
|
||||
addError(error, st);
|
||||
emit(const GitHubSignInStateUnavailable());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onRequested(
|
||||
GitHubSignInEventRequested event,
|
||||
Emitter<GitHubSignInState> emit,
|
||||
) async {
|
||||
emit(GitHubSignInStateBusy(cancellable: _auth.canCancelGitHub));
|
||||
try {
|
||||
switch (await _auth.linkGitHub()) {
|
||||
case GitHubLinkOutcome.linked:
|
||||
emit(const GitHubSignInStateLinked());
|
||||
case GitHubLinkOutcome.cancelled:
|
||||
emit(GitHubSignInStateReady(linking: _linking));
|
||||
case GitHubLinkOutcome.takenByAnotherPerson when _auth.isIdentified:
|
||||
emit(const GitHubSignInStateTaken());
|
||||
case GitHubLinkOutcome.takenByAnotherPerson:
|
||||
if (await _deviceHoldsContent()) {
|
||||
emit(const GitHubSignInStateConfirmSwitch());
|
||||
} else {
|
||||
await _switch(emit);
|
||||
}
|
||||
}
|
||||
} on Exception catch (error, st) {
|
||||
_failed(emit, error, st);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onSwitchConfirmed(
|
||||
GitHubSignInEventSwitchConfirmed event,
|
||||
Emitter<GitHubSignInState> emit,
|
||||
) async {
|
||||
emit(GitHubSignInStateBusy(cancellable: _auth.canCancelGitHub));
|
||||
try {
|
||||
await _switch(emit);
|
||||
} on Exception catch (error, st) {
|
||||
_failed(emit, error, st);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ready again after [error]: why, where the instance said so.
|
||||
void _failed(
|
||||
Emitter<GitHubSignInState> emit,
|
||||
Exception error,
|
||||
StackTrace st,
|
||||
) {
|
||||
addError(error, st);
|
||||
emit(
|
||||
GitHubSignInStateReady(
|
||||
linking: _linking,
|
||||
failed: true,
|
||||
problem: switch (error) {
|
||||
GitHubSignInFailed(:final problem) => problem,
|
||||
_ => null,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// After a switch, the previous person's whole subtree goes away, this bloc
|
||||
// with it, so nothing is emitted then.
|
||||
Future<void> _switch(Emitter<GitHubSignInState> emit) async {
|
||||
if (!await _auth.switchToGitHub()) {
|
||||
emit(GitHubSignInStateReady(linking: _linking));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// What happens to signing in with GitHub.
|
||||
sealed class const GitHubSignInEvent() with Equatable {
|
||||
@override
|
||||
List<Object?> get props => const [];
|
||||
}
|
||||
|
||||
/// The account dialog opens: does this instance offer GitHub sign-in?
|
||||
final class const GitHubSignInEventStarted() extends GitHubSignInEvent;
|
||||
|
||||
/// Sign in with GitHub.
|
||||
final class const GitHubSignInEventRequested() extends GitHubSignInEvent;
|
||||
|
||||
/// Switch to the GitHub account's person; this device's content stays
|
||||
/// behind.
|
||||
final class const GitHubSignInEventSwitchConfirmed() extends GitHubSignInEvent;
|
||||
|
||||
/// Stay anonymous.
|
||||
final class const GitHubSignInEventSwitchDeclined() extends GitHubSignInEvent;
|
||||
|
||||
/// Stop waiting for GitHub's page in the browser.
|
||||
final class const GitHubSignInEventCancelled() extends GitHubSignInEvent;
|
||||
|
||||
/// Signing in with GitHub, as the account dialog shows it.
|
||||
sealed class const GitHubSignInState() with Equatable {
|
||||
@override
|
||||
List<Object?> get props => const [];
|
||||
}
|
||||
|
||||
/// Asking the instance whether it offers GitHub sign-in.
|
||||
final class const GitHubSignInStateChecking() extends GitHubSignInState;
|
||||
|
||||
/// This instance has no GitHub sign-in, or can't be reached.
|
||||
final class const GitHubSignInStateUnavailable() extends GitHubSignInState;
|
||||
|
||||
/// Ready to sign in with GitHub.
|
||||
final class const GitHubSignInStateReady({
|
||||
/// Whether GitHub is added to people as a way in here: not where they
|
||||
/// sign in with a password (ADR 0017, D2), where someone identified is
|
||||
/// offered no Add.
|
||||
final bool linking = true,
|
||||
|
||||
/// Whether the last attempt failed.
|
||||
final bool failed = false,
|
||||
|
||||
/// Why it failed, where the instance said; null otherwise.
|
||||
final GitHubSignInProblem? problem,
|
||||
}) extends GitHubSignInState {
|
||||
@override
|
||||
List<Object?> get props => [linking, failed, problem];
|
||||
}
|
||||
|
||||
/// Signing in: GitHub's page is open, or the server is being asked.
|
||||
final class const GitHubSignInStateBusy({
|
||||
/// Whether the app offers to cancel, where no sign-in screen of the system
|
||||
/// does. A cancel only stops the wait for GitHub's page.
|
||||
final bool cancellable = false,
|
||||
}) extends GitHubSignInState {
|
||||
@override
|
||||
List<Object?> get props => [cancellable];
|
||||
}
|
||||
|
||||
/// The GitHub account belongs to another person and this device holds
|
||||
/// content: switch and leave it behind, or stay anonymous?
|
||||
final class const GitHubSignInStateConfirmSwitch() extends GitHubSignInState;
|
||||
|
||||
/// Linked: the person now signs in with GitHub.
|
||||
final class const GitHubSignInStateLinked() extends GitHubSignInState;
|
||||
|
||||
/// The GitHub account is another account's, and the person signed in is
|
||||
/// identified: nothing changed.
|
||||
final class const GitHubSignInStateTaken() extends GitHubSignInState;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -13,9 +13,7 @@ class SignInWaysCubit(final AuthRepository _auth)
|
|||
/// Reads it all anew.
|
||||
Future<void> load() async {
|
||||
try {
|
||||
final ways = _auth.isIdentified
|
||||
? await _auth.signInWays()
|
||||
: SignInWays.none;
|
||||
final ways = await _auth.signInWays();
|
||||
final address = await _auth.emailAddress();
|
||||
if (!isClosed) {
|
||||
emit(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import 'package:halkyon_app/src/auth/model/auth_status.dart';
|
||||
import 'package:halkyon_app/src/auth/model/github_link_outcome.dart';
|
||||
import 'package:halkyon_app/src/auth/model/github_sign_in.dart';
|
||||
import 'package:halkyon_app/src/auth/model/session_grant.dart';
|
||||
import 'package:halkyon_app/src/auth/model/sign_in_ways.dart';
|
||||
|
||||
|
|
@ -13,9 +11,6 @@ abstract interface class AuthGateway() {
|
|||
/// Every later change of [status].
|
||||
Stream<AuthStatus> get statusChanges;
|
||||
|
||||
/// Creates an anonymous account on the server and signs in with it.
|
||||
Future<void> signInAnonymously();
|
||||
|
||||
/// Signs out on this device.
|
||||
Future<void> signOut();
|
||||
|
||||
|
|
@ -23,38 +18,19 @@ abstract interface class AuthGateway() {
|
|||
/// and so on all their devices, and forgets the session here.
|
||||
Future<void> deleteAccount();
|
||||
|
||||
/// What this instance offers of GitHub: signing in, and adding it to
|
||||
/// people as a way in.
|
||||
Future<GitHubOffer> gitHubOffer();
|
||||
/// Whether this instance signs people in with GitHub: those who have it
|
||||
/// already, as GitHub is added to nobody any more (ADR 0020, D25).
|
||||
Future<bool> gitHubSignInOffered();
|
||||
|
||||
/// Authorizes with GitHub in the browser and links the GitHub account to the
|
||||
/// signed-in person: the lossless upgrade from an anonymous account.
|
||||
Future<GitHubLinkOutcome> linkGitHub();
|
||||
|
||||
/// Authorizes with GitHub again and switches this device to the person who
|
||||
/// has that GitHub account; false when the person cancelled on GitHub.
|
||||
/// Authorizes with GitHub in the browser and signs this device in as the
|
||||
/// person who has that GitHub account; false when the person cancelled
|
||||
/// on GitHub.
|
||||
Future<bool> switchToGitHub();
|
||||
|
||||
/// Whether the app itself waits for GitHub's page in the browser, so that
|
||||
/// only the app can cancel it (Linux, Windows). Elsewhere the system's
|
||||
/// sign-in screen has its own Cancel.
|
||||
bool get canCancelGitHub;
|
||||
|
||||
/// Stops waiting for GitHub's page in the browser: the [linkGitHub] or
|
||||
/// [switchToGitHub] in progress ends as cancelled. Does nothing once
|
||||
/// GitHub has answered.
|
||||
Future<void> cancelGitHub();
|
||||
|
||||
/// How the signed-in person signs in.
|
||||
Future<SignInWays> signInWays();
|
||||
|
||||
/// Puts [grant] in place of this device's session: its person is signed
|
||||
/// in. An anonymous account left behind is deleted, as nobody could sign
|
||||
/// into it again (as switching to GitHub does).
|
||||
/// in.
|
||||
Future<void> useSession(SessionGrant grant);
|
||||
|
||||
/// The signed-in person was made identified on the instance (a password
|
||||
/// and address set, ADR 0017): this device's copy of their session
|
||||
/// follows.
|
||||
Future<void> markIdentified();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@ import 'dart:developer';
|
|||
import 'package:halkyon_app/src/auth/data/auth_gateway.dart';
|
||||
import 'package:halkyon_app/src/auth/data/sign_in_addresses.dart';
|
||||
import 'package:halkyon_app/src/auth/model/auth_status.dart';
|
||||
import 'package:halkyon_app/src/auth/model/github_link_outcome.dart';
|
||||
import 'package:halkyon_app/src/auth/model/github_sign_in.dart';
|
||||
import 'package:halkyon_app/src/auth/model/sign_in_ways.dart';
|
||||
|
||||
/// Who is signed in, and signing in and out, as the features use it.
|
||||
|
|
@ -18,40 +16,32 @@ class AuthRepository(
|
|||
/// Every later change of [status].
|
||||
Stream<AuthStatus> get statusChanges => _gateway.statusChanges;
|
||||
|
||||
/// Whether the signed-in person has an identity, as GitHub or an email
|
||||
/// address.
|
||||
bool get isIdentified => switch (_gateway.status) {
|
||||
SignedIn(:final isIdentified) => isIdentified,
|
||||
/// Whether a session left from before anonymous accounts went is signed
|
||||
/// in (ADR 0020, D25): one without an identity.
|
||||
bool get isAnonymous => switch (_gateway.status) {
|
||||
SignedIn(:final isIdentified) => !isIdentified,
|
||||
SignedOut() => false,
|
||||
};
|
||||
|
||||
/// Signs in anonymously unless someone is signed in already, so the app
|
||||
/// works from the first launch without any sign-up.
|
||||
Future<void> ensureSignedIn() async {
|
||||
if (_gateway.status is SignedOut) {
|
||||
await _gateway.signInAnonymously();
|
||||
}
|
||||
}
|
||||
/// Signs out on this device.
|
||||
Future<void> signOut() => _gateway.signOut();
|
||||
|
||||
/// Signs out on this device. An anonymous account can never be signed into
|
||||
/// again, so signing out of one deletes it with its progress (decided
|
||||
/// 2026-09-30); the UI warns first. Should deleting it fail (on a member
|
||||
/// while the instance's register is out of reach, which deletes nobody
|
||||
/// then: the short reading's L2), the device signs out all the same, as a
|
||||
/// switch leaves one behind: the account is left to the instance's
|
||||
/// pruning of anonymous accounts unused.
|
||||
Future<void> signOut() async {
|
||||
if (_gateway.status case SignedIn(isIdentified: false)) {
|
||||
try {
|
||||
await _gateway.deleteAccount();
|
||||
return;
|
||||
} on Exception catch (error, st) {
|
||||
log(
|
||||
'Deleting the anonymous account failed: ${error.runtimeType}',
|
||||
name: 'Auth',
|
||||
stackTrace: st,
|
||||
);
|
||||
}
|
||||
/// Ends an anonymous account's session left from before they went (ADR
|
||||
/// 0020, D25), as the app starts: deletes the account with its progress,
|
||||
/// as nobody can sign into it again, and signs out. Should deleting it
|
||||
/// fail (the instance out of reach, or on a member the instance's
|
||||
/// register), the device signs out all the same, the account left to the
|
||||
/// instance's pruning, which deletes every anonymous person left.
|
||||
Future<void> endAnonymousSession() async {
|
||||
try {
|
||||
await _gateway.deleteAccount();
|
||||
return;
|
||||
} on Exception catch (error, st) {
|
||||
log(
|
||||
'Deleting the anonymous account failed: ${error.runtimeType}',
|
||||
name: 'Auth',
|
||||
stackTrace: st,
|
||||
);
|
||||
}
|
||||
await _gateway.signOut();
|
||||
}
|
||||
|
|
@ -60,29 +50,12 @@ class AuthRepository(
|
|||
/// and all their devices; the UI asks first.
|
||||
Future<void> deleteAccount() => _gateway.deleteAccount();
|
||||
|
||||
/// Whether this instance offers signing in with GitHub.
|
||||
Future<bool> canSignInWithGitHub() async =>
|
||||
(await _gateway.gitHubOffer()).signIn;
|
||||
/// Whether this instance signs in with GitHub those who have it.
|
||||
Future<bool> canSignInWithGitHub() => _gateway.gitHubSignInOffered();
|
||||
|
||||
/// What this instance offers of GitHub: signing in, and adding it to
|
||||
/// people as a way in (not where they sign in with a password).
|
||||
Future<GitHubOffer> gitHubOffer() => _gateway.gitHubOffer();
|
||||
|
||||
/// Links a GitHub account to the signed-in person; the person, and all
|
||||
/// their data, stay the same.
|
||||
Future<GitHubLinkOutcome> linkGitHub() => _gateway.linkGitHub();
|
||||
|
||||
/// Switches this device to the person who has the GitHub account. The
|
||||
/// anonymous account this device leaves behind can't be signed in again.
|
||||
/// Signs this device in as the person who has the GitHub account.
|
||||
Future<bool> switchToGitHub() => _gateway.switchToGitHub();
|
||||
|
||||
/// Whether the app offers to cancel a GitHub sign-in in progress, where no
|
||||
/// sign-in screen of the system does.
|
||||
bool get canCancelGitHub => _gateway.canCancelGitHub;
|
||||
|
||||
/// Cancels the GitHub sign-in in progress while GitHub's page is open.
|
||||
Future<void> cancelGitHub() => _gateway.cancelGitHub();
|
||||
|
||||
/// How the signed-in person signs in.
|
||||
Future<SignInWays> signInWays() => _gateway.signInWays();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_web_auth_2_platform_interface/flutter_web_auth_2_platform_interface.dart';
|
||||
import 'package:halkyon_app/src/auth/data/auth_gateway.dart';
|
||||
import 'package:halkyon_app/src/auth/model/auth_status.dart';
|
||||
import 'package:halkyon_app/src/auth/model/github_link_outcome.dart';
|
||||
import 'package:halkyon_app/src/auth/model/github_sign_in.dart';
|
||||
import 'package:halkyon_app/src/auth/model/session_grant.dart';
|
||||
import 'package:halkyon_app/src/auth/model/sign_in_ways.dart';
|
||||
|
|
@ -40,8 +38,7 @@ class ServerpodAuthGateway(
|
|||
required final String appName,
|
||||
|
||||
/// What the clients it makes for itself send through (to sign in as
|
||||
/// another person, and to delete an account left behind); tests give a
|
||||
/// fake one.
|
||||
/// another person); tests give a fake one.
|
||||
final http.Client Function()? _httpClient,
|
||||
}) implements AuthGateway {
|
||||
this {
|
||||
|
|
@ -50,7 +47,8 @@ class ServerpodAuthGateway(
|
|||
|
||||
final _changes = StreamController<AuthStatus>.broadcast();
|
||||
|
||||
/// The scope a person gets once they have an identity.
|
||||
/// The scope every account has (ADR 0020, D4): a session without it is
|
||||
/// an anonymous account's, left from before they went.
|
||||
static const _identifiedScope = 'identified';
|
||||
|
||||
/// Where GitHub sends the browser back after authorizing: the app's own
|
||||
|
|
@ -75,9 +73,6 @@ class ServerpodAuthGateway(
|
|||
? LoopbackWebAuth(appName: appName)
|
||||
: null;
|
||||
|
||||
/// Whether the person cancelled the GitHub sign-in in progress.
|
||||
var _gitHubCancelled = false;
|
||||
|
||||
@override
|
||||
AuthStatus get status => switch (_client.auth.authInfo) {
|
||||
null => const SignedOut(),
|
||||
|
|
@ -90,12 +85,6 @@ class ServerpodAuthGateway(
|
|||
@override
|
||||
Stream<AuthStatus> get statusChanges => _changes.stream;
|
||||
|
||||
@override
|
||||
Future<void> signInAnonymously() async {
|
||||
final authSuccess = await _base.anonymousIdp.login();
|
||||
await _client.auth.updateSignedInUser(authSuccess);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> signOut() async {
|
||||
await _client.auth.signOutDevice();
|
||||
|
|
@ -110,54 +99,14 @@ class ServerpodAuthGateway(
|
|||
}
|
||||
|
||||
@override
|
||||
Future<GitHubOffer> gitHubOffer() async {
|
||||
final basics = await _base.instance.basics();
|
||||
final signIn = basics.gitHubClientId != null;
|
||||
return GitHubOffer(
|
||||
signIn: signIn,
|
||||
linking: signIn && !basics.passwordSignIn && !basics.simpleLogin,
|
||||
);
|
||||
}
|
||||
Future<bool> gitHubSignInOffered() async =>
|
||||
(await _base.instance.basics()).gitHubClientId != null;
|
||||
|
||||
/// The client ID of this instance's GitHub OAuth app; null when GitHub
|
||||
/// sign-in is off on this instance.
|
||||
Future<String?> _gitHubClientId() async =>
|
||||
(await _base.instance.basics()).gitHubClientId;
|
||||
|
||||
@override
|
||||
Future<GitHubLinkOutcome> linkGitHub() async {
|
||||
final authorization = await _authorizeWithGitHub();
|
||||
if (authorization == null) {
|
||||
return GitHubLinkOutcome.cancelled;
|
||||
}
|
||||
try {
|
||||
await _base.gitHubUpgrade.upgrade(
|
||||
code: authorization.code,
|
||||
codeVerifier: authorization.codeVerifier,
|
||||
redirectUri: authorization.redirectUri,
|
||||
);
|
||||
} on GitHubSignInRefusedException catch (error) {
|
||||
if (error.reason != GitHubSignInRefusal.takenByAnotherPerson) {
|
||||
throw _gitHubFailure(error);
|
||||
}
|
||||
return GitHubLinkOutcome.takenByAnotherPerson;
|
||||
}
|
||||
await _becomeIdentified();
|
||||
return GitHubLinkOutcome.linked;
|
||||
}
|
||||
|
||||
/// The server gave the person's sessions the identified scope; the copy of
|
||||
/// this device's session follows.
|
||||
Future<void> _becomeIdentified() async {
|
||||
if (_client.auth.authInfo case final authInfo?) {
|
||||
await _client.auth.updateSignedInUser(
|
||||
authInfo.copyWith(
|
||||
scopeNames: {...authInfo.scopeNames, _identifiedScope},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> switchToGitHub() async {
|
||||
final authorization = await _authorizeWithGitHub();
|
||||
|
|
@ -179,36 +128,13 @@ class ServerpodAuthGateway(
|
|||
} finally {
|
||||
signedOut.close();
|
||||
}
|
||||
await _switchTo(person);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Switches this device to [person]. Swapping directly means the app never
|
||||
/// passes through "signed out", which would start yet another anonymous
|
||||
/// account. An anonymous account left behind is deleted, as nobody could
|
||||
/// sign into it again; an identified one stays, to be signed into again.
|
||||
Future<void> _switchTo(AuthSuccess person) async {
|
||||
final leftBehind = _client.auth.authInfo;
|
||||
await _client.auth.updateSignedInUser(person);
|
||||
if (leftBehind != null &&
|
||||
!leftBehind.scopeNames.contains(_identifiedScope)) {
|
||||
await _deleteLeftBehind(leftBehind);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get canCancelGitHub => _loopback != null;
|
||||
|
||||
@override
|
||||
Future<void> cancelGitHub() async {
|
||||
_gitHubCancelled = true;
|
||||
await _loopback?.cancel();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// GitHub's authorization in the browser, or null when the person
|
||||
/// cancelled it.
|
||||
Future<GitHubSignInResult?> _authorizeWithGitHub() async {
|
||||
_gitHubCancelled = false;
|
||||
final clientId = await _gitHubClientId();
|
||||
if (clientId == null) {
|
||||
throw StateError('This instance offers no GitHub sign-in.');
|
||||
|
|
@ -226,21 +152,10 @@ class ServerpodAuthGateway(
|
|||
// Here, after the package has put its own in place at startup.
|
||||
FlutterWebAuth2Platform.instance = loopback;
|
||||
}
|
||||
if (_gitHubCancelled) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return await GitHubSignInService.instance.signIn();
|
||||
} on OAuth2PkceUserCancelledException {
|
||||
return null;
|
||||
} on OAuth2PkceException {
|
||||
// Serverpod (checked with v4.0.3) reports a cancelled browser round
|
||||
// trip as an unknown error, so a cancel is known by having asked for
|
||||
// it.
|
||||
if (_gitHubCancelled) {
|
||||
return null;
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -252,38 +167,12 @@ class ServerpodAuthGateway(
|
|||
|
||||
@override
|
||||
Future<void> useSession(SessionGrant grant) =>
|
||||
_switchTo(grant.session as AuthSuccess);
|
||||
|
||||
@override
|
||||
Future<void> markIdentified() => _becomeIdentified();
|
||||
_client.auth.updateSignedInUser(grant.session as AuthSuccess);
|
||||
|
||||
/// The instance's refusal of a GitHub sign-in as [GitHubSignInFailed].
|
||||
static GitHubSignInFailed _gitHubFailure(GitHubSignInRefusedException e) =>
|
||||
GitHubSignInFailed(gitHubSignInProblemOf(e.reason));
|
||||
|
||||
/// Deletes the anonymous account this device leaves behind when switching:
|
||||
/// nobody could ever sign into it again. It signs in as that account from
|
||||
/// a client of its own; a failure only leaves the account to the pruning.
|
||||
Future<void> _deleteLeftBehind(AuthSuccess anonymous) async {
|
||||
final sessionManager = FlutterAuthSessionManager(
|
||||
storage: _MemoryAuthStorage(),
|
||||
);
|
||||
final client = _clientOfItsOwn()..authSessionManager = sessionManager;
|
||||
try {
|
||||
await sessionManager.updateSignedInUser(anonymous);
|
||||
await client.modules.account.delete();
|
||||
} on Exception catch (error, st) {
|
||||
log(
|
||||
'Deleting the anonymous account left behind failed',
|
||||
name: 'Auth',
|
||||
error: error,
|
||||
stackTrace: st,
|
||||
);
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// A client of the instance's halkyon_base, holding no session.
|
||||
BaseClient _clientOfItsOwn() =>
|
||||
BaseClient(_client.host, httpClientOverride: _httpClient?.call());
|
||||
|
|
@ -297,19 +186,6 @@ class ServerpodAuthGateway(
|
|||
void _onAuthInfoChanged() => _changes.add(status);
|
||||
}
|
||||
|
||||
/// Keeps one session in memory, for a client used once.
|
||||
final class _MemoryAuthStorage() implements ClientAuthSuccessStorage {
|
||||
AuthSuccess? _session;
|
||||
|
||||
@override
|
||||
Future<AuthSuccess?> get() async => _session;
|
||||
|
||||
@override
|
||||
Future<void> set(AuthSuccess? data) async {
|
||||
_session = data;
|
||||
}
|
||||
}
|
||||
|
||||
/// The instance's refusal [reason] of a GitHub sign-in as the app's problem,
|
||||
/// which the screens put into words.
|
||||
GitHubSignInProblem gitHubSignInProblemOf(GitHubSignInRefusal reason) =>
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
/// Whether the person on this device signed out of the instance, and
|
||||
/// nobody started since: the app then asks how to start rather than making
|
||||
/// an anonymous account by itself, at once and at every start after.
|
||||
abstract interface class SignedOutMark() {
|
||||
/// Whether the device is marked signed out.
|
||||
Future<bool> read();
|
||||
|
||||
/// Marks the device signed out, or not.
|
||||
Future<void> write({required bool signedOut});
|
||||
}
|
||||
|
||||
/// The mark as a file in the folder of the instance's people, which
|
||||
/// outlives every person's own.
|
||||
class FileSignedOutMark(final Directory _people) implements SignedOutMark {
|
||||
File get _file => File(p.join(_people.path, 'signed_out'));
|
||||
|
||||
@override
|
||||
Future<bool> read() async => _file.existsSync();
|
||||
|
||||
@override
|
||||
Future<void> write({required bool signedOut}) async {
|
||||
if (signedOut) {
|
||||
await _people.create(recursive: true);
|
||||
await _file.writeAsString('');
|
||||
} else if (_file.existsSync()) {
|
||||
await _file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -14,8 +14,9 @@ final class const SignedIn({
|
|||
/// The person, i.e. the server's `AuthUser` id.
|
||||
required final String authUserId,
|
||||
|
||||
/// Whether the person has an identity, such as GitHub. Anonymous people have
|
||||
/// none: their progress lives on this device, and signing out loses it.
|
||||
/// Whether the person has an identity, a way in. Every account has one
|
||||
/// (ADR 0020, D4): a session without it is an anonymous account's, left
|
||||
/// from before they went, which the app ends at its start (D25).
|
||||
required final bool isIdentified,
|
||||
}) extends AuthStatus {
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
/// How linking a GitHub account to the signed-in person ended.
|
||||
enum GitHubLinkOutcome() {
|
||||
/// Linked: the person now signs in with GitHub and counts as identified.
|
||||
linked,
|
||||
|
||||
/// The GitHub account belongs to another person; nothing changed.
|
||||
takenByAnotherPerson,
|
||||
|
||||
/// The person closed GitHub's page without authorizing; nothing changed.
|
||||
cancelled,
|
||||
}
|
||||
|
|
@ -1,9 +1,7 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Why signing in with GitHub didn't go through, beyond a cancel.
|
||||
enum GitHubSignInProblem() {
|
||||
/// No person has this GitHub account yet; only adding it to one makes it
|
||||
/// a way in.
|
||||
/// No person has this GitHub account: GitHub makes no account (ADR 0020,
|
||||
/// D25).
|
||||
unknownAccount,
|
||||
|
||||
/// The account signs in with its address and password now, not with
|
||||
|
|
@ -28,17 +26,3 @@ final class const GitHubSignInFailed(final GitHubSignInProblem problem)
|
|||
@override
|
||||
String toString() => 'GitHubSignInFailed(${problem.name})';
|
||||
}
|
||||
|
||||
/// What an instance offers of GitHub, from one look at its basics.
|
||||
final class const GitHubOffer({
|
||||
/// Whether people sign in with GitHub here.
|
||||
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) or with SimpleLogin (ADR 0020, D23),
|
||||
/// where only those who have it already sign in with it.
|
||||
required final bool linking,
|
||||
}) with Equatable {
|
||||
@override
|
||||
List<Object?> get props => [signIn, linking];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// How a person signs in, besides the sessions of their devices: with
|
||||
/// GitHub, with an email address, both, or neither (anonymous).
|
||||
/// GitHub, with an email address, both, or neither (with SimpleLogin alone,
|
||||
/// which `SimpleLoginStandingCubit` tells).
|
||||
final class const SignInWays({
|
||||
required final bool gitHub,
|
||||
required final bool email,
|
||||
}) with Equatable {
|
||||
/// An anonymous person's: neither.
|
||||
/// Neither, as long as nothing was read.
|
||||
static const none = SignInWays(gitHub: false, email: false);
|
||||
|
||||
@override
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue