Add types to InteractiveAuthEntryComponents

pull/21833/head
J. Ryan Stinnett 2021-05-12 19:28:22 +01:00
parent 6574ca98fa
commit df09bdf823
3 changed files with 213 additions and 180 deletions

View File

@ -36,14 +36,18 @@ export class Service {
} }
} }
interface Policy { export interface LocalisedPolicy {
name: string;
url: string;
}
export interface Policy {
// @ts-ignore: No great way to express indexed types together with other keys // @ts-ignore: No great way to express indexed types together with other keys
version: string; version: string;
[lang: string]: { [lang: string]: LocalisedPolicy;
url: string;
};
} }
type Policies = {
export type Policies = {
[policy: string]: Policy, [policy: string]: Policy,
}; };

View File

@ -1,7 +1,5 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016-2021 The Matrix.org Foundation C.I.C.
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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {createRef} from 'react'; import React, { ChangeEvent, createRef, FormEvent, MouseEvent } from 'react';
import PropTypes from 'prop-types'; import classNames from 'classnames';
import classnames from 'classnames'; import { MatrixClient } from "matrix-js-sdk/src/client";
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
@ -27,6 +25,7 @@ import AccessibleButton from "../elements/AccessibleButton";
import Spinner from "../elements/Spinner"; import Spinner from "../elements/Spinner";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import { LocalisedPolicy, Policies } from '../../../Terms';
/* This file contains a collection of components which are used by the /* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed * InteractiveAuth to prompt the user to enter the information needed
@ -74,21 +73,49 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
* focus: set the input focus appropriately in the form. * 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?: object;
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?: object;
threepidCreds?: object;
}
/* eslint-enable camelcase */
export const DEFAULT_PHASE = 0; export const DEFAULT_PHASE = 0;
@replaceableComponent("views.auth.PasswordAuthEntry") interface IAuthEntryProps {
export class PasswordAuthEntry extends React.Component { matrixClient: MatrixClient;
static LOGIN_TYPE = "m.login.password"; loginType: string;
authSessionId: string;
submitAuthDict: (auth: IAuthDict) => void;
errorText?: string;
// Is the auth logic currently waiting for something to happen?
busy?: boolean;
onPhaseChange: (phase: number) => void;
}
static propTypes = { @replaceableComponent("views.auth.PasswordAuthEntry")
matrixClient: PropTypes.object.isRequired, export class PasswordAuthEntry extends React.Component<IAuthEntryProps> {
submitAuthDict: PropTypes.func.isRequired, static LOGIN_TYPE = AuthType.Password;
errorText: PropTypes.string,
// is the auth logic currently waiting for something to
// happen?
busy: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
};
componentDidMount() { componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE); this.props.onPhaseChange(DEFAULT_PHASE);
@ -98,12 +125,12 @@ export class PasswordAuthEntry extends React.Component {
password: "", password: "",
}; };
_onSubmit = e => { private onSubmit = (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
if (this.props.busy) return; if (this.props.busy) return;
this.props.submitAuthDict({ this.props.submitAuthDict({
type: PasswordAuthEntry.LOGIN_TYPE, type: AuthType.Password,
// TODO: Remove `user` once servers support proper UIA // TODO: Remove `user` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312 // See https://github.com/vector-im/element-web/issues/10312
user: this.props.matrixClient.credentials.userId, user: this.props.matrixClient.credentials.userId,
@ -115,7 +142,7 @@ export class PasswordAuthEntry extends React.Component {
}); });
}; };
_onPasswordFieldChange = ev => { private onPasswordFieldChange = (ev: ChangeEvent<HTMLInputElement>) => {
// enable the submit button iff the password is non-empty // enable the submit button iff the password is non-empty
this.setState({ this.setState({
password: ev.target.value, password: ev.target.value,
@ -123,7 +150,7 @@ export class PasswordAuthEntry extends React.Component {
}; };
render() { render() {
const passwordBoxClass = classnames({ const passwordBoxClass = classNames({
"error": this.props.errorText, "error": this.props.errorText,
}); });
@ -155,7 +182,7 @@ export class PasswordAuthEntry extends React.Component {
return ( return (
<div> <div>
<p>{ _t("Confirm your identity by entering your account password below.") }</p> <p>{ _t("Confirm your identity by entering your account password below.") }</p>
<form onSubmit={this._onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection"> <form onSubmit={this.onSubmit} className="mx_InteractiveAuthEntryComponents_passwordSection">
<Field <Field
className={passwordBoxClass} className={passwordBoxClass}
type="password" type="password"
@ -163,7 +190,7 @@ export class PasswordAuthEntry extends React.Component {
label={_t('Password')} label={_t('Password')}
autoFocus={true} autoFocus={true}
value={this.state.password} value={this.state.password}
onChange={this._onPasswordFieldChange} onChange={this.onPasswordFieldChange}
/> />
<div className="mx_button_row"> <div className="mx_button_row">
{ submitButtonOrSpinner } { submitButtonOrSpinner }
@ -175,26 +202,26 @@ export class PasswordAuthEntry extends React.Component {
} }
} }
@replaceableComponent("views.auth.RecaptchaAuthEntry") /* eslint-disable camelcase */
export class RecaptchaAuthEntry extends React.Component { interface IRecaptchaAuthEntryProps extends IAuthEntryProps {
static LOGIN_TYPE = "m.login.recaptcha"; stageParams?: {
public_key?: string;
static propTypes = {
submitAuthDict: PropTypes.func.isRequired,
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
}; };
}
/* eslint-enable camelcase */
@replaceableComponent("views.auth.RecaptchaAuthEntry")
export class RecaptchaAuthEntry extends React.Component<IRecaptchaAuthEntryProps> {
static LOGIN_TYPE = AuthType.Recaptcha;
componentDidMount() { componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE); this.props.onPhaseChange(DEFAULT_PHASE);
} }
_onCaptchaResponse = response => { private onCaptchaResponse = (response: string) => {
CountlyAnalytics.instance.track("onboarding_grecaptcha_submit"); CountlyAnalytics.instance.track("onboarding_grecaptcha_submit");
this.props.submitAuthDict({ this.props.submitAuthDict({
type: RecaptchaAuthEntry.LOGIN_TYPE, type: AuthType.Recaptcha,
response: response, response: response,
}); });
}; };
@ -230,7 +257,7 @@ export class RecaptchaAuthEntry extends React.Component {
return ( return (
<div> <div>
<CaptchaForm sitePublicKey={sitePublicKey} <CaptchaForm sitePublicKey={sitePublicKey}
onCaptchaResponse={this._onCaptchaResponse} onCaptchaResponse={this.onCaptchaResponse}
/> />
{ errorSection } { errorSection }
</div> </div>
@ -238,18 +265,28 @@ export class RecaptchaAuthEntry extends React.Component {
} }
} }
@replaceableComponent("views.auth.TermsAuthEntry") interface ITermsAuthEntryProps extends IAuthEntryProps {
export class TermsAuthEntry extends React.Component { stageParams?: {
static LOGIN_TYPE = "m.login.terms"; policies?: Policies;
static propTypes = {
submitAuthDict: PropTypes.func.isRequired,
stageParams: PropTypes.object.isRequired,
errorText: PropTypes.string,
busy: PropTypes.bool,
showContinue: PropTypes.bool,
onPhaseChange: PropTypes.func.isRequired,
}; };
showContinue: boolean;
}
interface LocalisedPolicyWithId extends LocalisedPolicy {
id: string;
}
interface ITermsAuthEntryState {
policies: LocalisedPolicyWithId[];
toggledPolicies: {
[policy: string]: boolean;
};
errorText?: string;
}
@replaceableComponent("views.auth.TermsAuthEntry")
export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITermsAuthEntryState> {
static LOGIN_TYPE = AuthType.Terms;
constructor(props) { constructor(props) {
super(props); super(props);
@ -294,8 +331,11 @@ export class TermsAuthEntry extends React.Component {
initToggles[policyId] = false; initToggles[policyId] = false;
langPolicy.id = policyId; pickedPolicies.push({
pickedPolicies.push(langPolicy); id: policyId,
name: langPolicy.name,
url: langPolicy.url,
});
} }
this.state = { this.state = {
@ -312,10 +352,10 @@ export class TermsAuthEntry extends React.Component {
} }
tryContinue = () => { tryContinue = () => {
this._trySubmit(); this.trySubmit();
}; };
_togglePolicy(policyId) { private togglePolicy(policyId: string) {
const newToggles = {}; const newToggles = {};
for (const policy of this.state.policies) { for (const policy of this.state.policies) {
let checked = this.state.toggledPolicies[policy.id]; let checked = this.state.toggledPolicies[policy.id];
@ -326,7 +366,7 @@ export class TermsAuthEntry extends React.Component {
this.setState({"toggledPolicies": newToggles}); this.setState({"toggledPolicies": newToggles});
} }
_trySubmit = () => { private trySubmit = () => {
let allChecked = true; let allChecked = true;
for (const policy of this.state.policies) { for (const policy of this.state.policies) {
const checked = this.state.toggledPolicies[policy.id]; const checked = this.state.toggledPolicies[policy.id];
@ -334,7 +374,7 @@ export class TermsAuthEntry extends React.Component {
} }
if (allChecked) { if (allChecked) {
this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE}); this.props.submitAuthDict({type: AuthType.Terms});
CountlyAnalytics.instance.track("onboarding_terms_complete"); CountlyAnalytics.instance.track("onboarding_terms_complete");
} else { } else {
this.setState({errorText: _t("Please review and accept all of the homeserver's policies")}); this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
@ -356,7 +396,7 @@ export class TermsAuthEntry extends React.Component {
checkboxes.push( checkboxes.push(
// XXX: replace with StyledCheckbox // XXX: replace with StyledCheckbox
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy"> <label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} /> <input type="checkbox" onChange={() => this.togglePolicy(policy.id)} checked={checked} />
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a> <a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
</label>, </label>,
); );
@ -375,7 +415,7 @@ export class TermsAuthEntry extends React.Component {
if (this.props.showContinue !== false) { if (this.props.showContinue !== false) {
// XXX: button classes // XXX: button classes
submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton" submitButton = <button className="mx_InteractiveAuthEntryComponents_termsSubmit mx_GeneralButton"
onClick={this._trySubmit} disabled={!allChecked}>{_t("Accept")}</button>; onClick={this.trySubmit} disabled={!allChecked}>{_t("Accept")}</button>;
} }
return ( return (
@ -389,21 +429,18 @@ export class TermsAuthEntry extends React.Component {
} }
} }
@replaceableComponent("views.auth.EmailIdentityAuthEntry") interface IEmailIdentityAuthEntryProps extends IAuthEntryProps {
export class EmailIdentityAuthEntry extends React.Component { inputs?: {
static LOGIN_TYPE = "m.login.email.identity"; emailAddress?: string;
static propTypes = {
matrixClient: PropTypes.object.isRequired,
submitAuthDict: PropTypes.func.isRequired,
authSessionId: PropTypes.string.isRequired,
clientSecret: PropTypes.string.isRequired,
inputs: PropTypes.object.isRequired,
stageState: PropTypes.object.isRequired,
fail: PropTypes.func.isRequired,
setEmailSid: PropTypes.func.isRequired,
onPhaseChange: PropTypes.func.isRequired,
}; };
stageState?: {
emailSid: string;
};
}
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
export class EmailIdentityAuthEntry extends React.Component<IEmailIdentityAuthEntryProps> {
static LOGIN_TYPE = AuthType.Email;
componentDidMount() { componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE); this.props.onPhaseChange(DEFAULT_PHASE);
@ -427,7 +464,7 @@ export class EmailIdentityAuthEntry extends React.Component {
return ( return (
<div className="mx_InteractiveAuthEntryComponents_emailWrapper"> <div className="mx_InteractiveAuthEntryComponents_emailWrapper">
<p>{ _t("A confirmation email has been sent to %(emailAddress)s", <p>{ _t("A confirmation email has been sent to %(emailAddress)s",
{ emailAddress: (sub) => <b>{ this.props.inputs.emailAddress }</b> }, { emailAddress: <b>{ this.props.inputs.emailAddress }</b> },
) } ) }
</p> </p>
<p>{ _t("Open the link in the email to continue registration.") }</p> <p>{ _t("Open the link in the email to continue registration.") }</p>
@ -437,37 +474,34 @@ export class EmailIdentityAuthEntry extends React.Component {
} }
} }
@replaceableComponent("views.auth.MsisdnAuthEntry") interface IMsisdnAuthEntryProps extends IAuthEntryProps {
export class MsisdnAuthEntry extends React.Component { inputs: {
static LOGIN_TYPE = "m.login.msisdn"; phoneCountry: string;
phoneNumber: string;
static propTypes = {
inputs: PropTypes.shape({
phoneCountry: PropTypes.string,
phoneNumber: PropTypes.string,
}),
fail: PropTypes.func,
clientSecret: PropTypes.func,
submitAuthDict: PropTypes.func.isRequired,
matrixClient: PropTypes.object,
onPhaseChange: PropTypes.func.isRequired,
}; };
clientSecret: string;
fail: (error: Error) => void;
}
@replaceableComponent("views.auth.MsisdnAuthEntry")
export class MsisdnAuthEntry extends React.Component<IMsisdnAuthEntryProps> {
static LOGIN_TYPE = AuthType.Msisdn;
private submitUrl: string;
private sid: string;
private msisdn: string;
state = { state = {
token: '', token: '',
requestingToken: false, requestingToken: false,
errorText: '',
}; };
componentDidMount() { componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE); this.props.onPhaseChange(DEFAULT_PHASE);
this._submitUrl = null;
this._sid = null;
this._msisdn = null;
this._tokenBox = null;
this.setState({requestingToken: true}); this.setState({requestingToken: true});
this._requestMsisdnToken().catch((e) => { this.requestMsisdnToken().catch((e) => {
this.props.fail(e); this.props.fail(e);
}).finally(() => { }).finally(() => {
this.setState({requestingToken: false}); this.setState({requestingToken: false});
@ -477,26 +511,26 @@ export class MsisdnAuthEntry extends React.Component {
/* /*
* Requests a verification token by SMS. * Requests a verification token by SMS.
*/ */
_requestMsisdnToken() { private requestMsisdnToken(): Promise<void> {
return this.props.matrixClient.requestRegisterMsisdnToken( return this.props.matrixClient.requestRegisterMsisdnToken(
this.props.inputs.phoneCountry, this.props.inputs.phoneCountry,
this.props.inputs.phoneNumber, this.props.inputs.phoneNumber,
this.props.clientSecret, this.props.clientSecret,
1, // TODO: Multiple send attempts? 1, // TODO: Multiple send attempts?
).then((result) => { ).then((result) => {
this._submitUrl = result.submit_url; this.submitUrl = result.submit_url;
this._sid = result.sid; this.sid = result.sid;
this._msisdn = result.msisdn; this.msisdn = result.msisdn;
}); });
} }
_onTokenChange = e => { private onTokenChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({ this.setState({
token: e.target.value, token: e.target.value,
}); });
}; };
_onFormSubmit = async e => { private onFormSubmit = async (e: FormEvent) => {
e.preventDefault(); e.preventDefault();
if (this.state.token == '') return; if (this.state.token == '') return;
@ -506,20 +540,20 @@ export class MsisdnAuthEntry extends React.Component {
try { try {
let result; let result;
if (this._submitUrl) { if (this.submitUrl) {
result = await this.props.matrixClient.submitMsisdnTokenOtherUrl( result = await this.props.matrixClient.submitMsisdnTokenOtherUrl(
this._submitUrl, this._sid, this.props.clientSecret, this.state.token, this.submitUrl, this.sid, this.props.clientSecret, this.state.token,
); );
} else { } else {
throw new Error("The registration with MSISDN flow is misconfigured"); throw new Error("The registration with MSISDN flow is misconfigured");
} }
if (result.success) { if (result.success) {
const creds = { const creds = {
sid: this._sid, sid: this.sid,
client_secret: this.props.clientSecret, client_secret: this.props.clientSecret,
}; };
this.props.submitAuthDict({ this.props.submitAuthDict({
type: MsisdnAuthEntry.LOGIN_TYPE, type: AuthType.Msisdn,
// TODO: Remove `threepid_creds` once servers support proper UIA // TODO: Remove `threepid_creds` once servers support proper UIA
// See https://github.com/vector-im/element-web/issues/10312 // See https://github.com/vector-im/element-web/issues/10312
// See https://github.com/matrix-org/matrix-doc/issues/2220 // See https://github.com/matrix-org/matrix-doc/issues/2220
@ -543,7 +577,7 @@ export class MsisdnAuthEntry extends React.Component {
return <Loader />; return <Loader />;
} else { } else {
const enableSubmit = Boolean(this.state.token); const enableSubmit = Boolean(this.state.token);
const submitClasses = classnames({ const submitClasses = classNames({
mx_InteractiveAuthEntryComponents_msisdnSubmit: true, mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
mx_GeneralButton: true, mx_GeneralButton: true,
}); });
@ -558,16 +592,16 @@ export class MsisdnAuthEntry extends React.Component {
return ( return (
<div> <div>
<p>{ _t("A text message has been sent to %(msisdn)s", <p>{ _t("A text message has been sent to %(msisdn)s",
{ msisdn: <i>{ this._msisdn }</i> }, { msisdn: <i>{ this.msisdn }</i> },
) } ) }
</p> </p>
<p>{ _t("Please enter the code it contains:") }</p> <p>{ _t("Please enter the code it contains:") }</p>
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper"> <div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
<form onSubmit={this._onFormSubmit}> <form onSubmit={this.onFormSubmit}>
<input type="text" <input type="text"
className="mx_InteractiveAuthEntryComponents_msisdnEntry" className="mx_InteractiveAuthEntryComponents_msisdnEntry"
value={this.state.token} value={this.state.token}
onChange={this._onTokenChange} onChange={this.onTokenChange}
aria-label={ _t("Code")} aria-label={ _t("Code")}
/> />
<br /> <br />
@ -584,40 +618,40 @@ export class MsisdnAuthEntry extends React.Component {
} }
} }
@replaceableComponent("views.auth.SSOAuthEntry") interface ISSOAuthEntryProps extends IAuthEntryProps {
export class SSOAuthEntry extends React.Component { continueText?: string;
static propTypes = { continueKind?: string;
matrixClient: PropTypes.object.isRequired, onCancel?: () => void;
authSessionId: PropTypes.string.isRequired, }
loginType: PropTypes.string.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
onPhaseChange: PropTypes.func.isRequired,
continueText: PropTypes.string,
continueKind: PropTypes.string,
onCancel: PropTypes.func,
};
static LOGIN_TYPE = "m.login.sso"; interface ISSOAuthEntryState {
static UNSTABLE_LOGIN_TYPE = "org.matrix.login.sso"; phase: number;
attemptFailed: boolean;
}
@replaceableComponent("views.auth.SSOAuthEntry")
export class SSOAuthEntry extends React.Component<ISSOAuthEntryProps, ISSOAuthEntryState> {
static LOGIN_TYPE = AuthType.Sso;
static UNSTABLE_LOGIN_TYPE = AuthType.SsoUnstable;
static PHASE_PREAUTH = 1; // button to start SSO static PHASE_PREAUTH = 1; // button to start SSO
static PHASE_POSTAUTH = 2; // button to confirm SSO completed static PHASE_POSTAUTH = 2; // button to confirm SSO completed
_ssoUrl: string; private ssoUrl: string;
private popupWindow: Window;
constructor(props) { constructor(props) {
super(props); super(props);
// We actually send the user through fallback auth so we don't have to // We actually send the user through fallback auth so we don't have to
// deal with a redirect back to us, losing application context. // deal with a redirect back to us, losing application context.
this._ssoUrl = props.matrixClient.getFallbackAuthUrl( this.ssoUrl = props.matrixClient.getFallbackAuthUrl(
this.props.loginType, this.props.loginType,
this.props.authSessionId, this.props.authSessionId,
); );
this._popupWindow = null; this.popupWindow = null;
window.addEventListener("message", this._onReceiveMessage); window.addEventListener("message", this.onReceiveMessage);
this.state = { this.state = {
phase: SSOAuthEntry.PHASE_PREAUTH, phase: SSOAuthEntry.PHASE_PREAUTH,
@ -625,15 +659,15 @@ export class SSOAuthEntry extends React.Component {
}; };
} }
componentDidMount(): void { componentDidMount() {
this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH); this.props.onPhaseChange(SSOAuthEntry.PHASE_PREAUTH);
} }
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener("message", this._onReceiveMessage); window.removeEventListener("message", this.onReceiveMessage);
if (this._popupWindow) { if (this.popupWindow) {
this._popupWindow.close(); this.popupWindow.close();
this._popupWindow = null; this.popupWindow = null;
} }
} }
@ -643,11 +677,11 @@ export class SSOAuthEntry extends React.Component {
}); });
}; };
_onReceiveMessage = event => { private onReceiveMessage = (event: MessageEvent) => {
if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) { if (event.data === "authDone" && event.origin === this.props.matrixClient.getHomeserverUrl()) {
if (this._popupWindow) { if (this.popupWindow) {
this._popupWindow.close(); this.popupWindow.close();
this._popupWindow = null; this.popupWindow = null;
} }
} }
}; };
@ -657,7 +691,7 @@ export class SSOAuthEntry extends React.Component {
// certainly will need to open the thing in a new tab to avoid losing application // certainly will need to open the thing in a new tab to avoid losing application
// context. // context.
this._popupWindow = window.open(this._ssoUrl, "_blank"); this.popupWindow = window.open(this.ssoUrl, "_blank");
this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH}); this.setState({phase: SSOAuthEntry.PHASE_POSTAUTH});
this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH); this.props.onPhaseChange(SSOAuthEntry.PHASE_POSTAUTH);
}; };
@ -716,46 +750,37 @@ export class SSOAuthEntry extends React.Component {
} }
@replaceableComponent("views.auth.FallbackAuthEntry") @replaceableComponent("views.auth.FallbackAuthEntry")
export class FallbackAuthEntry extends React.Component { export class FallbackAuthEntry extends React.Component<IAuthEntryProps> {
static propTypes = { private popupWindow: Window;
matrixClient: PropTypes.object.isRequired, private fallbackButton = createRef<HTMLAnchorElement>();
authSessionId: PropTypes.string.isRequired,
loginType: PropTypes.string.isRequired,
submitAuthDict: PropTypes.func.isRequired,
errorText: PropTypes.string,
onPhaseChange: PropTypes.func.isRequired,
};
constructor(props) { constructor(props) {
super(props); super(props);
// we have to make the user click a button, as browsers will block // we have to make the user click a button, as browsers will block
// the popup if we open it immediately. // the popup if we open it immediately.
this._popupWindow = null; this.popupWindow = null;
window.addEventListener("message", this._onReceiveMessage); window.addEventListener("message", this.onReceiveMessage);
this._fallbackButton = createRef();
} }
componentDidMount() { componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE); this.props.onPhaseChange(DEFAULT_PHASE);
} }
componentWillUnmount() { componentWillUnmount() {
window.removeEventListener("message", this._onReceiveMessage); window.removeEventListener("message", this.onReceiveMessage);
if (this._popupWindow) { if (this.popupWindow) {
this._popupWindow.close(); this.popupWindow.close();
} }
} }
focus = () => { focus = () => {
if (this._fallbackButton.current) { if (this.fallbackButton.current) {
this._fallbackButton.current.focus(); this.fallbackButton.current.focus();
} }
}; };
_onShowFallbackClick = e => { private onShowFallbackClick = (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -763,10 +788,10 @@ export class FallbackAuthEntry extends React.Component {
this.props.loginType, this.props.loginType,
this.props.authSessionId, this.props.authSessionId,
); );
this._popupWindow = window.open(url, "_blank"); this.popupWindow = window.open(url, "_blank");
}; };
_onReceiveMessage = event => { private onReceiveMessage = (event: MessageEvent) => {
if ( if (
event.data === "authDone" && event.data === "authDone" &&
event.origin === this.props.matrixClient.getHomeserverUrl() event.origin === this.props.matrixClient.getHomeserverUrl()
@ -786,7 +811,7 @@ export class FallbackAuthEntry extends React.Component {
} }
return ( return (
<div> <div>
<a href="" ref={this._fallbackButton} onClick={this._onShowFallbackClick}>{ <a href="" ref={this.fallbackButton} onClick={this.onShowFallbackClick}>{
_t("Start authentication") _t("Start authentication")
}</a> }</a>
{errorSection} {errorSection}
@ -795,20 +820,22 @@ export class FallbackAuthEntry extends React.Component {
} }
} }
const AuthEntryComponents = [ export default function getEntryComponentForLoginType(loginType: AuthType): typeof React.Component {
PasswordAuthEntry, switch (loginType) {
RecaptchaAuthEntry, case AuthType.Password:
EmailIdentityAuthEntry, return PasswordAuthEntry;
MsisdnAuthEntry, case AuthType.Recaptcha:
TermsAuthEntry, return RecaptchaAuthEntry;
SSOAuthEntry, case AuthType.Email:
]; return EmailIdentityAuthEntry;
case AuthType.Msisdn:
export default function getEntryComponentForLoginType(loginType) { return MsisdnAuthEntry;
for (const c of AuthEntryComponents) { case AuthType.Terms:
if (c.LOGIN_TYPE === loginType || c.UNSTABLE_LOGIN_TYPE === loginType) { return TermsAuthEntry;
return c; case AuthType.Sso:
} case AuthType.SsoUnstable:
return SSOAuthEntry;
default:
return FallbackAuthEntry;
} }
return FallbackAuthEntry;
} }

View File

@ -105,12 +105,14 @@ function safeCounterpartTranslate(text: string, options?: object) {
return translated; return translated;
} }
type SubstitutionValue = number | string | React.ReactNode | ((sub: string) => React.ReactNode);
export interface IVariables { export interface IVariables {
count?: number; count?: number;
[key: string]: number | string; [key: string]: SubstitutionValue;
} }
type Tags = Record<string, (sub: string) => React.ReactNode>; type Tags = Record<string, SubstitutionValue>;
export type TranslatedString = string | React.ReactNode; export type TranslatedString = string | React.ReactNode;
@ -247,7 +249,7 @@ export function replaceByRegexes(text: string, mapping: IVariables | Tags): stri
let replaced; let replaced;
// If substitution is a function, call it // If substitution is a function, call it
if (mapping[regexpString] instanceof Function) { if (mapping[regexpString] instanceof Function) {
replaced = (mapping as Tags)[regexpString].apply(null, capturedGroups); replaced = ((mapping as Tags)[regexpString] as Function)(...capturedGroups);
} else { } else {
replaced = mapping[regexpString]; replaced = mapping[regexpString];
} }