mirror of https://github.com/vector-im/riot-web
Add types to InteractiveAuthEntryComponents
parent
6574ca98fa
commit
df09bdf823
14
src/Terms.ts
14
src/Terms.ts
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue