diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js
deleted file mode 100644
index 9ff830f66a..0000000000
--- a/src/components/structures/InteractiveAuth.js
+++ /dev/null
@@ -1,300 +0,0 @@
-/*
-Copyright 2017 Vector Creations Ltd.
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { InteractiveAuth } from "matrix-js-sdk/src/interactive-auth";
-import React, { createRef } from 'react';
-import PropTypes from 'prop-types';
-
-import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryComponents';
-
-import * as sdk from '../../index';
-import { replaceableComponent } from "../../utils/replaceableComponent";
-
-export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
-
-@replaceableComponent("structures.InteractiveAuthComponent")
-export default class InteractiveAuthComponent extends React.Component {
- static propTypes = {
- // matrix client to use for UI auth requests
- matrixClient: PropTypes.object.isRequired,
-
- // response from initial request. If not supplied, will do a request on
- // mount.
- authData: PropTypes.shape({
- flows: PropTypes.array,
- params: PropTypes.object,
- session: PropTypes.string,
- }),
-
- // callback
- makeRequest: PropTypes.func.isRequired,
-
- // callback called when the auth process has finished,
- // successfully or unsuccessfully.
- // @param {bool} status True if the operation requiring
- // auth was completed sucessfully, false if canceled.
- // @param {object} result The result of the authenticated call
- // if successful, otherwise the error object.
- // @param {object} extra Additional information about the UI Auth
- // process:
- // * emailSid {string} If email auth was performed, the sid of
- // the auth session.
- // * clientSecret {string} The client secret used in auth
- // sessions with the ID server.
- onAuthFinished: PropTypes.func.isRequired,
-
- // Inputs provided by the user to the auth process
- // and used by various stages. As passed to js-sdk
- // interactive-auth
- inputs: PropTypes.object,
-
- // As js-sdk interactive-auth
- requestEmailToken: PropTypes.func,
- sessionId: PropTypes.string,
- clientSecret: PropTypes.string,
- emailSid: PropTypes.string,
-
- // If true, poll to see if the auth flow has been completed
- // out-of-band
- poll: PropTypes.bool,
-
- // If true, components will be told that the 'Continue' button
- // is managed by some other party and should not be managed by
- // the component itself.
- continueIsManaged: PropTypes.bool,
-
- // 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: PropTypes.func,
-
- // continueText and continueKind are passed straight through to the AuthEntryComponent.
- continueText: PropTypes.string,
- continueKind: PropTypes.string,
- };
-
- constructor(props) {
- super(props);
-
- this.state = {
- authStage: null,
- busy: false,
- errorText: null,
- stageErrorText: null,
- submitButtonEnabled: false,
- };
-
- this._unmounted = false;
- this._authLogic = new InteractiveAuth({
- authData: this.props.authData,
- doRequest: this._requestCallback,
- busyChanged: this._onBusyChanged,
- inputs: this.props.inputs,
- stateUpdated: this._authStateUpdated,
- matrixClient: this.props.matrixClient,
- sessionId: this.props.sessionId,
- clientSecret: this.props.clientSecret,
- emailSid: this.props.emailSid,
- requestEmailToken: this._requestEmailToken,
- });
-
- this._intervalId = null;
- if (this.props.poll) {
- this._intervalId = setInterval(() => {
- this._authLogic.poll();
- }, 2000);
- }
-
- this._stageComponent = createRef();
- }
-
- // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
- UNSAFE_componentWillMount() { // eslint-disable-line camelcase
- this._authLogic.attemptAuth().then((result) => {
- const extra = {
- emailSid: this._authLogic.getEmailSid(),
- clientSecret: this._authLogic.getClientSecret(),
- };
- this.props.onAuthFinished(true, result, extra);
- }).catch((error) => {
- this.props.onAuthFinished(false, error);
- console.error("Error during user-interactive auth:", error);
- if (this._unmounted) {
- return;
- }
-
- const msg = error.message || error.toString();
- this.setState({
- errorText: msg,
- });
- });
- }
-
- componentWillUnmount() {
- this._unmounted = true;
-
- if (this._intervalId !== null) {
- clearInterval(this._intervalId);
- }
- }
-
- _requestEmailToken = async (...args) => {
- this.setState({
- busy: true,
- });
- try {
- return await this.props.requestEmailToken(...args);
- } finally {
- this.setState({
- busy: false,
- });
- }
- };
-
- tryContinue = () => {
- if (this._stageComponent.current && this._stageComponent.current.tryContinue) {
- this._stageComponent.current.tryContinue();
- }
- };
-
- _authStateUpdated = (stageType, stageState) => {
- const oldStage = this.state.authStage;
- this.setState({
- busy: false,
- authStage: stageType,
- stageState: stageState,
- errorText: stageState.error,
- }, () => {
- if (oldStage !== stageType) {
- this._setFocus();
- } else if (
- !stageState.error && this._stageComponent.current &&
- this._stageComponent.current.attemptFailed
- ) {
- this._stageComponent.current.attemptFailed();
- }
- });
- };
-
- _requestCallback = (auth) => {
- // This wrapper just exists because the js-sdk passes a second
- // 'busy' param for backwards compat. This throws the tests off
- // so discard it here.
- return this.props.makeRequest(auth);
- };
-
- _onBusyChanged = (busy) => {
- // if we've started doing stuff, reset the error messages
- if (busy) {
- this.setState({
- busy: true,
- errorText: null,
- stageErrorText: null,
- });
- }
- // The JS SDK eagerly reports itself as "not busy" right after any
- // immediate work has completed, but that's not really what we want at
- // the UI layer, so we ignore this signal and show a spinner until
- // there's a new screen to show the user. This is implemented by setting
- // `busy: false` in `_authStateUpdated`.
- // See also https://github.com/vector-im/element-web/issues/12546
- };
-
- _setFocus() {
- if (this._stageComponent.current && this._stageComponent.current.focus) {
- this._stageComponent.current.focus();
- }
- }
-
- _submitAuthDict = authData => {
- this._authLogic.submitAuthDict(authData);
- };
-
- _onPhaseChange = newPhase => {
- if (this.props.onStagePhaseChange) {
- this.props.onStagePhaseChange(this.state.authStage, newPhase || 0);
- }
- };
-
- _onStageCancel = () => {
- this.props.onAuthFinished(false, ERROR_USER_CANCELLED);
- };
-
- _renderCurrentStage() {
- const stage = this.state.authStage;
- if (!stage) {
- if (this.state.busy) {
- const Loader = sdk.getComponent("elements.Spinner");
- return ;
- } else {
- return null;
- }
- }
-
- const StageComponent = getEntryComponentForLoginType(stage);
- return (
-
- );
- }
-
- _onAuthStageFailed = e => {
- this.props.onAuthFinished(false, e);
- };
-
- _setEmailSid = sid => {
- this._authLogic.setEmailSid(sid);
- };
-
- render() {
- let error = null;
- if (this.state.errorText) {
- error = (
-
- { this.state.errorText }
-
- );
- }
-
- return (
-
-
- { this._renderCurrentStage() }
- { error }
-
-
- );
- }
-}
diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx
new file mode 100644
index 0000000000..017e34184f
--- /dev/null
+++ b/src/components/structures/InteractiveAuth.tsx
@@ -0,0 +1,300 @@
+/*
+Copyright 2017 - 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import {
+ AuthType,
+ IAuthData,
+ IAuthDict,
+ IInputs,
+ InteractiveAuth,
+ IStageStatus,
+} from "matrix-js-sdk/src/interactive-auth";
+import { MatrixClient } from "matrix-js-sdk/src/client";
+import React, { createRef } from 'react';
+
+import getEntryComponentForLoginType, { IStageComponent } from '../views/auth/InteractiveAuthEntryComponents';
+import Spinner from "../views/elements/Spinner";
+import { replaceableComponent } from "../../utils/replaceableComponent";
+
+export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
+
+interface IProps {
+ // matrix client to use for UI auth requests
+ matrixClient: MatrixClient;
+ // response from initial request. If not supplied, will do a request on mount.
+ authData: IAuthData;
+ // Inputs provided by the user to the auth process
+ // and used by various stages. As passed to js-sdk
+ // interactive-auth
+ inputs?: IInputs;
+ sessionId?: string;
+ clientSecret?: string;
+ emailSid?: string;
+ // If true, poll to see if the auth flow has been completed out-of-band
+ poll?: boolean;
+ // If true, components will be told that the 'Continue' button
+ // is managed by some other party and should not be managed by
+ // the component itself.
+ continueIsManaged?: boolean;
+ // continueText and continueKind are passed straight through to the AuthEntryComponent.
+ continueText?: string;
+ continueKind?: string;
+ // callback
+ makeRequest(auth: IAuthData): Promise;
+ // callback called when the auth process has finished,
+ // successfully or unsuccessfully.
+ // @param {boolean} status True if the operation requiring
+ // auth was completed successfully, false if canceled.
+ // @param {object} result The result of the authenticated call
+ // if successful, otherwise the error object.
+ // @param {object} extra Additional information about the UI Auth
+ // process:
+ // * emailSid {string} If email auth was performed, the sid of
+ // the auth session.
+ // * clientSecret {string} The client secret used in auth
+ // sessions with the ID server.
+ onAuthFinished(
+ status: boolean,
+ result: IAuthData | Error,
+ extra?: { emailSid?: string, clientSecret?: string },
+ ): void;
+ // As js-sdk interactive-auth
+ requestEmailToken?(email: string, secret: string, attempt: number, session: string): Promise<{ sid: string }>,
+ // 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: string, phase: string | number): void,
+}
+
+interface IState {
+ authStage?: AuthType;
+ stageState?: IStageStatus;
+ busy: boolean;
+ errorText?: string;
+ stageErrorText?: string;
+ submitButtonEnabled: boolean;
+}
+
+@replaceableComponent("structures.InteractiveAuthComponent")
+export default class InteractiveAuthComponent extends React.Component {
+ private readonly authLogic: InteractiveAuth;
+ private readonly _intervalId: NodeJS.Timeout = null;
+ private readonly stageComponent = createRef();
+
+ private unmounted = false;
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ authStage: null,
+ busy: false,
+ errorText: null,
+ stageErrorText: null,
+ submitButtonEnabled: false,
+ };
+
+ this.authLogic = new InteractiveAuth({
+ authData: this.props.authData,
+ doRequest: this.requestCallback,
+ busyChanged: this.onBusyChanged,
+ inputs: this.props.inputs,
+ stateUpdated: this.authStateUpdated,
+ matrixClient: this.props.matrixClient,
+ sessionId: this.props.sessionId,
+ clientSecret: this.props.clientSecret,
+ emailSid: this.props.emailSid,
+ requestEmailToken: this.requestEmailToken,
+ });
+
+ if (this.props.poll) {
+ this._intervalId = setInterval(() => {
+ this.authLogic.poll();
+ }, 2000);
+ }
+ }
+
+ // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs
+ UNSAFE_componentWillMount() { // eslint-disable-line camelcase
+ this.authLogic.attemptAuth().then((result) => {
+ const extra = {
+ emailSid: this.authLogic.getEmailSid(),
+ clientSecret: this.authLogic.getClientSecret(),
+ };
+ this.props.onAuthFinished(true, result, extra);
+ }).catch((error) => {
+ this.props.onAuthFinished(false, error);
+ console.error("Error during user-interactive auth:", error);
+ if (this.unmounted) {
+ return;
+ }
+
+ const msg = error.message || error.toString();
+ this.setState({
+ errorText: msg,
+ });
+ });
+ }
+
+ componentWillUnmount() {
+ this.unmounted = true;
+
+ if (this._intervalId !== null) {
+ clearInterval(this._intervalId);
+ }
+ }
+
+ private requestEmailToken = async (
+ email: string,
+ secret: string,
+ attempt: number,
+ session: string,
+ ): Promise<{sid: string}> => {
+ this.setState({
+ busy: true,
+ });
+ try {
+ return await this.props.requestEmailToken(email, secret, attempt, session);
+ } finally {
+ this.setState({
+ busy: false,
+ });
+ }
+ };
+
+ private tryContinue = (): void => {
+ this.stageComponent.current?.tryContinue?.();
+ };
+
+ private authStateUpdated = (stageType: AuthType, stageState: IStageStatus): void => {
+ const oldStage = this.state.authStage;
+ this.setState({
+ busy: false,
+ authStage: stageType,
+ stageState: stageState,
+ errorText: stageState.error,
+ }, () => {
+ if (oldStage !== stageType) {
+ this.setFocus();
+ } else if (!stageState.error) {
+ this.stageComponent.current?.attemptFailed?.();
+ }
+ });
+ };
+
+ private requestCallback = (auth: IAuthData, background: boolean): Promise => {
+ // This wrapper just exists because the js-sdk passes a second
+ // 'busy' param for backwards compat. This throws the tests off
+ // so discard it here.
+ return this.props.makeRequest(auth);
+ };
+
+ private onBusyChanged = (busy: boolean): void => {
+ // if we've started doing stuff, reset the error messages
+ if (busy) {
+ this.setState({
+ busy: true,
+ errorText: null,
+ stageErrorText: null,
+ });
+ }
+ // The JS SDK eagerly reports itself as "not busy" right after any
+ // immediate work has completed, but that's not really what we want at
+ // the UI layer, so we ignore this signal and show a spinner until
+ // there's a new screen to show the user. This is implemented by setting
+ // `busy: false` in `authStateUpdated`.
+ // See also https://github.com/vector-im/element-web/issues/12546
+ };
+
+ private setFocus(): void {
+ this.stageComponent.current?.focus?.();
+ }
+
+ private submitAuthDict = (authData: IAuthDict): void => {
+ this.authLogic.submitAuthDict(authData);
+ };
+
+ private onPhaseChange = (newPhase: number): void => {
+ this.props.onStagePhaseChange?.(this.state.authStage, newPhase || 0);
+ };
+
+ private onStageCancel = (): void => {
+ this.props.onAuthFinished(false, ERROR_USER_CANCELLED);
+ };
+
+ private renderCurrentStage() {
+ const stage = this.state.authStage;
+ if (!stage) {
+ if (this.state.busy) {
+ return ;
+ } else {
+ return null;
+ }
+ }
+
+ const StageComponent = getEntryComponentForLoginType(stage);
+ return (
+
+ );
+ }
+
+ private onAuthStageFailed = (e: Error): void => {
+ this.props.onAuthFinished(false, e);
+ };
+
+ private setEmailSid = (sid: string): void => {
+ this.authLogic.setEmailSid(sid);
+ };
+
+ render() {
+ let error = null;
+ if (this.state.errorText) {
+ error = (
+
+ { this.state.errorText }
+
+ );
+ }
+
+ return (
+
+
+ { this.renderCurrentStage() }
+ { error }
+
+
+ );
+ }
+}
diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx
index e002eb5717..a032414eb0 100644
--- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx
+++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx
@@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { ChangeEvent, createRef, FormEvent, MouseEvent } from 'react';
+import React, { ChangeEvent, ComponentClass, createRef, FormEvent, MouseEvent, RefObject } from 'react';
import classNames from 'classnames';
import { MatrixClient } from "matrix-js-sdk/src/client";
+import { AuthType, IAuthDict, IInputs, IStageStatus } from 'matrix-js-sdk/src/interactive-auth';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
@@ -73,33 +74,6 @@ import { LocalisedPolicy, Policies } from '../../../Terms';
* focus: set the input focus appropriately in the form.
*/
-enum AuthType {
- Password = "m.login.password",
- Recaptcha = "m.login.recaptcha",
- Terms = "m.login.terms",
- Email = "m.login.email.identity",
- Msisdn = "m.login.msisdn",
- Sso = "m.login.sso",
- SsoUnstable = "org.matrix.login.sso",
-}
-
-/* eslint-disable camelcase */
-interface IAuthDict {
- type?: AuthType;
- // TODO: Remove `user` once servers support proper UIA
- // See https://github.com/vector-im/element-web/issues/10312
- user?: string;
- identifier?: any;
- password?: string;
- response?: string;
- // TODO: Remove `threepid_creds` once servers support proper UIA
- // See https://github.com/vector-im/element-web/issues/10312
- // See https://github.com/matrix-org/matrix-doc/issues/2220
- threepid_creds?: any;
- threepidCreds?: any;
-}
-/* eslint-enable camelcase */
-
export const DEFAULT_PHASE = 0;
interface IAuthEntryProps {
@@ -837,7 +811,26 @@ export class FallbackAuthEntry extends React.Component {
}
}
-export default function getEntryComponentForLoginType(loginType: AuthType): typeof React.Component {
+export interface IStageComponentProps extends IAuthEntryProps {
+ clientSecret?: string;
+ stageParams?: Record;
+ inputs?: IInputs;
+ stageState?: IStageStatus;
+ showContinue?: boolean;
+ continueText?: string;
+ continueKind?: string;
+ fail?(e: Error): void;
+ setEmailSid?(sid: string): void;
+ onCancel?(): void;
+}
+
+export interface IStageComponent extends React.ComponentClass> {
+ tryContinue?(): void;
+ attemptFailed?(): void;
+ focus?(): void;
+}
+
+export default function getEntryComponentForLoginType(loginType: AuthType): IStageComponent {
switch (loginType) {
case AuthType.Password:
return PasswordAuthEntry;
diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx
index 6df6056670..d30f90d111 100644
--- a/src/components/views/dialogs/DeactivateAccountDialog.tsx
+++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx
@@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
+import { AuthType, IAuthData } from 'matrix-js-sdk/src/interactive-auth';
import * as sdk from '../../../index';
import Analytics from '../../../Analytics';
@@ -65,7 +66,7 @@ export default class DeactivateAccountDialog extends React.Component {
+ private onStagePhaseChange = (stage: AuthType, phase: string): void => {
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."),
@@ -115,7 +116,10 @@ export default class DeactivateAccountDialog extends React.Component {
+ private onUIAuthComplete = (auth: IAuthData): void => {
+ // XXX: this should be returning a promise to maintain the state inside the state machine correct
+ // but given that a deactivation is followed by a local logout and all object instances being thrown away
+ // this isn't done.
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
// Deactivation worked - logout & close this dialog
Analytics.trackEvent('Account', 'Deactivate Account');
@@ -182,7 +186,7 @@ export default class DeactivateAccountDialog extends React.Component