From 32aa18ff2e0e135c6edf9e1aeba00726b13d06d3 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 Mar 2023 10:45:55 +0000 Subject: [PATCH] Apply `strictNullChecks` to `src/components/views/auth/*` (#10299 * Apply `strictNullChecks` to src/components/views/auth/* * Iterate PR --- src/@types/common.ts | 10 ---- src/SdkConfig.ts | 5 +- src/components/structures/InteractiveAuth.tsx | 7 +-- src/components/structures/MatrixChat.tsx | 3 +- .../structures/auth/Registration.tsx | 49 ++++++++++++------- .../auth/InteractiveAuthEntryComponents.tsx | 40 +++++++-------- .../views/auth/LanguageSelector.tsx | 2 +- .../views/auth/PassphraseConfirmField.tsx | 7 +-- src/components/views/auth/PassphraseField.tsx | 16 +++--- .../views/auth/RegistrationForm.tsx | 39 +++++++-------- src/components/views/auth/Welcome.tsx | 2 +- .../views/dialogs/DeactivateAccountDialog.tsx | 30 +++++------- .../views/dialogs/InteractiveAuthDialog.tsx | 24 +++++---- .../dialogs/RegistrationEmailPromptDialog.tsx | 3 +- src/components/views/elements/Validation.tsx | 8 +-- src/utils/ErrorUtils.tsx | 4 +- 16 files changed, 127 insertions(+), 122 deletions(-) diff --git a/src/@types/common.ts b/src/@types/common.ts index ddcd0bb3dc6..be77708a82e 100644 --- a/src/@types/common.ts +++ b/src/@types/common.ts @@ -48,16 +48,6 @@ export type RecursivePartial = { : T[P]; }; -// Inspired by https://stackoverflow.com/a/60206860 -export type KeysWithObjectShape = { - [P in keyof Input]: Input[P] extends object - ? // Arrays are counted as objects - exclude them - Input[P] extends Array - ? never - : P - : never; -}[keyof Input]; - export type KeysStartingWith = { // eslint-disable-next-line @typescript-eslint/no-unused-vars [P in keyof Input]: P extends `${Str}${infer _X}` ? P : never; // we don't use _X diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index fab5cb29c6a..70032cdabb8 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -19,7 +19,6 @@ import { Optional } from "matrix-events-sdk"; import { SnakedObject } from "./utils/SnakedObject"; import { IConfigOptions, ISsoRedirectOptions } from "./IConfigOptions"; -import { KeysWithObjectShape } from "./@types/common"; // see element-web config.md for docs, or the IConfigOptions interface for dev docs export const DEFAULTS: IConfigOptions = { @@ -78,10 +77,10 @@ export default class SdkConfig { return SdkConfig.fallback.get(key, altCaseName); } - public static getObject>( + public static getObject( key: K, altCaseName?: string, - ): Optional> { + ): Optional>> { const val = SdkConfig.get(key, altCaseName); if (val !== null && val !== undefined) { return new SnakedObject(val); diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index 77113052f11..d737c6dd5e2 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -80,7 +80,7 @@ interface IProps { // Called when the stage changes, or the stage's phase changes. First // argument is the stage, second is the phase. Some stages do not have // phases and will be counted as 0 (numeric). - onStagePhaseChange?(stage: AuthType, phase: number): void; + onStagePhaseChange?(stage: AuthType | null, phase: number): void; } interface IState { @@ -170,7 +170,8 @@ export default class InteractiveAuthComponent extends React.Component { - this.props.onStagePhaseChange?.(this.state.authStage, newPhase || 0); + this.props.onStagePhaseChange?.(this.state.authStage ?? null, newPhase || 0); }; private onStageCancel = (): void => { diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 18c61240c9e..00a05b169a3 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -2015,7 +2015,7 @@ export default class MatrixChat extends React.PureComponent { public render(): React.ReactNode { const fragmentAfterLogin = this.getFragmentAfterLogin(); - let view = null; + let view: JSX.Element; if (this.state.view === Views.LOADING) { view = ( @@ -2132,6 +2132,7 @@ export default class MatrixChat extends React.PureComponent { view = => this.onShowPostLoginScreen(useCase)} />; } else { logger.error(`Unknown view ${this.state.view}`); + return null; } return ( diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 196b3904177..dd9be190b2a 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -78,9 +78,9 @@ interface IProps { } interface IState { + // true if we're waiting for the user to complete busy: boolean; errorText?: ReactNode; - // true if we're waiting for the user to complete // We remember the values entered by the user because // the registration form will be unmounted during the // course of registration, but if there's an error we @@ -88,7 +88,7 @@ interface IState { // values the user entered still in it. We can keep // them in this component's state since this component // persist for the duration of the registration process. - formVals: Record; + formVals: Record; // user-interactive auth // If we've been given a session ID, we're resuming // straight back into UI auth @@ -96,9 +96,11 @@ interface IState { // If set, we've registered but are not going to log // the user in to their new account automatically. completedNoSignin: boolean; - flows: { - stages: string[]; - }[]; + flows: + | { + stages: string[]; + }[] + | null; // We perform liveliness checks later, but for now suppress the errors. // We also track the server dead errors independently of the regular errors so // that we can render it differently, and override any other error the user may @@ -158,7 +160,7 @@ export default class Registration extends React.Component { window.removeEventListener("beforeunload", this.unloadCallback); } - private unloadCallback = (event: BeforeUnloadEvent): string => { + private unloadCallback = (event: BeforeUnloadEvent): string | undefined => { if (this.state.doingUIAuth) { event.preventDefault(); event.returnValue = ""; @@ -215,7 +217,7 @@ export default class Registration extends React.Component { this.loginLogic.setHomeserverUrl(hsUrl); this.loginLogic.setIdentityServerUrl(isUrl); - let ssoFlow: ISSOFlow; + let ssoFlow: ISSOFlow | undefined; try { const loginFlows = await this.loginLogic.getFlows(); if (serverConfig !== this.latestServerConfig) return; // discard, serverConfig changed from under us @@ -289,6 +291,7 @@ export default class Registration extends React.Component { sendAttempt: number, sessionId: string, ): Promise => { + if (!this.state.matrixClient) throw new Error("Matrix client has not yet been loaded"); return this.state.matrixClient.requestRegisterEmailToken( emailAddress, clientSecret, @@ -303,6 +306,8 @@ export default class Registration extends React.Component { }; private onUIAuthFinished: InteractiveAuthCallback = async (success, response): Promise => { + if (!this.state.matrixClient) throw new Error("Matrix client has not yet been loaded"); + debuglog("Registration: ui authentication finished: ", { success, response }); if (!success) { let errorText: ReactNode = (response as Error).message || (response as Error).toString(); @@ -327,10 +332,8 @@ export default class Registration extends React.Component { ); } else if ((response as IAuthData).required_stages?.includes(AuthType.Msisdn)) { - let msisdnAvailable = false; - for (const flow of (response as IAuthData).available_flows) { - msisdnAvailable = msisdnAvailable || flow.stages.includes(AuthType.Msisdn); - } + const flows = (response as IAuthData).available_flows ?? []; + const msisdnAvailable = flows.some((flow) => flow.stages.includes(AuthType.Msisdn)); if (!msisdnAvailable) { errorText = _t("This server does not support authentication with a phone number."); } @@ -348,12 +351,16 @@ export default class Registration extends React.Component { return; } - MatrixClientPeg.setJustRegisteredUserId((response as IAuthData).user_id); + const userId = (response as IAuthData).user_id; + const accessToken = (response as IAuthData).access_token; + if (!userId || !accessToken) throw new Error("Registration failed"); + + MatrixClientPeg.setJustRegisteredUserId(userId); const newState: Partial = { doingUIAuth: false, registeredUsername: (response as IAuthData).user_id, - differentLoggedInUserId: null, + differentLoggedInUserId: undefined, completedNoSignin: false, // we're still busy until we get unmounted: don't show the registration form again busy: true, @@ -393,13 +400,13 @@ export default class Registration extends React.Component { // the email, not the client that started the registration flow await this.props.onLoggedIn( { - userId: (response as IAuthData).user_id, + userId, deviceId: (response as IAuthData).device_id, homeserverUrl: this.state.matrixClient.getHomeserverUrl(), identityServerUrl: this.state.matrixClient.getIdentityServerUrl(), - accessToken: (response as IAuthData).access_token, + accessToken, }, - this.state.formVals.password, + this.state.formVals.password!, ); this.setupPushers(); @@ -457,6 +464,8 @@ export default class Registration extends React.Component { }; private makeRegisterRequest = (auth: IAuthData | null): Promise => { + if (!this.state.matrixClient) throw new Error("Matrix client has not yet been loaded"); + const registerParams: IRegisterRequestParams = { username: this.state.formVals.username, password: this.state.formVals.password, @@ -494,7 +503,7 @@ export default class Registration extends React.Component { return sessionLoaded; }; - private renderRegisterComponent(): JSX.Element { + private renderRegisterComponent(): ReactNode { if (this.state.matrixClient && this.state.doingUIAuth) { return ( { ); - } else if (this.state.flows.length) { - let ssoSection; + } else if (this.state.matrixClient && this.state.flows.length) { + let ssoSection: JSX.Element | undefined; if (this.state.ssoFlow) { let continueWithSection; const providers = this.state.ssoFlow.identity_providers || []; @@ -571,6 +580,8 @@ export default class Registration extends React.Component { ); } + + return null; } public render(): React.ReactNode { diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 79f777ce5d3..8910b91945c 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -83,7 +83,7 @@ export const DEFAULT_PHASE = 0; interface IAuthEntryProps { matrixClient: MatrixClient; loginType: string; - authSessionId: string; + authSessionId?: string; errorText?: string; errorCode?: string; // Is the auth logic currently waiting for something to happen? @@ -120,7 +120,7 @@ export class PasswordAuthEntry extends React.Component = {}; const pickedPolicies: { @@ -300,12 +300,12 @@ export class TermsAuthEntry extends React.Component e !== "version"); - langPolicy = policy[firstLang]; + langPolicy = firstLang ? policy[firstLang] : undefined; } if (!langPolicy) throw new Error("Failed to find a policy to show the user"); @@ -358,7 +358,7 @@ export class TermsAuthEntry extends React.Component; } - const checkboxes = []; + const checkboxes: JSX.Element[] = []; let allChecked = true; for (const policy of this.state.policies) { const checked = this.state.toggledPolicies[policy.id]; @@ -384,7 +384,7 @@ export class TermsAuthEntry extends React.Component { public static LOGIN_TYPE = AuthType.Msisdn; - private submitUrl: string; + private submitUrl?: string; private sid: string; private msisdn: string; @@ -798,11 +798,13 @@ export class SSOAuthEntry extends React.Component {_t("Cancel")} @@ -909,7 +911,7 @@ export class SSOAuthEntry extends React.Component { - private popupWindow: Window; + private popupWindow: Window | null; private fallbackButton = createRef(); public constructor(props: IAuthEntryProps) { @@ -927,18 +929,16 @@ export class FallbackAuthEntry extends React.Component { public componentWillUnmount(): void { window.removeEventListener("message", this.onReceiveMessage); - if (this.popupWindow) { - this.popupWindow.close(); - } + this.popupWindow?.close(); } public focus = (): void => { - if (this.fallbackButton.current) { - this.fallbackButton.current.focus(); - } + this.fallbackButton.current?.focus(); }; private onShowFallbackClick = (e: MouseEvent): void => { + if (!this.props.authSessionId) return; + e.preventDefault(); e.stopPropagation(); diff --git a/src/components/views/auth/LanguageSelector.tsx b/src/components/views/auth/LanguageSelector.tsx index 3eee9940662..b7816ea2967 100644 --- a/src/components/views/auth/LanguageSelector.tsx +++ b/src/components/views/auth/LanguageSelector.tsx @@ -26,7 +26,7 @@ import LanguageDropdown from "../elements/LanguageDropdown"; function onChange(newLang: string): void { if (getCurrentLanguage() !== newLang) { SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang); - PlatformPeg.get().reload(); + PlatformPeg.get()?.reload(); } } diff --git a/src/components/views/auth/PassphraseConfirmField.tsx b/src/components/views/auth/PassphraseConfirmField.tsx index a1fb67c5280..2411547912e 100644 --- a/src/components/views/auth/PassphraseConfirmField.tsx +++ b/src/components/views/auth/PassphraseConfirmField.tsx @@ -20,15 +20,16 @@ import Field, { IInputProps } from "../elements/Field"; import withValidation, { IFieldState, IValidationResult } from "../elements/Validation"; import { _t, _td } from "../../../languageHandler"; -interface IProps extends Omit { +interface IProps extends Omit { id?: string; fieldRef?: RefCallback | RefObject; autoComplete?: string; value: string; password: string; // The password we're confirming - labelRequired?: string; - labelInvalid?: string; + label: string; + labelRequired: string; + labelInvalid: string; onChange(ev: React.FormEvent): void; onValidate?(result: IValidationResult): void; diff --git a/src/components/views/auth/PassphraseField.tsx b/src/components/views/auth/PassphraseField.tsx index b221322e523..5175de6aa7a 100644 --- a/src/components/views/auth/PassphraseField.tsx +++ b/src/components/views/auth/PassphraseField.tsx @@ -31,10 +31,10 @@ interface IProps extends Omit { value: string; fieldRef?: RefCallback | RefObject; - label?: string; - labelEnterPassword?: string; - labelStrongPassword?: string; - labelAllowedButUnsafe?: string; + label: string; + labelEnterPassword: string; + labelStrongPassword: string; + labelAllowedButUnsafe: string; onChange(ev: React.FormEvent): void; onValidate?(result: IValidationResult): void; @@ -48,12 +48,12 @@ class PassphraseField extends PureComponent { labelAllowedButUnsafe: _td("Password is allowed, but unsafe"), }; - public readonly validate = withValidation({ + public readonly validate = withValidation({ description: function (complexity) { const score = complexity ? complexity.score : 0; return ; }, - deriveData: async ({ value }): Promise => { + deriveData: async ({ value }): Promise => { if (!value) return null; const { scorePassword } = await import("../../../utils/PasswordScorer"); return scorePassword(value); @@ -67,7 +67,7 @@ class PassphraseField extends PureComponent { { key: "complexity", test: async function ({ value }, complexity): Promise { - if (!value) { + if (!value || !complexity) { return false; } const safe = complexity.score >= this.props.minScore; @@ -78,7 +78,7 @@ class PassphraseField extends PureComponent { // Unsafe passwords that are valid are only possible through a // configuration flag. We'll print some helper text to signal // to the user that their password is allowed, but unsafe. - if (complexity.score >= this.props.minScore) { + if (complexity && complexity.score >= this.props.minScore) { return _t(this.props.labelStrongPassword); } return _t(this.props.labelAllowedButUnsafe); diff --git a/src/components/views/auth/RegistrationForm.tsx b/src/components/views/auth/RegistrationForm.tsx index 25a319e8179..17d540b4ed6 100644 --- a/src/components/views/auth/RegistrationForm.tsx +++ b/src/components/views/auth/RegistrationForm.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { BaseSyntheticEvent } from "react"; +import React, { BaseSyntheticEvent, ReactNode } from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { logger } from "matrix-js-sdk/src/logger"; import { MatrixError } from "matrix-js-sdk/src/matrix"; @@ -82,7 +82,7 @@ interface IState { // Field error codes by field ID fieldValid: Partial>; // The ISO2 country code selected in the phone number entry - phoneCountry: string; + phoneCountry?: string; username: string; email: string; phoneNumber: string; @@ -95,11 +95,11 @@ interface IState { * A pure UI component which displays a registration form. */ export default class RegistrationForm extends React.PureComponent { - private [RegistrationField.Email]: Field; - private [RegistrationField.Password]: Field; - private [RegistrationField.PasswordConfirm]: Field; - private [RegistrationField.Username]: Field; - private [RegistrationField.PhoneNumber]: Field; + private [RegistrationField.Email]: Field | null; + private [RegistrationField.Password]: Field | null; + private [RegistrationField.PasswordConfirm]: Field | null; + private [RegistrationField.Username]: Field | null; + private [RegistrationField.PhoneNumber]: Field | null; public static defaultProps = { onValidationChange: logger.error, @@ -117,7 +117,6 @@ export default class RegistrationForm extends React.PureComponent => { - if (confirmed) { + if (confirmed && email !== undefined) { this.setState( { email, @@ -265,7 +264,7 @@ export default class RegistrationForm extends React.PureComponent { - this.markFieldValid(RegistrationField.Email, result.valid); + this.markFieldValid(RegistrationField.Email, !!result.valid); }; private validateEmailRules = withValidation({ @@ -294,7 +293,7 @@ export default class RegistrationForm extends React.PureComponent { - this.markFieldValid(RegistrationField.Password, result.valid); + this.markFieldValid(RegistrationField.Password, !!result.valid); }; private onPasswordConfirmChange = (ev: React.ChangeEvent): void => { @@ -304,7 +303,7 @@ export default class RegistrationForm extends React.PureComponent { - this.markFieldValid(RegistrationField.PasswordConfirm, result.valid); + this.markFieldValid(RegistrationField.PasswordConfirm, !!result.valid); }; private onPhoneCountryChange = (newVal: PhoneNumberCountryDefinition): void => { @@ -321,7 +320,7 @@ export default class RegistrationForm extends React.PureComponent => { const result = await this.validatePhoneNumberRules(fieldState); - this.markFieldValid(RegistrationField.PhoneNumber, result.valid); + this.markFieldValid(RegistrationField.PhoneNumber, !!result.valid); return result; }; @@ -352,14 +351,14 @@ export default class RegistrationForm extends React.PureComponent => { const result = await this.validateUsernameRules(fieldState); - this.markFieldValid(RegistrationField.Username, result.valid); + this.markFieldValid(RegistrationField.Username, !!result.valid); return result; }; private validateUsernameRules = withValidation({ description: (_, results) => { // omit the description if the only failing result is the `available` one as it makes no sense for it. - if (results.every(({ key, valid }) => key === "available" || valid)) return; + if (results.every(({ key, valid }) => key === "available" || valid)) return null; return _t("Use lowercase letters, numbers, dashes and underscores only"); }, hideDescriptionIfValid: true, @@ -448,7 +447,7 @@ export default class RegistrationForm extends React.PureComponent ); - let emailHelperText = null; + let emailHelperText: JSX.Element | undefined; if (this.showEmail()) { if (this.showPhoneNumber()) { emailHelperText = ( diff --git a/src/components/views/auth/Welcome.tsx b/src/components/views/auth/Welcome.tsx index 003a6cc5094..29294f70a60 100644 --- a/src/components/views/auth/Welcome.tsx +++ b/src/components/views/auth/Welcome.tsx @@ -34,7 +34,7 @@ interface IProps {} export default class Welcome extends React.PureComponent { public render(): React.ReactNode { const pagesConfig = SdkConfig.getObject("embedded_pages"); - let pageUrl!: string; + let pageUrl: string | undefined; if (pagesConfig) { pageUrl = pagesConfig.get("welcome_url"); } diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx index 5c1269033d1..8e8f0be7703 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.tsx +++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx @@ -44,15 +44,15 @@ interface IProps { interface IState { shouldErase: boolean; - errStr: string; + errStr: string | null; authData: any; // for UIA authEnabled: boolean; // see usages for information // A few strings that are passed to InteractiveAuth for design or are displayed // next to the InteractiveAuth component. - bodyText: string; - continueText: string; - continueKind: string; + bodyText?: string; + continueText?: string; + continueKind?: string; } export default class DeactivateAccountDialog extends React.Component { @@ -64,12 +64,6 @@ export default class DeactivateAccountDialog extends React.Component{this.state.errStr}; } diff --git a/src/components/views/dialogs/InteractiveAuthDialog.tsx b/src/components/views/dialogs/InteractiveAuthDialog.tsx index dbc6c5fda76..14b800243dd 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.tsx +++ b/src/components/views/dialogs/InteractiveAuthDialog.tsx @@ -76,7 +76,7 @@ export interface InteractiveAuthDialogProps { } interface IState { - authError: Error; + authError: Error | null; // See _onUpdateStagePhase() uiaStage: AuthType | null; @@ -147,16 +147,22 @@ export default class InteractiveAuthDialog extends React.Component = ({ onFinished }) => { diff --git a/src/components/views/elements/Validation.tsx b/src/components/views/elements/Validation.tsx index 0af6417145c..db51c46bae6 100644 --- a/src/components/views/elements/Validation.tsx +++ b/src/components/views/elements/Validation.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactNode } from "react"; import classNames from "classnames"; type Data = Pick; @@ -31,13 +31,13 @@ interface IRule { final?: boolean; skip?(this: T, data: Data, derivedData: D): boolean; test(this: T, data: Data, derivedData: D): boolean | Promise; - valid?(this: T, derivedData: D): string; - invalid?(this: T, derivedData: D): string; + valid?(this: T, derivedData: D): string | null; + invalid?(this: T, derivedData: D): string | null; } interface IArgs { rules: IRule[]; - description?(this: T, derivedData: D, results: IResult[]): React.ReactChild; + description?(this: T, derivedData: D, results: IResult[]): ReactNode; hideDescriptionIfValid?: boolean; deriveData?(data: Data): Promise; } diff --git a/src/utils/ErrorUtils.tsx b/src/utils/ErrorUtils.tsx index 1cc049c03a4..e45e3091a1a 100644 --- a/src/utils/ErrorUtils.tsx +++ b/src/utils/ErrorUtils.tsx @@ -34,12 +34,12 @@ import { _t, _td, Tags, TranslatedString } from "../languageHandler"; * @returns {*} Translated string or react component */ export function messageForResourceLimitError( - limitType: string, + limitType: string | undefined, adminContact: string | undefined, strings: Record, extraTranslations?: Tags, ): TranslatedString { - let errString = strings[limitType]; + let errString = limitType ? strings[limitType] : undefined; if (errString === undefined) errString = strings[""]; const linkSub = (sub: string): ReactNode => {