Add a new state between LOADING/SOFT_LOGOUT and LOGGED_IN

... so that we can transition into COMPLETE_SECURITY without going via
LOGGED_IN.
This commit is contained in:
Richard van der Hoff 2025-11-26 10:36:39 +00:00
parent 66dd507ea2
commit a525c53fd9
3 changed files with 104 additions and 65 deletions

View File

@ -27,6 +27,7 @@ const notLoggedInMap: Record<Exclude<Views, Views.LOGGED_IN>, ScreenName> = {
[Views.FORGOT_PASSWORD]: "ForgotPassword",
[Views.COMPLETE_SECURITY]: "CompleteSecurity",
[Views.E2E_SETUP]: "E2ESetup",
[Views.PENDING_CLIENT_START]: "Loading",
[Views.SOFT_LOGOUT]: "SoftLogout",
[Views.LOCK_STOLEN]: "SessionLockStolen",
};

View File

@ -20,52 +20,62 @@ Please see LICENSE files in the repository root for full details.
* LOADING CONFIRM_LOCK_
* THEFT
* Lock theft confirmed
* Session recovered
*
* No previous session
* Token/OIDC login succeeded
*
*
* WELCOME (from all other states
* except LOCK_STOLEN)
*
* "Create Account" "Sign in" Client logged out
*
*
*
* "Create an ▼ ▼ "Forgot
* account" ┌─────────────────┐ password"
* REGISTER LOGIN FORGOT_PASSWORD
*
* "Sign in here" Complete /
* "Sign in instead"
*
*
*
*
* (postLoginSetup)
*
*
* E2EE not enabled
* Account has Account lacks
* cross-signing cross-signing
* keys keys
* Client started and
* force_verification
* pending COMPLETE_ E2E_SETUP
* SECURITY
* "Forgotten
* your
* password?"
*
* (from all other states
* except LOCK_STOLEN)
*
* Soft logout error
*
* LOGGED_IN Re-authentication succeeded SOFT_LOGOUT
*
*
* Session recovered Token/OIDC login succeeded
*
*
* No previous session
*
* (from all other states
* except LOCK_STOLEN) WELCOME
*
* Client logged out
*
* "Sign in" "Create account"
*
*
* "Forgot ▼ ▼ "Create an
* password" ┌─────────────────┐ account"
* FORGOT_PASSWORD LOGIN REGISTER
*
* Complete / "Sign in here"
* "Sign in instead"
*
* "Forgotten your
* password?"
*
* Soft-logout error
* SOFT_LOGOUT (from all other states
* except LOCK_STOLEN)
*
* Re-authentication succeeded
*
* (postLoginSetup)
*
* PENDING_CLIENT_ Account has Account lacks
* START cross-signing cross-signing
* keys keys
*
*
* Client started,
* force_verification pending
*
* Client started,
* force_verification COMPLETE_
* not needed SECURITY
* E2E_SETUP
*
* E2EE not enabled
*
*
*
*
*
*
*
*
* LOGGED_IN
*
*
*
* (from all other states)
*
@ -102,6 +112,12 @@ enum Views {
// flow to setup SSSS / cross-signing on this account
E2E_SETUP,
/**
* We have successfully recovered a session from localstorage, but the client
* has not yet been started.
*/
PENDING_CLIENT_START,
// we are logged in with an active matrix client. The logged_in state also
// includes guests users as they too are logged in at the client level.
LOGGED_IN,

View File

@ -1535,18 +1535,27 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.stores.client = MatrixClientPeg.safeGet();
StorageManager.tryPersistStorage();
// If we're in the middle of a login/registration, we wait for it to complete before transitioning to the logged
// in view the login flow will call `postLoginSetup` when it's done, which will arrange for `onShowPostLoginScreen`
// to be called.
// If we're loading the app for the first time, we can now transition to a splash screen while we wait for the
// client to start. The exceptions are:
//
// - If there is a token login in flight: in that case we wait for the login to complete (which hits
// `postLoginSetup`).
//
// - Lifecycle emits an `Action.OnLoggedIn` event during startup even if the localstorage flag indicating a
// previous soft logout is set. In that situation we actually want to wait for the `Action.ClientNotViable`
// event, which will transition us into Views.SOFT_LOGOUT. We therefore have to check for !isSoftLogout().
// There will be a subsequent `Action.OnLoggedIn` event once the reauthentication completes.
//
// XXX: fix this properly by having Lifecycle not emit OnLoggedIn when it knows it is about to emit a
// ClientNotViable.
//
// If we're already in the SOFT_LOGOUT view, that means that reauthentication has succeeded, and we can
// transition to the splash screen.
if (
!this.tokenLogin &&
!Lifecycle.isSoftLogout() &&
this.state.view !== Views.LOGIN &&
this.state.view !== Views.REGISTER &&
this.state.view !== Views.COMPLETE_SECURITY &&
this.state.view !== Views.E2E_SETUP
(this.state.view === Views.LOADING && !Lifecycle.isSoftLogout() && !this.tokenLogin) ||
this.state.view === Views.SOFT_LOGOUT
) {
this.onShowPostLoginScreen();
this.setStateForNewView({ view: Views.PENDING_CLIENT_START });
}
}
@ -1779,15 +1788,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const cli = MatrixClientPeg.safeGet();
const shouldForceVerification = await this.shouldForceVerification();
// XXX: Don't replace the screen if it's already one of these: postLoginSetup
// changes to these screens in certain circumstances so we shouldn't clobber it.
// We should probably have one place where we decide what the next screen is after
// login.
if (![Views.COMPLETE_SECURITY, Views.E2E_SETUP].includes(this.state.view)) {
if (shouldForceVerification) {
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
}
}
const crypto = cli.getCrypto();
if (crypto) {
@ -1804,6 +1804,19 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.setState({
ready: true,
});
// If the view is PENDING_CLIENT_START, that means we recovered the session from localstorage, or from
// soft-logout: we can now transition to the logged-in view.
//
// If the view is something else, that probably means it's a login or registration view; we handle that in
// `postLoginSetup`.
if (this.state.view === Views.PENDING_CLIENT_START) {
if (shouldForceVerification) {
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
} else {
await this.onShowPostLoginScreen();
}
}
}
public showScreen(screen: string, params?: { [key: string]: any }): void {
@ -2162,6 +2175,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
view = <CompleteSecurity onFinished={this.onCompleteSecurityE2eSetupFinished} />;
} else if (this.state.view === Views.E2E_SETUP) {
view = <E2eSetup onCancelled={this.onCompleteSecurityE2eSetupFinished} />;
} else if (this.state.view === Views.PENDING_CLIENT_START) {
// we think we are logged in, but are still waiting for the /sync to complete
view = (
<LoginSplashView
matrixClient={MatrixClientPeg.safeGet()}
onLogoutClick={this.onLogoutClick}
syncError={this.state.syncError}
/>
);
} else if (this.state.view === Views.LOGGED_IN) {
// `ready` and `view==LOGGED_IN` may be set before `page_type` (because the
// latter is set via the dispatcher). If we don't yet have a `page_type`,