Merge pull request #5433 from matrix-org/t3chguy/socials_preamble
Auth typescripting and validation tweaks
This commit is contained in:
commit
56ffa17b89
9 changed files with 924 additions and 783 deletions
|
@ -1,7 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016, 2017, 2018, 2019 New Vector Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2018, 2019 New Vector Ltd
|
|
||||||
|
|
||||||
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,8 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {ComponentProps, ReactNode} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {_t, _td} from '../../../languageHandler';
|
import {_t, _td} from '../../../languageHandler';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import Login from '../../../Login';
|
import Login from '../../../Login';
|
||||||
|
@ -31,15 +29,12 @@ import PlatformPeg from '../../../PlatformPeg';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {UIFeature} from "../../../settings/UIFeature";
|
import {UIFeature} from "../../../settings/UIFeature";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
|
import {IMatrixClientCreds} from "../../../MatrixClientPeg";
|
||||||
// For validating phone numbers without country codes
|
import ServerConfig from "../../views/auth/ServerConfig";
|
||||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
import PasswordLogin from "../../views/auth/PasswordLogin";
|
||||||
|
import SignInToText from "../../views/auth/SignInToText";
|
||||||
// Phases
|
import InlineSpinner from "../../views/elements/InlineSpinner";
|
||||||
// Show controls to configure server details
|
import Spinner from "../../views/elements/Spinner";
|
||||||
const PHASE_SERVER_DETAILS = 0;
|
|
||||||
// Show the appropriate login flow(s) for the server
|
|
||||||
const PHASE_LOGIN = 1;
|
|
||||||
|
|
||||||
// Enable phases for login
|
// Enable phases for login
|
||||||
const PHASES_ENABLED = true;
|
const PHASES_ENABLED = true;
|
||||||
|
@ -55,64 +50,88 @@ _td("Invalid base_url for m.identity_server");
|
||||||
_td("Identity server URL does not appear to be a valid identity server");
|
_td("Identity server URL does not appear to be a valid identity server");
|
||||||
_td("General failure");
|
_td("General failure");
|
||||||
|
|
||||||
/*
|
interface IProps {
|
||||||
* A wire component which glues together login UI components and Login logic
|
serverConfig: ValidatedServerConfig;
|
||||||
*/
|
// If true, the component will consider itself busy.
|
||||||
export default class LoginComponent extends React.Component {
|
busy?: boolean;
|
||||||
static propTypes = {
|
isSyncing?: boolean;
|
||||||
|
// Secondary HS which we try to log into if the user is using
|
||||||
|
// the default HS but login fails. Useful for migrating to a
|
||||||
|
// different homeserver without confusing users.
|
||||||
|
fallbackHsUrl?: string;
|
||||||
|
defaultDeviceDisplayName?: string;
|
||||||
|
fragmentAfterLogin?: string;
|
||||||
|
|
||||||
// Called when the user has logged in. Params:
|
// Called when the user has logged in. Params:
|
||||||
// - The object returned by the login API
|
// - The object returned by the login API
|
||||||
// - The user's password, if applicable, (may be cached in memory for a
|
// - The user's password, if applicable, (may be cached in memory for a
|
||||||
// short time so the user is not required to re-enter their password
|
// short time so the user is not required to re-enter their password
|
||||||
// for operations like uploading cross-signing keys).
|
// for operations like uploading cross-signing keys).
|
||||||
onLoggedIn: PropTypes.func.isRequired,
|
onLoggedIn(data: IMatrixClientCreds, password: string): void;
|
||||||
|
|
||||||
// If true, the component will consider itself busy.
|
// login shouldn't know or care how registration, password recovery, etc is done.
|
||||||
busy: PropTypes.bool,
|
onRegisterClick(): void;
|
||||||
|
onForgotPasswordClick?(): void;
|
||||||
|
onServerConfigChange(config: ValidatedServerConfig): void;
|
||||||
|
}
|
||||||
|
|
||||||
// Secondary HS which we try to log into if the user is using
|
enum Phase {
|
||||||
// the default HS but login fails. Useful for migrating to a
|
// Show controls to configure server details
|
||||||
// different homeserver without confusing users.
|
ServerDetails,
|
||||||
fallbackHsUrl: PropTypes.string,
|
// Show the appropriate login flow(s) for the server
|
||||||
|
Login,
|
||||||
|
}
|
||||||
|
|
||||||
defaultDeviceDisplayName: PropTypes.string,
|
interface IState {
|
||||||
|
busy: boolean;
|
||||||
|
busyLoggingIn?: boolean;
|
||||||
|
errorText?: ReactNode;
|
||||||
|
loginIncorrect: boolean;
|
||||||
|
// can we attempt to log in or are there validation errors?
|
||||||
|
canTryLogin: boolean;
|
||||||
|
|
||||||
// login shouldn't know or care how registration, password recovery,
|
// used for preserving form values when changing homeserver
|
||||||
// etc is done.
|
username: string;
|
||||||
onRegisterClick: PropTypes.func.isRequired,
|
phoneCountry?: string;
|
||||||
onForgotPasswordClick: PropTypes.func,
|
phoneNumber: string;
|
||||||
onServerConfigChange: PropTypes.func.isRequired,
|
|
||||||
|
|
||||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
// Phase of the overall login dialog.
|
||||||
isSyncing: PropTypes.bool,
|
phase: Phase;
|
||||||
};
|
// The current login flow, such as password, SSO, etc.
|
||||||
|
// we need to load the flows from the server
|
||||||
|
currentFlow?: string;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// be seeing.
|
||||||
|
serverIsAlive: boolean;
|
||||||
|
serverErrorIsFatal: boolean;
|
||||||
|
serverDeadError: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A wire component which glues together login UI components and Login logic
|
||||||
|
*/
|
||||||
|
export default class LoginComponent extends React.Component<IProps, IState> {
|
||||||
|
private unmounted = false;
|
||||||
|
private loginLogic: Login;
|
||||||
|
private readonly stepRendererMap: Record<string, () => ReactNode>;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._unmounted = false;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
busy: false,
|
busy: false,
|
||||||
busyLoggingIn: null,
|
busyLoggingIn: null,
|
||||||
errorText: null,
|
errorText: null,
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
canTryLogin: true, // can we attempt to log in or are there validation errors?
|
canTryLogin: true,
|
||||||
|
|
||||||
// used for preserving form values when changing homeserver
|
|
||||||
username: "",
|
username: "",
|
||||||
phoneCountry: null,
|
phoneCountry: null,
|
||||||
phoneNumber: "",
|
phoneNumber: "",
|
||||||
|
phase: Phase.Login,
|
||||||
// Phase of the overall login dialog.
|
currentFlow: null,
|
||||||
phase: PHASE_LOGIN,
|
|
||||||
// The current login flow, such as password, SSO, etc.
|
|
||||||
currentFlow: null, // we need to load the flows from the server
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// be seeing.
|
|
||||||
serverIsAlive: true,
|
serverIsAlive: true,
|
||||||
serverErrorIsFatal: false,
|
serverErrorIsFatal: false,
|
||||||
serverDeadError: "",
|
serverDeadError: "",
|
||||||
|
@ -120,12 +139,12 @@ export default class LoginComponent extends React.Component {
|
||||||
|
|
||||||
// map from login step type to a function which will render a control
|
// map from login step type to a function which will render a control
|
||||||
// letting you do that login type
|
// letting you do that login type
|
||||||
this._stepRendererMap = {
|
this.stepRendererMap = {
|
||||||
'm.login.password': this._renderPasswordStep,
|
'm.login.password': this.renderPasswordStep,
|
||||||
|
|
||||||
// CAS and SSO are the same thing, modulo the url we link to
|
// CAS and SSO are the same thing, modulo the url we link to
|
||||||
'm.login.cas': () => this._renderSsoStep("cas"),
|
'm.login.cas': () => this.renderSsoStep("cas"),
|
||||||
'm.login.sso': () => this._renderSsoStep("sso"),
|
'm.login.sso': () => this.renderSsoStep("sso"),
|
||||||
};
|
};
|
||||||
|
|
||||||
CountlyAnalytics.instance.track("onboarding_login_begin");
|
CountlyAnalytics.instance.track("onboarding_login_begin");
|
||||||
|
@ -134,11 +153,11 @@ export default class LoginComponent extends React.Component {
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
UNSAFE_componentWillMount() {
|
UNSAFE_componentWillMount() {
|
||||||
this._initLoginLogic();
|
this.initLoginLogic(this.props.serverConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._unmounted = true;
|
this.unmounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
|
@ -148,16 +167,9 @@ export default class LoginComponent extends React.Component {
|
||||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||||
|
|
||||||
// Ensure that we end up actually logging in to the right place
|
// Ensure that we end up actually logging in to the right place
|
||||||
this._initLoginLogic(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl);
|
this.initLoginLogic(newProps.serverConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
onPasswordLoginError = errorText => {
|
|
||||||
this.setState({
|
|
||||||
errorText,
|
|
||||||
loginIncorrect: Boolean(errorText),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
isBusy = () => this.state.busy || this.props.busy;
|
isBusy = () => this.state.busy || this.props.busy;
|
||||||
|
|
||||||
onPasswordLogin = async (username, phoneCountry, phoneNumber, password) => {
|
onPasswordLogin = async (username, phoneCountry, phoneNumber, password) => {
|
||||||
|
@ -194,13 +206,13 @@ export default class LoginComponent extends React.Component {
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
this._loginLogic.loginViaPassword(
|
this.loginLogic.loginViaPassword(
|
||||||
username, phoneCountry, phoneNumber, password,
|
username, phoneCountry, phoneNumber, password,
|
||||||
).then((data) => {
|
).then((data) => {
|
||||||
this.setState({serverIsAlive: true}); // it must be, we logged in.
|
this.setState({serverIsAlive: true}); // it must be, we logged in.
|
||||||
this.props.onLoggedIn(data, password);
|
this.props.onLoggedIn(data, password);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
if (this._unmounted) {
|
if (this.unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let errorText;
|
let errorText;
|
||||||
|
@ -212,21 +224,23 @@ export default class LoginComponent extends React.Component {
|
||||||
} else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
} else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||||
const errorTop = messageForResourceLimitError(
|
const errorTop = messageForResourceLimitError(
|
||||||
error.data.limit_type,
|
error.data.limit_type,
|
||||||
error.data.admin_contact, {
|
error.data.admin_contact,
|
||||||
|
{
|
||||||
'monthly_active_user': _td(
|
'monthly_active_user': _td(
|
||||||
"This homeserver has hit its Monthly Active User limit.",
|
"This homeserver has hit its Monthly Active User limit.",
|
||||||
),
|
),
|
||||||
'': _td(
|
'': _td(
|
||||||
"This homeserver has exceeded one of its resource limits.",
|
"This homeserver has exceeded one of its resource limits.",
|
||||||
),
|
),
|
||||||
});
|
},
|
||||||
|
);
|
||||||
const errorDetail = messageForResourceLimitError(
|
const errorDetail = messageForResourceLimitError(
|
||||||
error.data.limit_type,
|
error.data.limit_type,
|
||||||
error.data.admin_contact, {
|
error.data.admin_contact,
|
||||||
'': _td(
|
{
|
||||||
"Please <a>contact your service administrator</a> to continue using this service.",
|
'': _td("Please <a>contact your service administrator</a> to continue using this service."),
|
||||||
),
|
},
|
||||||
});
|
);
|
||||||
errorText = (
|
errorText = (
|
||||||
<div>
|
<div>
|
||||||
<div>{errorTop}</div>
|
<div>{errorTop}</div>
|
||||||
|
@ -253,7 +267,7 @@ export default class LoginComponent extends React.Component {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// other errors, not specific to doing a password login
|
// other errors, not specific to doing a password login
|
||||||
errorText = this._errorTextFromError(error);
|
errorText = this.errorTextFromError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -291,7 +305,7 @@ export default class LoginComponent extends React.Component {
|
||||||
// the busy state. In the case of a full MXID that resolves to the same
|
// the busy state. In the case of a full MXID that resolves to the same
|
||||||
// HS as Element's default HS though, there may not be any server change.
|
// HS as Element's default HS though, there may not be any server change.
|
||||||
// To avoid this trap, we clear busy here. For cases where the server
|
// To avoid this trap, we clear busy here. For cases where the server
|
||||||
// actually has changed, `_initLoginLogic` will be called and manages
|
// actually has changed, `initLoginLogic` will be called and manages
|
||||||
// busy state for its own liveness check.
|
// busy state for its own liveness check.
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false,
|
busy: false,
|
||||||
|
@ -304,7 +318,7 @@ export default class LoginComponent extends React.Component {
|
||||||
message = e.translatedMessage;
|
message = e.translatedMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
let errorText = message;
|
let errorText: ReactNode = message;
|
||||||
let discoveryState = {};
|
let discoveryState = {};
|
||||||
if (AutoDiscoveryUtils.isLivelinessError(e)) {
|
if (AutoDiscoveryUtils.isLivelinessError(e)) {
|
||||||
errorText = this.state.errorText;
|
errorText = this.state.errorText;
|
||||||
|
@ -330,21 +344,6 @@ export default class LoginComponent extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onPhoneNumberBlur = phoneNumber => {
|
|
||||||
// Validate the phone number entered
|
|
||||||
if (!PHONE_NUMBER_REGEX.test(phoneNumber)) {
|
|
||||||
this.setState({
|
|
||||||
errorText: _t('The phone number entered looks invalid'),
|
|
||||||
canTryLogin: false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
errorText: null,
|
|
||||||
canTryLogin: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onRegisterClick = ev => {
|
onRegisterClick = ev => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
@ -352,14 +351,14 @@ export default class LoginComponent extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
onTryRegisterClick = ev => {
|
onTryRegisterClick = ev => {
|
||||||
const step = this._getCurrentFlowStep();
|
const step = this.getCurrentFlowStep();
|
||||||
if (step === 'm.login.sso' || step === 'm.login.cas') {
|
if (step === 'm.login.sso' || step === 'm.login.cas') {
|
||||||
// If we're showing SSO it means that registration is also probably disabled,
|
// If we're showing SSO it means that registration is also probably disabled,
|
||||||
// so intercept the click and instead pretend the user clicked 'Sign in with SSO'.
|
// so intercept the click and instead pretend the user clicked 'Sign in with SSO'.
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
const ssoKind = step === 'm.login.sso' ? 'sso' : 'cas';
|
const ssoKind = step === 'm.login.sso' ? 'sso' : 'cas';
|
||||||
PlatformPeg.get().startSingleSignOn(this._loginLogic.createTemporaryClient(), ssoKind,
|
PlatformPeg.get().startSingleSignOn(this.loginLogic.createTemporaryClient(), ssoKind,
|
||||||
this.props.fragmentAfterLogin);
|
this.props.fragmentAfterLogin);
|
||||||
} else {
|
} else {
|
||||||
// Don't intercept - just go through to the register page
|
// Don't intercept - just go through to the register page
|
||||||
|
@ -367,24 +366,21 @@ export default class LoginComponent extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onServerDetailsNextPhaseClick = () => {
|
private onServerDetailsNextPhaseClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_LOGIN,
|
phase: Phase.Login,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onEditServerDetailsClick = ev => {
|
private onEditServerDetailsClick = ev => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_SERVER_DETAILS,
|
phase: Phase.ServerDetails,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
async _initLoginLogic(hsUrl, isUrl) {
|
private async initLoginLogic({hsUrl, isUrl}: ValidatedServerConfig) {
|
||||||
hsUrl = hsUrl || this.props.serverConfig.hsUrl;
|
|
||||||
isUrl = isUrl || this.props.serverConfig.isUrl;
|
|
||||||
|
|
||||||
let isDefaultServer = false;
|
let isDefaultServer = false;
|
||||||
if (this.props.serverConfig.isDefault
|
if (this.props.serverConfig.isDefault
|
||||||
&& hsUrl === this.props.serverConfig.hsUrl
|
&& hsUrl === this.props.serverConfig.hsUrl
|
||||||
|
@ -397,7 +393,7 @@ export default class LoginComponent extends React.Component {
|
||||||
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||||
});
|
});
|
||||||
this._loginLogic = loginLogic;
|
this.loginLogic = loginLogic;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: true,
|
busy: true,
|
||||||
|
@ -428,7 +424,7 @@ export default class LoginComponent extends React.Component {
|
||||||
if (this.state.serverErrorIsFatal) {
|
if (this.state.serverErrorIsFatal) {
|
||||||
// Server is dead: show server details prompt instead
|
// Server is dead: show server details prompt instead
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_SERVER_DETAILS,
|
phase: Phase.ServerDetails,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -437,7 +433,7 @@ export default class LoginComponent extends React.Component {
|
||||||
loginLogic.getFlows().then((flows) => {
|
loginLogic.getFlows().then((flows) => {
|
||||||
// look for a flow where we understand all of the steps.
|
// look for a flow where we understand all of the steps.
|
||||||
for (let i = 0; i < flows.length; i++ ) {
|
for (let i = 0; i < flows.length; i++ ) {
|
||||||
if (!this._isSupportedFlow(flows[i])) {
|
if (!this.isSupportedFlow(flows[i])) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -446,7 +442,7 @@ export default class LoginComponent extends React.Component {
|
||||||
// that for now).
|
// that for now).
|
||||||
loginLogic.chooseFlow(i);
|
loginLogic.chooseFlow(i);
|
||||||
this.setState({
|
this.setState({
|
||||||
currentFlow: this._getCurrentFlowStep(),
|
currentFlow: this.getCurrentFlowStep(),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -460,7 +456,7 @@ export default class LoginComponent extends React.Component {
|
||||||
});
|
});
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
errorText: this._errorTextFromError(err),
|
errorText: this.errorTextFromError(err),
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
canTryLogin: false,
|
canTryLogin: false,
|
||||||
});
|
});
|
||||||
|
@ -471,27 +467,27 @@ export default class LoginComponent extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_isSupportedFlow(flow) {
|
private isSupportedFlow(flow) {
|
||||||
// technically the flow can have multiple steps, but no one does this
|
// technically the flow can have multiple steps, but no one does this
|
||||||
// for login and loginLogic doesn't support it so we can ignore it.
|
// for login and loginLogic doesn't support it so we can ignore it.
|
||||||
if (!this._stepRendererMap[flow.type]) {
|
if (!this.stepRendererMap[flow.type]) {
|
||||||
console.log("Skipping flow", flow, "due to unsupported login type", flow.type);
|
console.log("Skipping flow", flow, "due to unsupported login type", flow.type);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getCurrentFlowStep() {
|
private getCurrentFlowStep() {
|
||||||
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
|
return this.loginLogic ? this.loginLogic.getCurrentFlowStep() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_errorTextFromError(err) {
|
private errorTextFromError(err) {
|
||||||
let errCode = err.errcode;
|
let errCode = err.errcode;
|
||||||
if (!errCode && err.httpStatus) {
|
if (!errCode && err.httpStatus) {
|
||||||
errCode = "HTTP " + err.httpStatus;
|
errCode = "HTTP " + err.httpStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
let errorText = _t("Error: Problem communicating with the given homeserver.") +
|
let errorText: ReactNode = _t("Error: Problem communicating with the given homeserver.") +
|
||||||
(errCode ? " (" + errCode + ")" : "");
|
(errCode ? " (" + errCode + ")" : "");
|
||||||
|
|
||||||
if (err.cors === 'rejected') {
|
if (err.cors === 'rejected') {
|
||||||
|
@ -510,8 +506,7 @@ export default class LoginComponent extends React.Component {
|
||||||
{ sub }
|
{ sub }
|
||||||
</a>;
|
</a>;
|
||||||
},
|
},
|
||||||
},
|
}) }
|
||||||
) }
|
|
||||||
</span>;
|
</span>;
|
||||||
} else {
|
} else {
|
||||||
errorText = <span>
|
errorText = <span>
|
||||||
|
@ -523,8 +518,7 @@ export default class LoginComponent extends React.Component {
|
||||||
<a target="_blank" rel="noreferrer noopener" href={this.props.serverConfig.hsUrl}>
|
<a target="_blank" rel="noreferrer noopener" href={this.props.serverConfig.hsUrl}>
|
||||||
{ sub }
|
{ sub }
|
||||||
</a>,
|
</a>,
|
||||||
},
|
}) }
|
||||||
) }
|
|
||||||
</span>;
|
</span>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -532,18 +526,16 @@ export default class LoginComponent extends React.Component {
|
||||||
return errorText;
|
return errorText;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderServerComponent() {
|
private renderServerComponent() {
|
||||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
|
||||||
|
|
||||||
if (SdkConfig.get()['disable_custom_urls']) {
|
if (SdkConfig.get()['disable_custom_urls']) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS) {
|
if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverDetailsProps = {};
|
const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
|
||||||
if (PHASES_ENABLED) {
|
if (PHASES_ENABLED) {
|
||||||
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
|
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
|
||||||
serverDetailsProps.submitText = _t("Next");
|
serverDetailsProps.submitText = _t("Next");
|
||||||
|
@ -558,8 +550,8 @@ export default class LoginComponent extends React.Component {
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLoginComponentForStep() {
|
private renderLoginComponentForStep() {
|
||||||
if (PHASES_ENABLED && this.state.phase !== PHASE_LOGIN) {
|
if (PHASES_ENABLED && this.state.phase !== Phase.Login) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -569,7 +561,7 @@ export default class LoginComponent extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stepRenderer = this._stepRendererMap[step];
|
const stepRenderer = this.stepRendererMap[step];
|
||||||
|
|
||||||
if (stepRenderer) {
|
if (stepRenderer) {
|
||||||
return stepRenderer();
|
return stepRenderer();
|
||||||
|
@ -578,9 +570,7 @@ export default class LoginComponent extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderPasswordStep = () => {
|
private renderPasswordStep = () => {
|
||||||
const PasswordLogin = sdk.getComponent('auth.PasswordLogin');
|
|
||||||
|
|
||||||
let onEditServerDetailsClick = null;
|
let onEditServerDetailsClick = null;
|
||||||
// If custom URLs are allowed, wire up the server details edit link.
|
// If custom URLs are allowed, wire up the server details edit link.
|
||||||
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
|
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
|
||||||
|
@ -590,16 +580,14 @@ export default class LoginComponent extends React.Component {
|
||||||
return (
|
return (
|
||||||
<PasswordLogin
|
<PasswordLogin
|
||||||
onSubmit={this.onPasswordLogin}
|
onSubmit={this.onPasswordLogin}
|
||||||
onError={this.onPasswordLoginError}
|
|
||||||
onEditServerDetailsClick={onEditServerDetailsClick}
|
onEditServerDetailsClick={onEditServerDetailsClick}
|
||||||
initialUsername={this.state.username}
|
username={this.state.username}
|
||||||
initialPhoneCountry={this.state.phoneCountry}
|
phoneCountry={this.state.phoneCountry}
|
||||||
initialPhoneNumber={this.state.phoneNumber}
|
phoneNumber={this.state.phoneNumber}
|
||||||
onUsernameChanged={this.onUsernameChanged}
|
onUsernameChanged={this.onUsernameChanged}
|
||||||
onUsernameBlur={this.onUsernameBlur}
|
onUsernameBlur={this.onUsernameBlur}
|
||||||
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
||||||
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||||
onPhoneNumberBlur={this.onPhoneNumberBlur}
|
|
||||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||||
loginIncorrect={this.state.loginIncorrect}
|
loginIncorrect={this.state.loginIncorrect}
|
||||||
serverConfig={this.props.serverConfig}
|
serverConfig={this.props.serverConfig}
|
||||||
|
@ -609,9 +597,7 @@ export default class LoginComponent extends React.Component {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
_renderSsoStep = loginType => {
|
private renderSsoStep = loginType => {
|
||||||
const SignInToText = sdk.getComponent('views.auth.SignInToText');
|
|
||||||
|
|
||||||
let onEditServerDetailsClick = null;
|
let onEditServerDetailsClick = null;
|
||||||
// If custom URLs are allowed, wire up the server details edit link.
|
// If custom URLs are allowed, wire up the server details edit link.
|
||||||
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
|
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
|
||||||
|
@ -632,7 +618,7 @@ export default class LoginComponent extends React.Component {
|
||||||
|
|
||||||
<SSOButton
|
<SSOButton
|
||||||
className="mx_Login_sso_link mx_Login_submit"
|
className="mx_Login_sso_link mx_Login_submit"
|
||||||
matrixClient={this._loginLogic.createTemporaryClient()}
|
matrixClient={this.loginLogic.createTemporaryClient()}
|
||||||
loginType={loginType}
|
loginType={loginType}
|
||||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||||
/>
|
/>
|
||||||
|
@ -641,12 +627,10 @@ export default class LoginComponent extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
|
||||||
const InlineSpinner = sdk.getComponent("elements.InlineSpinner");
|
|
||||||
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
||||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||||
const loader = this.isBusy() && !this.state.busyLoggingIn ?
|
const loader = this.isBusy() && !this.state.busyLoggingIn ?
|
||||||
<div className="mx_Login_loader"><Loader /></div> : null;
|
<div className="mx_Login_loader"><Spinner /></div> : null;
|
||||||
|
|
||||||
const errorText = this.state.errorText;
|
const errorText = this.state.errorText;
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016, 2017, 2018, 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2018, 2019 New Vector 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.
|
||||||
|
@ -18,8 +15,9 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import Matrix from 'matrix-js-sdk';
|
import Matrix from 'matrix-js-sdk';
|
||||||
import React from 'react';
|
import React, {ComponentProps, ReactNode} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
@ -34,43 +32,51 @@ import Login from "../../../Login";
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
|
|
||||||
// Phases
|
// Phases
|
||||||
|
enum Phase {
|
||||||
// Show controls to configure server details
|
// Show controls to configure server details
|
||||||
const PHASE_SERVER_DETAILS = 0;
|
ServerDetails = 0,
|
||||||
// Show the appropriate registration flow(s) for the server
|
// Show the appropriate registration flow(s) for the server
|
||||||
const PHASE_REGISTRATION = 1;
|
Registration = 1,
|
||||||
|
}
|
||||||
|
|
||||||
// Enable phases for registration
|
interface IProps {
|
||||||
const PHASES_ENABLED = true;
|
serverConfig: ValidatedServerConfig;
|
||||||
|
defaultDeviceDisplayName: string;
|
||||||
|
email?: string;
|
||||||
|
brand?: string;
|
||||||
|
clientSecret?: string;
|
||||||
|
sessionId?: string;
|
||||||
|
idSid?: string;
|
||||||
|
|
||||||
export default class Registration extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
// Called when the user has logged in. Params:
|
// Called when the user has logged in. Params:
|
||||||
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
|
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
|
||||||
// - The user's password, if available and applicable (may be cached in memory
|
// - The user's password, if available and applicable (may be cached in memory
|
||||||
// for a short time so the user is not required to re-enter their password
|
// for a short time so the user is not required to re-enter their password
|
||||||
// for operations like uploading cross-signing keys).
|
// for operations like uploading cross-signing keys).
|
||||||
onLoggedIn: PropTypes.func.isRequired,
|
onLoggedIn(params: {
|
||||||
|
userId: string;
|
||||||
clientSecret: PropTypes.string,
|
deviceId: string
|
||||||
sessionId: PropTypes.string,
|
homeserverUrl: string;
|
||||||
makeRegistrationUrl: PropTypes.func.isRequired,
|
identityServerUrl?: string;
|
||||||
idSid: PropTypes.string,
|
accessToken: string;
|
||||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
}, password: string): void;
|
||||||
brand: PropTypes.string,
|
makeRegistrationUrl(params: {
|
||||||
email: PropTypes.string,
|
/* eslint-disable camelcase */
|
||||||
|
client_secret: string;
|
||||||
|
hs_url: string;
|
||||||
|
is_url?: string;
|
||||||
|
session_id: string;
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
}): void;
|
||||||
// registration shouldn't know or care how login is done.
|
// registration shouldn't know or care how login is done.
|
||||||
onLoginClick: PropTypes.func.isRequired,
|
onLoginClick(): void;
|
||||||
onServerConfigChange: PropTypes.func.isRequired,
|
onServerConfigChange(config: ValidatedServerConfig): void;
|
||||||
defaultDeviceDisplayName: PropTypes.string,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
interface IState {
|
||||||
super(props);
|
busy: boolean;
|
||||||
|
errorText?: ReactNode;
|
||||||
const serverType = ServerType.getTypeFromServerConfig(this.props.serverConfig);
|
// true if we're waiting for the user to complete
|
||||||
this.state = {
|
|
||||||
busy: false,
|
|
||||||
errorText: null,
|
|
||||||
// We remember the values entered by the user because
|
// We remember the values entered by the user because
|
||||||
// the registration form will be unmounted during the
|
// the registration form will be unmounted during the
|
||||||
// course of registration, but if there's an error we
|
// course of registration, but if there's an error we
|
||||||
|
@ -78,49 +84,67 @@ export default class Registration extends React.Component {
|
||||||
// values the user entered still in it. We can keep
|
// values the user entered still in it. We can keep
|
||||||
// them in this component's state since this component
|
// them in this component's state since this component
|
||||||
// persist for the duration of the registration process.
|
// persist for the duration of the registration process.
|
||||||
formVals: {
|
formVals: Record<string, string>;
|
||||||
email: this.props.email,
|
|
||||||
},
|
|
||||||
// true if we're waiting for the user to complete
|
|
||||||
// user-interactive auth
|
// user-interactive auth
|
||||||
// If we've been given a session ID, we're resuming
|
// If we've been given a session ID, we're resuming
|
||||||
// straight back into UI auth
|
// straight back into UI auth
|
||||||
doingUIAuth: Boolean(this.props.sessionId),
|
doingUIAuth: boolean;
|
||||||
serverType,
|
|
||||||
// Phase of the overall registration dialog.
|
|
||||||
phase: PHASE_REGISTRATION,
|
|
||||||
flows: null,
|
|
||||||
// If set, we've registered but are not going to log
|
// If set, we've registered but are not going to log
|
||||||
// the user in to their new account automatically.
|
// the user in to their new account automatically.
|
||||||
completedNoSignin: false,
|
completedNoSignin: boolean;
|
||||||
|
serverType: ServerType.FREE | ServerType.PREMIUM | ServerType.ADVANCED;
|
||||||
|
// Phase of the overall registration dialog.
|
||||||
|
phase: Phase;
|
||||||
|
flows: {
|
||||||
|
stages: string[];
|
||||||
|
}[];
|
||||||
// We perform liveliness checks later, but for now suppress the errors.
|
// We perform liveliness checks later, but for now suppress the errors.
|
||||||
// We also track the server dead errors independently of the regular errors so
|
// 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
|
// that we can render it differently, and override any other error the user may
|
||||||
// be seeing.
|
// be seeing.
|
||||||
serverIsAlive: true,
|
serverIsAlive: boolean;
|
||||||
serverErrorIsFatal: false,
|
serverErrorIsFatal: boolean;
|
||||||
serverDeadError: "",
|
serverDeadError: string;
|
||||||
|
|
||||||
// Our matrix client - part of state because we can't render the UI auth
|
// Our matrix client - part of state because we can't render the UI auth
|
||||||
// component without it.
|
// component without it.
|
||||||
matrixClient: null,
|
matrixClient?: MatrixClient;
|
||||||
|
|
||||||
// whether the HS requires an ID server to register with a threepid
|
// whether the HS requires an ID server to register with a threepid
|
||||||
serverRequiresIdServer: null,
|
serverRequiresIdServer?: boolean;
|
||||||
|
|
||||||
// The user ID we've just registered
|
// The user ID we've just registered
|
||||||
registeredUsername: null,
|
registeredUsername?: string;
|
||||||
|
|
||||||
// if a different user ID to the one we just registered is logged in,
|
// if a different user ID to the one we just registered is logged in,
|
||||||
// this is the user ID that's logged in.
|
// this is the user ID that's logged in.
|
||||||
differentLoggedInUserId: null,
|
differentLoggedInUserId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable phases for registration
|
||||||
|
const PHASES_ENABLED = true;
|
||||||
|
|
||||||
|
export default class Registration extends React.Component<IProps, IState> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
const serverType = ServerType.getTypeFromServerConfig(this.props.serverConfig);
|
||||||
|
this.state = {
|
||||||
|
busy: false,
|
||||||
|
errorText: null,
|
||||||
|
formVals: {
|
||||||
|
email: this.props.email,
|
||||||
|
},
|
||||||
|
doingUIAuth: Boolean(this.props.sessionId),
|
||||||
|
serverType,
|
||||||
|
phase: Phase.Registration,
|
||||||
|
flows: null,
|
||||||
|
completedNoSignin: false,
|
||||||
|
serverIsAlive: true,
|
||||||
|
serverErrorIsFatal: false,
|
||||||
|
serverDeadError: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this._unmounted = false;
|
this.replaceClient(this.props.serverConfig);
|
||||||
this._replaceClient();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||||
|
@ -129,7 +153,7 @@ export default class Registration extends React.Component {
|
||||||
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
|
||||||
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
|
||||||
|
|
||||||
this._replaceClient(newProps.serverConfig);
|
this.replaceClient(newProps.serverConfig);
|
||||||
|
|
||||||
// Handle cases where the user enters "https://matrix.org" for their server
|
// Handle cases where the user enters "https://matrix.org" for their server
|
||||||
// from the advanced option - we should default to FREE at that point.
|
// from the advanced option - we should default to FREE at that point.
|
||||||
|
@ -138,25 +162,25 @@ export default class Registration extends React.Component {
|
||||||
// Reset the phase to default phase for the server type.
|
// Reset the phase to default phase for the server type.
|
||||||
this.setState({
|
this.setState({
|
||||||
serverType,
|
serverType,
|
||||||
phase: this.getDefaultPhaseForServerType(serverType),
|
phase: Registration.getDefaultPhaseForServerType(serverType),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultPhaseForServerType(type) {
|
private static getDefaultPhaseForServerType(type: IState["serverType"]) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case ServerType.FREE: {
|
case ServerType.FREE: {
|
||||||
// Move directly to the registration phase since the server
|
// Move directly to the registration phase since the server
|
||||||
// details are fixed.
|
// details are fixed.
|
||||||
return PHASE_REGISTRATION;
|
return Phase.Registration;
|
||||||
}
|
}
|
||||||
case ServerType.PREMIUM:
|
case ServerType.PREMIUM:
|
||||||
case ServerType.ADVANCED:
|
case ServerType.ADVANCED:
|
||||||
return PHASE_SERVER_DETAILS;
|
return Phase.ServerDetails;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onServerTypeChange = type => {
|
private onServerTypeChange = (type: IState["serverType"]) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
serverType: type,
|
serverType: type,
|
||||||
});
|
});
|
||||||
|
@ -181,11 +205,11 @@ export default class Registration extends React.Component {
|
||||||
|
|
||||||
// Reset the phase to default phase for the server type.
|
// Reset the phase to default phase for the server type.
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: this.getDefaultPhaseForServerType(type),
|
phase: Registration.getDefaultPhaseForServerType(type),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
async _replaceClient(serverConfig) {
|
private async replaceClient(serverConfig: ValidatedServerConfig) {
|
||||||
this.setState({
|
this.setState({
|
||||||
errorText: null,
|
errorText: null,
|
||||||
serverDeadError: null,
|
serverDeadError: null,
|
||||||
|
@ -194,7 +218,6 @@ export default class Registration extends React.Component {
|
||||||
// the UI auth component while we don't have a matrix client)
|
// the UI auth component while we don't have a matrix client)
|
||||||
busy: true,
|
busy: true,
|
||||||
});
|
});
|
||||||
if (!serverConfig) serverConfig = this.props.serverConfig;
|
|
||||||
|
|
||||||
// Do a liveliness check on the URLs
|
// Do a liveliness check on the URLs
|
||||||
try {
|
try {
|
||||||
|
@ -246,7 +269,7 @@ export default class Registration extends React.Component {
|
||||||
// do SSO instead. If we've already started the UI Auth process though, we don't
|
// do SSO instead. If we've already started the UI Auth process though, we don't
|
||||||
// need to.
|
// need to.
|
||||||
if (!this.state.doingUIAuth) {
|
if (!this.state.doingUIAuth) {
|
||||||
await this._makeRegisterRequest(null);
|
await this.makeRegisterRequest(null);
|
||||||
// This should never succeed since we specified no auth object.
|
// This should never succeed since we specified no auth object.
|
||||||
console.log("Expecting 401 from register request but got success!");
|
console.log("Expecting 401 from register request but got success!");
|
||||||
}
|
}
|
||||||
|
@ -287,7 +310,7 @@ export default class Registration extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onFormSubmit = formVals => {
|
private onFormSubmit = formVals => {
|
||||||
this.setState({
|
this.setState({
|
||||||
errorText: "",
|
errorText: "",
|
||||||
busy: true,
|
busy: true,
|
||||||
|
@ -296,7 +319,7 @@ export default class Registration extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_requestEmailToken = (emailAddress, clientSecret, sendAttempt, sessionId) => {
|
private requestEmailToken = (emailAddress, clientSecret, sendAttempt, sessionId) => {
|
||||||
return this.state.matrixClient.requestRegisterEmailToken(
|
return this.state.matrixClient.requestRegisterEmailToken(
|
||||||
emailAddress,
|
emailAddress,
|
||||||
clientSecret,
|
clientSecret,
|
||||||
|
@ -310,28 +333,26 @@ export default class Registration extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onUIAuthFinished = async (success, response, extra) => {
|
private onUIAuthFinished = async (success, response, extra) => {
|
||||||
if (!success) {
|
if (!success) {
|
||||||
let msg = response.message || response.toString();
|
let msg = response.message || response.toString();
|
||||||
// can we give a better error message?
|
// can we give a better error message?
|
||||||
if (response.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
if (response.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||||
const errorTop = messageForResourceLimitError(
|
const errorTop = messageForResourceLimitError(
|
||||||
response.data.limit_type,
|
response.data.limit_type,
|
||||||
response.data.admin_contact, {
|
response.data.admin_contact,
|
||||||
'monthly_active_user': _td(
|
{
|
||||||
"This homeserver has hit its Monthly Active User limit.",
|
'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."),
|
||||||
),
|
'': _td("This homeserver has exceeded one of its resource limits."),
|
||||||
'': _td(
|
},
|
||||||
"This homeserver has exceeded one of its resource limits.",
|
);
|
||||||
),
|
|
||||||
});
|
|
||||||
const errorDetail = messageForResourceLimitError(
|
const errorDetail = messageForResourceLimitError(
|
||||||
response.data.limit_type,
|
response.data.limit_type,
|
||||||
response.data.admin_contact, {
|
response.data.admin_contact,
|
||||||
'': _td(
|
{
|
||||||
"Please <a>contact your service administrator</a> to continue using this service.",
|
'': _td("Please <a>contact your service administrator</a> to continue using this service."),
|
||||||
),
|
},
|
||||||
});
|
);
|
||||||
msg = <div>
|
msg = <div>
|
||||||
<p>{errorTop}</p>
|
<p>{errorTop}</p>
|
||||||
<p>{errorDetail}</p>
|
<p>{errorDetail}</p>
|
||||||
|
@ -339,7 +360,7 @@ export default class Registration extends React.Component {
|
||||||
} else if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
} else if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
||||||
let msisdnAvailable = false;
|
let msisdnAvailable = false;
|
||||||
for (const flow of response.available_flows) {
|
for (const flow of response.available_flows) {
|
||||||
msisdnAvailable |= flow.stages.indexOf('m.login.msisdn') > -1;
|
msisdnAvailable = msisdnAvailable || flow.stages.includes('m.login.msisdn');
|
||||||
}
|
}
|
||||||
if (!msisdnAvailable) {
|
if (!msisdnAvailable) {
|
||||||
msg = _t('This server does not support authentication with a phone number.');
|
msg = _t('This server does not support authentication with a phone number.');
|
||||||
|
@ -358,6 +379,10 @@ export default class Registration extends React.Component {
|
||||||
const newState = {
|
const newState = {
|
||||||
doingUIAuth: false,
|
doingUIAuth: false,
|
||||||
registeredUsername: response.user_id,
|
registeredUsername: response.user_id,
|
||||||
|
differentLoggedInUserId: null,
|
||||||
|
completedNoSignin: false,
|
||||||
|
// we're still busy until we get unmounted: don't show the registration form again
|
||||||
|
busy: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// The user came in through an email validation link. To avoid overwriting
|
// The user came in through an email validation link. To avoid overwriting
|
||||||
|
@ -372,8 +397,6 @@ export default class Registration extends React.Component {
|
||||||
`Found a session for ${sessionOwner} but ${response.userId} has just registered.`,
|
`Found a session for ${sessionOwner} but ${response.userId} has just registered.`,
|
||||||
);
|
);
|
||||||
newState.differentLoggedInUserId = sessionOwner;
|
newState.differentLoggedInUserId = sessionOwner;
|
||||||
} else {
|
|
||||||
newState.differentLoggedInUserId = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.access_token) {
|
if (response.access_token) {
|
||||||
|
@ -385,9 +408,7 @@ export default class Registration extends React.Component {
|
||||||
accessToken: response.access_token,
|
accessToken: response.access_token,
|
||||||
}, this.state.formVals.password);
|
}, this.state.formVals.password);
|
||||||
|
|
||||||
this._setupPushers();
|
this.setupPushers();
|
||||||
// we're still busy until we get unmounted: don't show the registration form again
|
|
||||||
newState.busy = true;
|
|
||||||
} else {
|
} else {
|
||||||
newState.busy = false;
|
newState.busy = false;
|
||||||
newState.completedNoSignin = true;
|
newState.completedNoSignin = true;
|
||||||
|
@ -396,7 +417,7 @@ export default class Registration extends React.Component {
|
||||||
this.setState(newState);
|
this.setState(newState);
|
||||||
};
|
};
|
||||||
|
|
||||||
_setupPushers() {
|
private setupPushers() {
|
||||||
if (!this.props.brand) {
|
if (!this.props.brand) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@ -419,38 +440,38 @@ export default class Registration extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoginClick = ev => {
|
private onLoginClick = ev => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.props.onLoginClick();
|
this.props.onLoginClick();
|
||||||
};
|
};
|
||||||
|
|
||||||
onGoToFormClicked = ev => {
|
private onGoToFormClicked = ev => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this._replaceClient();
|
this.replaceClient(this.props.serverConfig);
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false,
|
busy: false,
|
||||||
doingUIAuth: false,
|
doingUIAuth: false,
|
||||||
phase: PHASE_REGISTRATION,
|
phase: Phase.Registration,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onServerDetailsNextPhaseClick = async () => {
|
private onServerDetailsNextPhaseClick = async () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_REGISTRATION,
|
phase: Phase.Registration,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onEditServerDetailsClick = ev => {
|
private onEditServerDetailsClick = ev => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_SERVER_DETAILS,
|
phase: Phase.ServerDetails,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_makeRegisterRequest = auth => {
|
private makeRegisterRequest = auth => {
|
||||||
// We inhibit login if we're trying to register with an email address: this
|
// We inhibit login if we're trying to register with an email address: this
|
||||||
// avoids a lot of complex race conditions that can occur if we try to log
|
// avoids a lot of complex race conditions that can occur if we try to log
|
||||||
// the user in one one or both of the tabs they might end up with after
|
// the user in one one or both of the tabs they might end up with after
|
||||||
|
@ -466,13 +487,15 @@ export default class Registration extends React.Component {
|
||||||
username: this.state.formVals.username,
|
username: this.state.formVals.username,
|
||||||
password: this.state.formVals.password,
|
password: this.state.formVals.password,
|
||||||
initial_device_display_name: this.props.defaultDeviceDisplayName,
|
initial_device_display_name: this.props.defaultDeviceDisplayName,
|
||||||
|
auth: undefined,
|
||||||
|
inhibit_login: undefined,
|
||||||
};
|
};
|
||||||
if (auth) registerParams.auth = auth;
|
if (auth) registerParams.auth = auth;
|
||||||
if (inhibitLogin !== undefined && inhibitLogin !== null) registerParams.inhibit_login = inhibitLogin;
|
if (inhibitLogin !== undefined && inhibitLogin !== null) registerParams.inhibit_login = inhibitLogin;
|
||||||
return this.state.matrixClient.registerRequest(registerParams);
|
return this.state.matrixClient.registerRequest(registerParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
_getUIAuthInputs() {
|
private getUIAuthInputs() {
|
||||||
return {
|
return {
|
||||||
emailAddress: this.state.formVals.email,
|
emailAddress: this.state.formVals.email,
|
||||||
phoneCountry: this.state.formVals.phoneCountry,
|
phoneCountry: this.state.formVals.phoneCountry,
|
||||||
|
@ -483,7 +506,7 @@ export default class Registration extends React.Component {
|
||||||
// Links to the login page shown after registration is completed are routed through this
|
// Links to the login page shown after registration is completed are routed through this
|
||||||
// which checks the user hasn't already logged in somewhere else (perhaps we should do
|
// which checks the user hasn't already logged in somewhere else (perhaps we should do
|
||||||
// this more generally?)
|
// this more generally?)
|
||||||
_onLoginClickWithCheck = async ev => {
|
private onLoginClickWithCheck = async ev => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
const sessionLoaded = await Lifecycle.loadSession({ignoreGuest: true});
|
const sessionLoaded = await Lifecycle.loadSession({ignoreGuest: true});
|
||||||
|
@ -493,7 +516,7 @@ export default class Registration extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderServerComponent() {
|
private renderServerComponent() {
|
||||||
const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
|
const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
|
||||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||||
const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
|
const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
|
||||||
|
@ -503,7 +526,7 @@ export default class Registration extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide the server picker once the user is doing UI Auth unless encountered a fatal server error
|
// Hide the server picker once the user is doing UI Auth unless encountered a fatal server error
|
||||||
if (this.state.phase !== PHASE_SERVER_DETAILS && this.state.doingUIAuth && !this.state.serverErrorIsFatal) {
|
if (this.state.phase !== Phase.ServerDetails && this.state.doingUIAuth && !this.state.serverErrorIsFatal) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -511,7 +534,7 @@ export default class Registration extends React.Component {
|
||||||
// which is always shown if we allow custom URLs at all.
|
// which is always shown if we allow custom URLs at all.
|
||||||
// (if there's a fatal server error, we need to show the full server
|
// (if there's a fatal server error, we need to show the full server
|
||||||
// config as the user may need to change servers to resolve the error).
|
// config as the user may need to change servers to resolve the error).
|
||||||
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS && !this.state.serverErrorIsFatal) {
|
if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails && !this.state.serverErrorIsFatal) {
|
||||||
return <div>
|
return <div>
|
||||||
<ServerTypeSelector
|
<ServerTypeSelector
|
||||||
selected={this.state.serverType}
|
selected={this.state.serverType}
|
||||||
|
@ -520,7 +543,7 @@ export default class Registration extends React.Component {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverDetailsProps = {};
|
const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
|
||||||
if (PHASES_ENABLED) {
|
if (PHASES_ENABLED) {
|
||||||
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
|
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
|
||||||
serverDetailsProps.submitText = _t("Next");
|
serverDetailsProps.submitText = _t("Next");
|
||||||
|
@ -559,8 +582,8 @@ export default class Registration extends React.Component {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderRegisterComponent() {
|
private renderRegisterComponent() {
|
||||||
if (PHASES_ENABLED && this.state.phase !== PHASE_REGISTRATION) {
|
if (PHASES_ENABLED && this.state.phase !== Phase.Registration) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -571,10 +594,10 @@ export default class Registration extends React.Component {
|
||||||
if (this.state.matrixClient && this.state.doingUIAuth) {
|
if (this.state.matrixClient && this.state.doingUIAuth) {
|
||||||
return <InteractiveAuth
|
return <InteractiveAuth
|
||||||
matrixClient={this.state.matrixClient}
|
matrixClient={this.state.matrixClient}
|
||||||
makeRequest={this._makeRegisterRequest}
|
makeRequest={this.makeRegisterRequest}
|
||||||
onAuthFinished={this._onUIAuthFinished}
|
onAuthFinished={this.onUIAuthFinished}
|
||||||
inputs={this._getUIAuthInputs()}
|
inputs={this.getUIAuthInputs()}
|
||||||
requestEmailToken={this._requestEmailToken}
|
requestEmailToken={this.requestEmailToken}
|
||||||
sessionId={this.props.sessionId}
|
sessionId={this.props.sessionId}
|
||||||
clientSecret={this.props.clientSecret}
|
clientSecret={this.props.clientSecret}
|
||||||
emailSid={this.props.idSid}
|
emailSid={this.props.idSid}
|
||||||
|
@ -633,7 +656,7 @@ export default class Registration extends React.Component {
|
||||||
|
|
||||||
// Only show the 'go back' button if you're not looking at the form
|
// Only show the 'go back' button if you're not looking at the form
|
||||||
let goBack;
|
let goBack;
|
||||||
if ((PHASES_ENABLED && this.state.phase !== PHASE_REGISTRATION) || this.state.doingUIAuth) {
|
if ((PHASES_ENABLED && this.state.phase !== Phase.Registration) || this.state.doingUIAuth) {
|
||||||
goBack = <a className="mx_AuthBody_changeFlow" onClick={this.onGoToFormClicked} href="#">
|
goBack = <a className="mx_AuthBody_changeFlow" onClick={this.onGoToFormClicked} href="#">
|
||||||
{ _t('Go back') }
|
{ _t('Go back') }
|
||||||
</a>;
|
</a>;
|
||||||
|
@ -651,7 +674,7 @@ export default class Registration extends React.Component {
|
||||||
loggedInUserId: this.state.differentLoggedInUserId,
|
loggedInUserId: this.state.differentLoggedInUserId,
|
||||||
},
|
},
|
||||||
)}</p>
|
)}</p>
|
||||||
<p><AccessibleButton element="span" className="mx_linkButton" onClick={this._onLoginClickWithCheck}>
|
<p><AccessibleButton element="span" className="mx_linkButton" onClick={this.onLoginClickWithCheck}>
|
||||||
{_t("Continue with previous account")}
|
{_t("Continue with previous account")}
|
||||||
</AccessibleButton></p>
|
</AccessibleButton></p>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -660,7 +683,7 @@ export default class Registration extends React.Component {
|
||||||
regDoneText = <h3>{_t(
|
regDoneText = <h3>{_t(
|
||||||
"<a>Log in</a> to your new account.", {},
|
"<a>Log in</a> to your new account.", {},
|
||||||
{
|
{
|
||||||
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
|
a: (sub) => <a href="#/login" onClick={this.onLoginClickWithCheck}>{sub}</a>,
|
||||||
},
|
},
|
||||||
)}</h3>;
|
)}</h3>;
|
||||||
} else {
|
} else {
|
||||||
|
@ -670,7 +693,7 @@ export default class Registration extends React.Component {
|
||||||
regDoneText = <h3>{_t(
|
regDoneText = <h3>{_t(
|
||||||
"You can now close this window or <a>log in</a> to your new account.", {},
|
"You can now close this window or <a>log in</a> to your new account.", {},
|
||||||
{
|
{
|
||||||
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>,
|
a: (sub) => <a href="#/login" onClick={this.onLoginClickWithCheck}>{sub}</a>,
|
||||||
},
|
},
|
||||||
)}</h3>;
|
)}</h3>;
|
||||||
}
|
}
|
||||||
|
@ -679,7 +702,7 @@ export default class Registration extends React.Component {
|
||||||
{ regDoneText }
|
{ regDoneText }
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', {
|
let yourMatrixAccountText: ReactNode = _t('Create your Matrix account on %(serverName)s', {
|
||||||
serverName: this.props.serverConfig.hsName,
|
serverName: this.props.serverConfig.hsName,
|
||||||
});
|
});
|
||||||
if (this.props.serverConfig.hsNameIsDifferent) {
|
if (this.props.serverConfig.hsNameIsDifferent) {
|
||||||
|
@ -717,7 +740,7 @@ export default class Registration extends React.Component {
|
||||||
{ errorText }
|
{ errorText }
|
||||||
{ serverDeadSection }
|
{ serverDeadSection }
|
||||||
{ this.renderServerComponent() }
|
{ this.renderServerComponent() }
|
||||||
{ this.state.phase !== PHASE_SERVER_DETAILS && <h3>
|
{ this.state.phase !== Phase.ServerDetails && <h3>
|
||||||
{yourMatrixAccountText}
|
{yourMatrixAccountText}
|
||||||
{editLink}
|
{editLink}
|
||||||
</h3> }
|
</h3> }
|
|
@ -21,9 +21,9 @@ import zxcvbn from "zxcvbn";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import withValidation, {IFieldState, IValidationResult} from "../elements/Validation";
|
import withValidation, {IFieldState, IValidationResult} from "../elements/Validation";
|
||||||
import {_t, _td} from "../../../languageHandler";
|
import {_t, _td} from "../../../languageHandler";
|
||||||
import Field from "../elements/Field";
|
import Field, {IInputProps} from "../elements/Field";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps extends Omit<IInputProps, "onValidate"> {
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
id?: string;
|
id?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
|
@ -1,377 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2019 New Vector Ltd.
|
|
||||||
|
|
||||||
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 React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import SdkConfig from '../../../SdkConfig';
|
|
||||||
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A pure UI component which displays a username/password form.
|
|
||||||
*/
|
|
||||||
export default class PasswordLogin extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
onSubmit: PropTypes.func.isRequired, // fn(username, password)
|
|
||||||
onError: PropTypes.func,
|
|
||||||
onEditServerDetailsClick: PropTypes.func,
|
|
||||||
onForgotPasswordClick: PropTypes.func, // fn()
|
|
||||||
initialUsername: PropTypes.string,
|
|
||||||
initialPhoneCountry: PropTypes.string,
|
|
||||||
initialPhoneNumber: PropTypes.string,
|
|
||||||
initialPassword: PropTypes.string,
|
|
||||||
onUsernameChanged: PropTypes.func,
|
|
||||||
onPhoneCountryChanged: PropTypes.func,
|
|
||||||
onPhoneNumberChanged: PropTypes.func,
|
|
||||||
onPasswordChanged: PropTypes.func,
|
|
||||||
loginIncorrect: PropTypes.bool,
|
|
||||||
disableSubmit: PropTypes.bool,
|
|
||||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
|
||||||
busy: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
onError: function() {},
|
|
||||||
onEditServerDetailsClick: null,
|
|
||||||
onUsernameChanged: function() {},
|
|
||||||
onUsernameBlur: function() {},
|
|
||||||
onPasswordChanged: function() {},
|
|
||||||
onPhoneCountryChanged: function() {},
|
|
||||||
onPhoneNumberChanged: function() {},
|
|
||||||
onPhoneNumberBlur: function() {},
|
|
||||||
initialUsername: "",
|
|
||||||
initialPhoneCountry: "",
|
|
||||||
initialPhoneNumber: "",
|
|
||||||
initialPassword: "",
|
|
||||||
loginIncorrect: false,
|
|
||||||
disableSubmit: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
static LOGIN_FIELD_EMAIL = "login_field_email";
|
|
||||||
static LOGIN_FIELD_MXID = "login_field_mxid";
|
|
||||||
static LOGIN_FIELD_PHONE = "login_field_phone";
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
username: this.props.initialUsername,
|
|
||||||
password: this.props.initialPassword,
|
|
||||||
phoneCountry: this.props.initialPhoneCountry,
|
|
||||||
phoneNumber: this.props.initialPhoneNumber,
|
|
||||||
loginType: PasswordLogin.LOGIN_FIELD_MXID,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.onForgotPasswordClick = this.onForgotPasswordClick.bind(this);
|
|
||||||
this.onSubmitForm = this.onSubmitForm.bind(this);
|
|
||||||
this.onUsernameChanged = this.onUsernameChanged.bind(this);
|
|
||||||
this.onUsernameBlur = this.onUsernameBlur.bind(this);
|
|
||||||
this.onLoginTypeChange = this.onLoginTypeChange.bind(this);
|
|
||||||
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
|
|
||||||
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
|
|
||||||
this.onPhoneNumberBlur = this.onPhoneNumberBlur.bind(this);
|
|
||||||
this.onPasswordChanged = this.onPasswordChanged.bind(this);
|
|
||||||
this.isLoginEmpty = this.isLoginEmpty.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
onForgotPasswordClick(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
this.props.onForgotPasswordClick();
|
|
||||||
}
|
|
||||||
|
|
||||||
onSubmitForm(ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
|
|
||||||
let username = ''; // XXX: Synapse breaks if you send null here:
|
|
||||||
let phoneCountry = null;
|
|
||||||
let phoneNumber = null;
|
|
||||||
let error;
|
|
||||||
|
|
||||||
switch (this.state.loginType) {
|
|
||||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
|
||||||
username = this.state.username;
|
|
||||||
if (!username) {
|
|
||||||
error = _t('The email field must not be blank.');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
|
||||||
username = this.state.username;
|
|
||||||
if (!username) {
|
|
||||||
error = _t('The username field must not be blank.');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
|
||||||
phoneCountry = this.state.phoneCountry;
|
|
||||||
phoneNumber = this.state.phoneNumber;
|
|
||||||
if (!phoneNumber) {
|
|
||||||
error = _t('The phone number field must not be blank.');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
this.props.onError(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.state.password) {
|
|
||||||
this.props.onError(_t('The password field must not be blank.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onSubmit(
|
|
||||||
username,
|
|
||||||
phoneCountry,
|
|
||||||
phoneNumber,
|
|
||||||
this.state.password,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
onUsernameChanged(ev) {
|
|
||||||
this.setState({username: ev.target.value});
|
|
||||||
this.props.onUsernameChanged(ev.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
onUsernameFocus() {
|
|
||||||
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_MXID) {
|
|
||||||
CountlyAnalytics.instance.track("onboarding_login_mxid_focus");
|
|
||||||
} else {
|
|
||||||
CountlyAnalytics.instance.track("onboarding_login_email_focus");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onUsernameBlur(ev) {
|
|
||||||
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_MXID) {
|
|
||||||
CountlyAnalytics.instance.track("onboarding_login_mxid_blur");
|
|
||||||
} else {
|
|
||||||
CountlyAnalytics.instance.track("onboarding_login_email_blur");
|
|
||||||
}
|
|
||||||
this.props.onUsernameBlur(ev.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoginTypeChange(ev) {
|
|
||||||
const loginType = ev.target.value;
|
|
||||||
this.props.onError(null); // send a null error to clear any error messages
|
|
||||||
this.setState({
|
|
||||||
loginType: loginType,
|
|
||||||
username: "", // Reset because email and username use the same state
|
|
||||||
});
|
|
||||||
CountlyAnalytics.instance.track("onboarding_login_type_changed", { loginType });
|
|
||||||
}
|
|
||||||
|
|
||||||
onPhoneCountryChanged(country) {
|
|
||||||
this.setState({
|
|
||||||
phoneCountry: country.iso2,
|
|
||||||
phonePrefix: country.prefix,
|
|
||||||
});
|
|
||||||
this.props.onPhoneCountryChanged(country.iso2);
|
|
||||||
}
|
|
||||||
|
|
||||||
onPhoneNumberChanged(ev) {
|
|
||||||
this.setState({phoneNumber: ev.target.value});
|
|
||||||
this.props.onPhoneNumberChanged(ev.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
onPhoneNumberFocus() {
|
|
||||||
CountlyAnalytics.instance.track("onboarding_login_phone_number_focus");
|
|
||||||
}
|
|
||||||
|
|
||||||
onPhoneNumberBlur(ev) {
|
|
||||||
this.props.onPhoneNumberBlur(ev.target.value);
|
|
||||||
CountlyAnalytics.instance.track("onboarding_login_phone_number_blur");
|
|
||||||
}
|
|
||||||
|
|
||||||
onPasswordChanged(ev) {
|
|
||||||
this.setState({password: ev.target.value});
|
|
||||||
this.props.onPasswordChanged(ev.target.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderLoginField(loginType, autoFocus) {
|
|
||||||
const Field = sdk.getComponent('elements.Field');
|
|
||||||
|
|
||||||
const classes = {};
|
|
||||||
|
|
||||||
switch (loginType) {
|
|
||||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
|
||||||
classes.error = this.props.loginIncorrect && !this.state.username;
|
|
||||||
return <Field
|
|
||||||
className={classNames(classes)}
|
|
||||||
name="username" // make it a little easier for browser's remember-password
|
|
||||||
key="email_input"
|
|
||||||
type="text"
|
|
||||||
label={_t("Email")}
|
|
||||||
placeholder="joe@example.com"
|
|
||||||
value={this.state.username}
|
|
||||||
onChange={this.onUsernameChanged}
|
|
||||||
onFocus={this.onUsernameFocus}
|
|
||||||
onBlur={this.onUsernameBlur}
|
|
||||||
disabled={this.props.disableSubmit}
|
|
||||||
autoFocus={autoFocus}
|
|
||||||
/>;
|
|
||||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
|
||||||
classes.error = this.props.loginIncorrect && !this.state.username;
|
|
||||||
return <Field
|
|
||||||
className={classNames(classes)}
|
|
||||||
name="username" // make it a little easier for browser's remember-password
|
|
||||||
key="username_input"
|
|
||||||
type="text"
|
|
||||||
label={_t("Username")}
|
|
||||||
value={this.state.username}
|
|
||||||
onChange={this.onUsernameChanged}
|
|
||||||
onFocus={this.onUsernameFocus}
|
|
||||||
onBlur={this.onUsernameBlur}
|
|
||||||
disabled={this.props.disableSubmit}
|
|
||||||
autoFocus={autoFocus}
|
|
||||||
/>;
|
|
||||||
case PasswordLogin.LOGIN_FIELD_PHONE: {
|
|
||||||
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
|
|
||||||
classes.error = this.props.loginIncorrect && !this.state.phoneNumber;
|
|
||||||
|
|
||||||
const phoneCountry = <CountryDropdown
|
|
||||||
value={this.state.phoneCountry}
|
|
||||||
isSmall={true}
|
|
||||||
showPrefix={true}
|
|
||||||
onOptionChange={this.onPhoneCountryChanged}
|
|
||||||
/>;
|
|
||||||
|
|
||||||
return <Field
|
|
||||||
className={classNames(classes)}
|
|
||||||
name="phoneNumber"
|
|
||||||
key="phone_input"
|
|
||||||
type="text"
|
|
||||||
label={_t("Phone")}
|
|
||||||
value={this.state.phoneNumber}
|
|
||||||
prefixComponent={phoneCountry}
|
|
||||||
onChange={this.onPhoneNumberChanged}
|
|
||||||
onFocus={this.onPhoneNumberFocus}
|
|
||||||
onBlur={this.onPhoneNumberBlur}
|
|
||||||
disabled={this.props.disableSubmit}
|
|
||||||
autoFocus={autoFocus}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoginEmpty() {
|
|
||||||
switch (this.state.loginType) {
|
|
||||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
|
||||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
|
||||||
return !this.state.username;
|
|
||||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
|
||||||
return !this.state.phoneCountry || !this.state.phoneNumber;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const Field = sdk.getComponent('elements.Field');
|
|
||||||
const SignInToText = sdk.getComponent('views.auth.SignInToText');
|
|
||||||
|
|
||||||
let forgotPasswordJsx;
|
|
||||||
|
|
||||||
if (this.props.onForgotPasswordClick) {
|
|
||||||
forgotPasswordJsx = <span>
|
|
||||||
{_t('Not sure of your password? <a>Set a new one</a>', {}, {
|
|
||||||
a: sub => (
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_Login_forgot"
|
|
||||||
disabled={this.props.busy}
|
|
||||||
kind="link"
|
|
||||||
onClick={this.onForgotPasswordClick}
|
|
||||||
>
|
|
||||||
{sub}
|
|
||||||
</AccessibleButton>
|
|
||||||
),
|
|
||||||
})}
|
|
||||||
</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pwFieldClass = classNames({
|
|
||||||
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
|
|
||||||
});
|
|
||||||
|
|
||||||
// If login is empty, autoFocus login, otherwise autoFocus password.
|
|
||||||
// this is for when auto server discovery remounts us when the user tries to tab from username to password
|
|
||||||
const autoFocusPassword = !this.isLoginEmpty();
|
|
||||||
const loginField = this.renderLoginField(this.state.loginType, !autoFocusPassword);
|
|
||||||
|
|
||||||
let loginType;
|
|
||||||
if (!SdkConfig.get().disable_3pid_login) {
|
|
||||||
loginType = (
|
|
||||||
<div className="mx_Login_type_container">
|
|
||||||
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
|
|
||||||
<Field
|
|
||||||
element="select"
|
|
||||||
value={this.state.loginType}
|
|
||||||
onChange={this.onLoginTypeChange}
|
|
||||||
disabled={this.props.disableSubmit}
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
key={PasswordLogin.LOGIN_FIELD_MXID}
|
|
||||||
value={PasswordLogin.LOGIN_FIELD_MXID}
|
|
||||||
>
|
|
||||||
{_t('Username')}
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
key={PasswordLogin.LOGIN_FIELD_EMAIL}
|
|
||||||
value={PasswordLogin.LOGIN_FIELD_EMAIL}
|
|
||||||
>
|
|
||||||
{_t('Email address')}
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
key={PasswordLogin.LOGIN_FIELD_PHONE}
|
|
||||||
value={PasswordLogin.LOGIN_FIELD_PHONE}
|
|
||||||
>
|
|
||||||
{_t('Phone')}
|
|
||||||
</option>
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<SignInToText serverConfig={this.props.serverConfig}
|
|
||||||
onEditServerDetailsClick={this.props.onEditServerDetailsClick} />
|
|
||||||
<form onSubmit={this.onSubmitForm}>
|
|
||||||
{loginType}
|
|
||||||
{loginField}
|
|
||||||
<Field
|
|
||||||
className={pwFieldClass}
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
label={_t('Password')}
|
|
||||||
value={this.state.password}
|
|
||||||
onChange={this.onPasswordChanged}
|
|
||||||
disabled={this.props.disableSubmit}
|
|
||||||
autoFocus={autoFocusPassword}
|
|
||||||
/>
|
|
||||||
{forgotPasswordJsx}
|
|
||||||
{ !this.props.busy && <input className="mx_Login_submit"
|
|
||||||
type="submit"
|
|
||||||
value={_t('Sign in')}
|
|
||||||
disabled={this.props.disableSubmit}
|
|
||||||
/> }
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
495
src/components/views/auth/PasswordLogin.tsx
Normal file
495
src/components/views/auth/PasswordLogin.tsx
Normal file
|
@ -0,0 +1,495 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016, 2017, 2019 New Vector Ltd.
|
||||||
|
|
||||||
|
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 React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
|
import withValidation from "../elements/Validation";
|
||||||
|
import * as Email from "../../../email";
|
||||||
|
import Field from "../elements/Field";
|
||||||
|
import CountryDropdown from "./CountryDropdown";
|
||||||
|
import SignInToText from "./SignInToText";
|
||||||
|
|
||||||
|
// For validating phone numbers without country codes
|
||||||
|
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
username: string; // also used for email address
|
||||||
|
phoneCountry: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
|
||||||
|
serverConfig: ValidatedServerConfig;
|
||||||
|
loginIncorrect?: boolean;
|
||||||
|
disableSubmit?: boolean;
|
||||||
|
busy?: boolean;
|
||||||
|
|
||||||
|
onSubmit(username: string, phoneCountry: void, phoneNumber: void, password: string): void;
|
||||||
|
onSubmit(username: void, phoneCountry: string, phoneNumber: string, password: string): void;
|
||||||
|
onUsernameChanged?(username: string): void;
|
||||||
|
onUsernameBlur?(username: string): void;
|
||||||
|
onPhoneCountryChanged?(phoneCountry: string): void;
|
||||||
|
onPhoneNumberChanged?(phoneNumber: string): void;
|
||||||
|
onEditServerDetailsClick?(): void;
|
||||||
|
onForgotPasswordClick?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
fieldValid: Partial<Record<LoginField, boolean>>;
|
||||||
|
loginType: LoginField.Email | LoginField.MatrixId | LoginField.Phone,
|
||||||
|
password: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LoginField {
|
||||||
|
Email = "login_field_email",
|
||||||
|
MatrixId = "login_field_mxid",
|
||||||
|
Phone = "login_field_phone",
|
||||||
|
Password = "login_field_phone",
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A pure UI component which displays a username/password form.
|
||||||
|
* The email/username/phone fields are fully-controlled, the password field is not.
|
||||||
|
*/
|
||||||
|
export default class PasswordLogin extends React.PureComponent<IProps, IState> {
|
||||||
|
static defaultProps = {
|
||||||
|
onEditServerDetailsClick: null,
|
||||||
|
onUsernameChanged: function() {},
|
||||||
|
onUsernameBlur: function() {},
|
||||||
|
onPhoneCountryChanged: function() {},
|
||||||
|
onPhoneNumberChanged: function() {},
|
||||||
|
loginIncorrect: false,
|
||||||
|
disableSubmit: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
// Field error codes by field ID
|
||||||
|
fieldValid: {},
|
||||||
|
loginType: LoginField.MatrixId,
|
||||||
|
password: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private onForgotPasswordClick = ev => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
this.props.onForgotPasswordClick();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onSubmitForm = async ev => {
|
||||||
|
ev.preventDefault();
|
||||||
|
|
||||||
|
const allFieldsValid = await this.verifyFieldsBeforeSubmit();
|
||||||
|
if (!allFieldsValid) {
|
||||||
|
CountlyAnalytics.instance.track("onboarding_registration_submit_failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let username = ''; // XXX: Synapse breaks if you send null here:
|
||||||
|
let phoneCountry = null;
|
||||||
|
let phoneNumber = null;
|
||||||
|
|
||||||
|
switch (this.state.loginType) {
|
||||||
|
case LoginField.Email:
|
||||||
|
case LoginField.MatrixId:
|
||||||
|
username = this.props.username;
|
||||||
|
break;
|
||||||
|
case LoginField.Phone:
|
||||||
|
phoneCountry = this.props.phoneCountry;
|
||||||
|
phoneNumber = this.props.phoneNumber;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onSubmit(username, phoneCountry, phoneNumber, this.state.password);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onUsernameChanged = ev => {
|
||||||
|
this.props.onUsernameChanged(ev.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onUsernameFocus = () => {
|
||||||
|
if (this.state.loginType === LoginField.MatrixId) {
|
||||||
|
CountlyAnalytics.instance.track("onboarding_login_mxid_focus");
|
||||||
|
} else {
|
||||||
|
CountlyAnalytics.instance.track("onboarding_login_email_focus");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private onUsernameBlur = ev => {
|
||||||
|
if (this.state.loginType === LoginField.MatrixId) {
|
||||||
|
CountlyAnalytics.instance.track("onboarding_login_mxid_blur");
|
||||||
|
} else {
|
||||||
|
CountlyAnalytics.instance.track("onboarding_login_email_blur");
|
||||||
|
}
|
||||||
|
this.props.onUsernameBlur(ev.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onLoginTypeChange = ev => {
|
||||||
|
const loginType = ev.target.value;
|
||||||
|
this.setState({ loginType });
|
||||||
|
this.props.onUsernameChanged(""); // Reset because email and username use the same state
|
||||||
|
CountlyAnalytics.instance.track("onboarding_login_type_changed", { loginType });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onPhoneCountryChanged = country => {
|
||||||
|
this.props.onPhoneCountryChanged(country.iso2);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onPhoneNumberChanged = ev => {
|
||||||
|
this.props.onPhoneNumberChanged(ev.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onPhoneNumberFocus = () => {
|
||||||
|
CountlyAnalytics.instance.track("onboarding_login_phone_number_focus");
|
||||||
|
};
|
||||||
|
|
||||||
|
private onPhoneNumberBlur = ev => {
|
||||||
|
CountlyAnalytics.instance.track("onboarding_login_phone_number_blur");
|
||||||
|
};
|
||||||
|
|
||||||
|
private onPasswordChanged = ev => {
|
||||||
|
this.setState({password: ev.target.value});
|
||||||
|
};
|
||||||
|
|
||||||
|
private async verifyFieldsBeforeSubmit() {
|
||||||
|
// Blur the active element if any, so we first run its blur validation,
|
||||||
|
// which is less strict than the pass we're about to do below for all fields.
|
||||||
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
|
if (activeElement) {
|
||||||
|
activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldIDsInDisplayOrder = [
|
||||||
|
this.state.loginType,
|
||||||
|
LoginField.Password,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Run all fields with stricter validation that no longer allows empty
|
||||||
|
// values for required fields.
|
||||||
|
for (const fieldID of fieldIDsInDisplayOrder) {
|
||||||
|
const field = this[fieldID];
|
||||||
|
if (!field) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// We must wait for these validations to finish before queueing
|
||||||
|
// up the setState below so our setState goes in the queue after
|
||||||
|
// all the setStates from these validate calls (that's how we
|
||||||
|
// know they've finished).
|
||||||
|
await field.validate({ allowEmpty: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation and state updates are async, so we need to wait for them to complete
|
||||||
|
// first. Queue a `setState` callback and wait for it to resolve.
|
||||||
|
await new Promise(resolve => this.setState({}, resolve));
|
||||||
|
|
||||||
|
if (this.allFieldsValid()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidField = this.findFirstInvalidField(fieldIDsInDisplayOrder);
|
||||||
|
|
||||||
|
if (!invalidField) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus the first invalid field and show feedback in the stricter mode
|
||||||
|
// that no longer allows empty values for required fields.
|
||||||
|
invalidField.focus();
|
||||||
|
invalidField.validate({ allowEmpty: false, focused: true });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private allFieldsValid() {
|
||||||
|
const keys = Object.keys(this.state.fieldValid);
|
||||||
|
for (let i = 0; i < keys.length; ++i) {
|
||||||
|
if (!this.state.fieldValid[keys[i]]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private findFirstInvalidField(fieldIDs: LoginField[]) {
|
||||||
|
for (const fieldID of fieldIDs) {
|
||||||
|
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
||||||
|
return this[fieldID];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private markFieldValid(fieldID: LoginField, valid: boolean) {
|
||||||
|
const { fieldValid } = this.state;
|
||||||
|
fieldValid[fieldID] = valid;
|
||||||
|
this.setState({
|
||||||
|
fieldValid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateUsernameRules = withValidation({
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
key: "required",
|
||||||
|
test({ value, allowEmpty }) {
|
||||||
|
return allowEmpty || !!value;
|
||||||
|
},
|
||||||
|
invalid: () => _t("Enter username"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
private onUsernameValidate = async (fieldState) => {
|
||||||
|
const result = await this.validateUsernameRules(fieldState);
|
||||||
|
this.markFieldValid(LoginField.MatrixId, result.valid);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
private validateEmailRules = withValidation({
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
key: "required",
|
||||||
|
test({ value, allowEmpty }) {
|
||||||
|
return allowEmpty || !!value;
|
||||||
|
},
|
||||||
|
invalid: () => _t("Enter email address"),
|
||||||
|
}, {
|
||||||
|
key: "email",
|
||||||
|
test: ({ value }) => !value || Email.looksValid(value),
|
||||||
|
invalid: () => _t("Doesn't look like a valid email address"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
private onEmailValidate = async (fieldState) => {
|
||||||
|
const result = await this.validateEmailRules(fieldState);
|
||||||
|
this.markFieldValid(LoginField.Email, result.valid);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
private validatePhoneNumberRules = withValidation({
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
key: "required",
|
||||||
|
test({ value, allowEmpty }) {
|
||||||
|
return allowEmpty || !!value;
|
||||||
|
},
|
||||||
|
invalid: () => _t("Enter phone number"),
|
||||||
|
}, {
|
||||||
|
key: "number",
|
||||||
|
test: ({ value }) => !value || PHONE_NUMBER_REGEX.test(value),
|
||||||
|
invalid: () => _t("Doesn't look like a valid phone number"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
private onPhoneNumberValidate = async (fieldState) => {
|
||||||
|
const result = await this.validatePhoneNumberRules(fieldState);
|
||||||
|
this.markFieldValid(LoginField.Password, result.valid);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
private validatePasswordRules = withValidation({
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
key: "required",
|
||||||
|
test({ value, allowEmpty }) {
|
||||||
|
return allowEmpty || !!value;
|
||||||
|
},
|
||||||
|
invalid: () => _t("Enter password"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
private onPasswordValidate = async (fieldState) => {
|
||||||
|
const result = await this.validatePasswordRules(fieldState);
|
||||||
|
this.markFieldValid(LoginField.Password, result.valid);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderLoginField(loginType: IState["loginType"], autoFocus: boolean) {
|
||||||
|
const classes = {
|
||||||
|
error: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (loginType) {
|
||||||
|
case LoginField.Email:
|
||||||
|
classes.error = this.props.loginIncorrect && !this.props.username;
|
||||||
|
return <Field
|
||||||
|
className={classNames(classes)}
|
||||||
|
name="username" // make it a little easier for browser's remember-password
|
||||||
|
key="email_input"
|
||||||
|
type="text"
|
||||||
|
label={_t("Email")}
|
||||||
|
placeholder="joe@example.com"
|
||||||
|
value={this.props.username}
|
||||||
|
onChange={this.onUsernameChanged}
|
||||||
|
onFocus={this.onUsernameFocus}
|
||||||
|
onBlur={this.onUsernameBlur}
|
||||||
|
disabled={this.props.disableSubmit}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
onValidate={this.onEmailValidate}
|
||||||
|
ref={field => this[LoginField.Email] = field}
|
||||||
|
/>;
|
||||||
|
case LoginField.MatrixId:
|
||||||
|
classes.error = this.props.loginIncorrect && !this.props.username;
|
||||||
|
return <Field
|
||||||
|
className={classNames(classes)}
|
||||||
|
name="username" // make it a little easier for browser's remember-password
|
||||||
|
key="username_input"
|
||||||
|
type="text"
|
||||||
|
label={_t("Username")}
|
||||||
|
value={this.props.username}
|
||||||
|
onChange={this.onUsernameChanged}
|
||||||
|
onFocus={this.onUsernameFocus}
|
||||||
|
onBlur={this.onUsernameBlur}
|
||||||
|
disabled={this.props.disableSubmit}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
onValidate={this.onUsernameValidate}
|
||||||
|
ref={field => this[LoginField.MatrixId] = field}
|
||||||
|
/>;
|
||||||
|
case LoginField.Phone: {
|
||||||
|
classes.error = this.props.loginIncorrect && !this.props.phoneNumber;
|
||||||
|
|
||||||
|
const phoneCountry = <CountryDropdown
|
||||||
|
value={this.props.phoneCountry}
|
||||||
|
isSmall={true}
|
||||||
|
showPrefix={true}
|
||||||
|
onOptionChange={this.onPhoneCountryChanged}
|
||||||
|
/>;
|
||||||
|
|
||||||
|
return <Field
|
||||||
|
className={classNames(classes)}
|
||||||
|
name="phoneNumber"
|
||||||
|
key="phone_input"
|
||||||
|
type="text"
|
||||||
|
label={_t("Phone")}
|
||||||
|
value={this.props.phoneNumber}
|
||||||
|
prefixComponent={phoneCountry}
|
||||||
|
onChange={this.onPhoneNumberChanged}
|
||||||
|
onFocus={this.onPhoneNumberFocus}
|
||||||
|
onBlur={this.onPhoneNumberBlur}
|
||||||
|
disabled={this.props.disableSubmit}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
onValidate={this.onPhoneNumberValidate}
|
||||||
|
ref={field => this[LoginField.Password] = field}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isLoginEmpty() {
|
||||||
|
switch (this.state.loginType) {
|
||||||
|
case LoginField.Email:
|
||||||
|
case LoginField.MatrixId:
|
||||||
|
return !this.props.username;
|
||||||
|
case LoginField.Phone:
|
||||||
|
return !this.props.phoneCountry || !this.props.phoneNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let forgotPasswordJsx;
|
||||||
|
|
||||||
|
if (this.props.onForgotPasswordClick) {
|
||||||
|
forgotPasswordJsx = <span>
|
||||||
|
{_t('Not sure of your password? <a>Set a new one</a>', {}, {
|
||||||
|
a: sub => (
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_Login_forgot"
|
||||||
|
disabled={this.props.busy}
|
||||||
|
kind="link"
|
||||||
|
onClick={this.onForgotPasswordClick}
|
||||||
|
>
|
||||||
|
{sub}
|
||||||
|
</AccessibleButton>
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pwFieldClass = classNames({
|
||||||
|
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
|
||||||
|
});
|
||||||
|
|
||||||
|
// If login is empty, autoFocus login, otherwise autoFocus password.
|
||||||
|
// this is for when auto server discovery remounts us when the user tries to tab from username to password
|
||||||
|
const autoFocusPassword = !this.isLoginEmpty();
|
||||||
|
const loginField = this.renderLoginField(this.state.loginType, !autoFocusPassword);
|
||||||
|
|
||||||
|
let loginType;
|
||||||
|
if (!SdkConfig.get().disable_3pid_login) {
|
||||||
|
loginType = (
|
||||||
|
<div className="mx_Login_type_container">
|
||||||
|
<label className="mx_Login_type_label">{ _t('Sign in with') }</label>
|
||||||
|
<Field
|
||||||
|
element="select"
|
||||||
|
value={this.state.loginType}
|
||||||
|
onChange={this.onLoginTypeChange}
|
||||||
|
disabled={this.props.disableSubmit}
|
||||||
|
>
|
||||||
|
<option key={LoginField.MatrixId} value={LoginField.MatrixId}>
|
||||||
|
{_t('Username')}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
key={LoginField.Email}
|
||||||
|
value={LoginField.Email}
|
||||||
|
>
|
||||||
|
{_t('Email address')}
|
||||||
|
</option>
|
||||||
|
<option key={LoginField.Password} value={LoginField.Password}>
|
||||||
|
{_t('Phone')}
|
||||||
|
</option>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SignInToText serverConfig={this.props.serverConfig}
|
||||||
|
onEditServerDetailsClick={this.props.onEditServerDetailsClick} />
|
||||||
|
<form onSubmit={this.onSubmitForm}>
|
||||||
|
{loginType}
|
||||||
|
{loginField}
|
||||||
|
<Field
|
||||||
|
className={pwFieldClass}
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
label={_t('Password')}
|
||||||
|
value={this.state.password}
|
||||||
|
onChange={this.onPasswordChanged}
|
||||||
|
disabled={this.props.disableSubmit}
|
||||||
|
autoFocus={autoFocusPassword}
|
||||||
|
onValidate={this.onPasswordValidate}
|
||||||
|
ref={field => this[LoginField.Password] = field}
|
||||||
|
/>
|
||||||
|
{forgotPasswordJsx}
|
||||||
|
{ !this.props.busy && <input className="mx_Login_submit"
|
||||||
|
type="submit"
|
||||||
|
value={_t('Sign in')}
|
||||||
|
disabled={this.props.disableSubmit}
|
||||||
|
/> }
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
|
||||||
Copyright 2017 Vector Creations Ltd
|
|
||||||
Copyright 2018, 2019 New Vector Ltd
|
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
Copyright 2015, 2016, 2017, 2018, 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.
|
||||||
|
@ -18,7 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import * as Email from '../../../email';
|
import * as Email from '../../../email';
|
||||||
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
|
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
|
||||||
|
@ -31,32 +29,57 @@ import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||||
import PassphraseField from "./PassphraseField";
|
import PassphraseField from "./PassphraseField";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
|
|
||||||
const FIELD_EMAIL = 'field_email';
|
enum RegistrationField {
|
||||||
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
Email = "field_email",
|
||||||
const FIELD_USERNAME = 'field_username';
|
PhoneNumber = "field_phone_number",
|
||||||
const FIELD_PASSWORD = 'field_password';
|
Username = "field_username",
|
||||||
const FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
|
Password = "field_password",
|
||||||
|
PasswordConfirm = "field_password_confirm",
|
||||||
|
}
|
||||||
|
|
||||||
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
|
const PASSWORD_MIN_SCORE = 3; // safely unguessable: moderate protection from offline slow-hash scenario.
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// Values pre-filled in the input boxes when the component loads
|
||||||
|
defaultEmail?: string;
|
||||||
|
defaultPhoneCountry?: string;
|
||||||
|
defaultPhoneNumber?: string;
|
||||||
|
defaultUsername?: string;
|
||||||
|
defaultPassword?: string;
|
||||||
|
flows: {
|
||||||
|
stages: string[];
|
||||||
|
}[];
|
||||||
|
serverConfig: ValidatedServerConfig;
|
||||||
|
canSubmit?: boolean;
|
||||||
|
serverRequiresIdServer?: boolean;
|
||||||
|
|
||||||
|
onRegisterClick(params: {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
email?: string;
|
||||||
|
phoneCountry?: string;
|
||||||
|
phoneNumber?: string;
|
||||||
|
}): Promise<void>;
|
||||||
|
onEditServerDetailsClick?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
// Field error codes by field ID
|
||||||
|
fieldValid: Partial<Record<RegistrationField, boolean>>;
|
||||||
|
// The ISO2 country code selected in the phone number entry
|
||||||
|
phoneCountry: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
password: string;
|
||||||
|
passwordConfirm: string;
|
||||||
|
passwordComplexity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A pure UI component which displays a registration form.
|
* A pure UI component which displays a registration form.
|
||||||
*/
|
*/
|
||||||
export default class RegistrationForm extends React.Component {
|
export default class RegistrationForm extends React.PureComponent<IProps, IState> {
|
||||||
static propTypes = {
|
|
||||||
// Values pre-filled in the input boxes when the component loads
|
|
||||||
defaultEmail: PropTypes.string,
|
|
||||||
defaultPhoneCountry: PropTypes.string,
|
|
||||||
defaultPhoneNumber: PropTypes.string,
|
|
||||||
defaultUsername: PropTypes.string,
|
|
||||||
defaultPassword: PropTypes.string,
|
|
||||||
onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise
|
|
||||||
flows: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
|
|
||||||
canSubmit: PropTypes.bool,
|
|
||||||
serverRequiresIdServer: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
onValidationChange: console.error,
|
onValidationChange: console.error,
|
||||||
canSubmit: true,
|
canSubmit: true,
|
||||||
|
@ -66,9 +89,7 @@ export default class RegistrationForm extends React.Component {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
// Field error codes by field ID
|
|
||||||
fieldValid: {},
|
fieldValid: {},
|
||||||
// The ISO2 country code selected in the phone number entry
|
|
||||||
phoneCountry: this.props.defaultPhoneCountry,
|
phoneCountry: this.props.defaultPhoneCountry,
|
||||||
username: this.props.defaultUsername || "",
|
username: this.props.defaultUsername || "",
|
||||||
email: this.props.defaultEmail || "",
|
email: this.props.defaultEmail || "",
|
||||||
|
@ -81,7 +102,7 @@ export default class RegistrationForm extends React.Component {
|
||||||
CountlyAnalytics.instance.track("onboarding_registration_begin");
|
CountlyAnalytics.instance.track("onboarding_registration_begin");
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit = async ev => {
|
private onSubmit = async ev => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
if (!this.props.canSubmit) return;
|
if (!this.props.canSubmit) return;
|
||||||
|
@ -92,7 +113,6 @@ export default class RegistrationForm extends React.Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const self = this;
|
|
||||||
if (this.state.email === '') {
|
if (this.state.email === '') {
|
||||||
const haveIs = Boolean(this.props.serverConfig.isUrl);
|
const haveIs = Boolean(this.props.serverConfig.isUrl);
|
||||||
|
|
||||||
|
@ -102,14 +122,14 @@ export default class RegistrationForm extends React.Component {
|
||||||
"No identity server is configured so you cannot add an email address in order to " +
|
"No identity server is configured so you cannot add an email address in order to " +
|
||||||
"reset your password in the future.",
|
"reset your password in the future.",
|
||||||
);
|
);
|
||||||
} else if (this._showEmail()) {
|
} else if (this.showEmail()) {
|
||||||
desc = _t(
|
desc = _t(
|
||||||
"If you don't specify an email address, you won't be able to reset your password. " +
|
"If you don't specify an email address, you won't be able to reset your password. " +
|
||||||
"Are you sure?",
|
"Are you sure?",
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// user can't set an e-mail so don't prompt them to
|
// user can't set an e-mail so don't prompt them to
|
||||||
self._doSubmit(ev);
|
this.doSubmit(ev);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,18 +140,18 @@ export default class RegistrationForm extends React.Component {
|
||||||
title: _t("Warning!"),
|
title: _t("Warning!"),
|
||||||
description: desc,
|
description: desc,
|
||||||
button: _t("Continue"),
|
button: _t("Continue"),
|
||||||
onFinished(confirmed) {
|
onFinished: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
self._doSubmit(ev);
|
this.doSubmit(ev);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
self._doSubmit(ev);
|
this.doSubmit(ev);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_doSubmit(ev) {
|
private doSubmit(ev) {
|
||||||
const email = this.state.email.trim();
|
const email = this.state.email.trim();
|
||||||
|
|
||||||
CountlyAnalytics.instance.track("onboarding_registration_submit_ok", {
|
CountlyAnalytics.instance.track("onboarding_registration_submit_ok", {
|
||||||
|
@ -154,20 +174,20 @@ export default class RegistrationForm extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyFieldsBeforeSubmit() {
|
private async verifyFieldsBeforeSubmit() {
|
||||||
// Blur the active element if any, so we first run its blur validation,
|
// Blur the active element if any, so we first run its blur validation,
|
||||||
// which is less strict than the pass we're about to do below for all fields.
|
// which is less strict than the pass we're about to do below for all fields.
|
||||||
const activeElement = document.activeElement;
|
const activeElement = document.activeElement as HTMLElement;
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
activeElement.blur();
|
activeElement.blur();
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldIDsInDisplayOrder = [
|
const fieldIDsInDisplayOrder = [
|
||||||
FIELD_USERNAME,
|
RegistrationField.Username,
|
||||||
FIELD_PASSWORD,
|
RegistrationField.Password,
|
||||||
FIELD_PASSWORD_CONFIRM,
|
RegistrationField.PasswordConfirm,
|
||||||
FIELD_EMAIL,
|
RegistrationField.Email,
|
||||||
FIELD_PHONE_NUMBER,
|
RegistrationField.PhoneNumber,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Run all fields with stricter validation that no longer allows empty
|
// Run all fields with stricter validation that no longer allows empty
|
||||||
|
@ -208,7 +228,7 @@ export default class RegistrationForm extends React.Component {
|
||||||
/**
|
/**
|
||||||
* @returns {boolean} true if all fields were valid last time they were validated.
|
* @returns {boolean} true if all fields were valid last time they were validated.
|
||||||
*/
|
*/
|
||||||
allFieldsValid() {
|
private allFieldsValid() {
|
||||||
const keys = Object.keys(this.state.fieldValid);
|
const keys = Object.keys(this.state.fieldValid);
|
||||||
for (let i = 0; i < keys.length; ++i) {
|
for (let i = 0; i < keys.length; ++i) {
|
||||||
if (!this.state.fieldValid[keys[i]]) {
|
if (!this.state.fieldValid[keys[i]]) {
|
||||||
|
@ -218,7 +238,7 @@ export default class RegistrationForm extends React.Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
findFirstInvalidField(fieldIDs) {
|
private findFirstInvalidField(fieldIDs: RegistrationField[]) {
|
||||||
for (const fieldID of fieldIDs) {
|
for (const fieldID of fieldIDs) {
|
||||||
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
if (!this.state.fieldValid[fieldID] && this[fieldID]) {
|
||||||
return this[fieldID];
|
return this[fieldID];
|
||||||
|
@ -227,7 +247,7 @@ export default class RegistrationForm extends React.Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
markFieldValid(fieldID, valid) {
|
private markFieldValid(fieldID: RegistrationField, valid: boolean) {
|
||||||
const { fieldValid } = this.state;
|
const { fieldValid } = this.state;
|
||||||
fieldValid[fieldID] = valid;
|
fieldValid[fieldID] = valid;
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -235,26 +255,26 @@ export default class RegistrationForm extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onEmailChange = ev => {
|
private onEmailChange = ev => {
|
||||||
this.setState({
|
this.setState({
|
||||||
email: ev.target.value,
|
email: ev.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onEmailValidate = async fieldState => {
|
private onEmailValidate = async fieldState => {
|
||||||
const result = await this.validateEmailRules(fieldState);
|
const result = await this.validateEmailRules(fieldState);
|
||||||
this.markFieldValid(FIELD_EMAIL, result.valid);
|
this.markFieldValid(RegistrationField.Email, result.valid);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
validateEmailRules = withValidation({
|
private validateEmailRules = withValidation({
|
||||||
description: () => _t("Use an email address to recover your account"),
|
description: () => _t("Use an email address to recover your account"),
|
||||||
hideDescriptionIfValid: true,
|
hideDescriptionIfValid: true,
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
key: "required",
|
key: "required",
|
||||||
test({ value, allowEmpty }) {
|
test(this: RegistrationForm, { value, allowEmpty }) {
|
||||||
return allowEmpty || !this._authStepIsRequired('m.login.email.identity') || !!value;
|
return allowEmpty || !this.authStepIsRequired('m.login.email.identity') || !!value;
|
||||||
},
|
},
|
||||||
invalid: () => _t("Enter email address (required on this homeserver)"),
|
invalid: () => _t("Enter email address (required on this homeserver)"),
|
||||||
},
|
},
|
||||||
|
@ -266,29 +286,29 @@ export default class RegistrationForm extends React.Component {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
onPasswordChange = ev => {
|
private onPasswordChange = ev => {
|
||||||
this.setState({
|
this.setState({
|
||||||
password: ev.target.value,
|
password: ev.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onPasswordValidate = result => {
|
private onPasswordValidate = result => {
|
||||||
this.markFieldValid(FIELD_PASSWORD, result.valid);
|
this.markFieldValid(RegistrationField.Password, result.valid);
|
||||||
};
|
};
|
||||||
|
|
||||||
onPasswordConfirmChange = ev => {
|
private onPasswordConfirmChange = ev => {
|
||||||
this.setState({
|
this.setState({
|
||||||
passwordConfirm: ev.target.value,
|
passwordConfirm: ev.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onPasswordConfirmValidate = async fieldState => {
|
private onPasswordConfirmValidate = async fieldState => {
|
||||||
const result = await this.validatePasswordConfirmRules(fieldState);
|
const result = await this.validatePasswordConfirmRules(fieldState);
|
||||||
this.markFieldValid(FIELD_PASSWORD_CONFIRM, result.valid);
|
this.markFieldValid(RegistrationField.PasswordConfirm, result.valid);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
validatePasswordConfirmRules = withValidation({
|
private validatePasswordConfirmRules = withValidation({
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
key: "required",
|
key: "required",
|
||||||
|
@ -297,7 +317,7 @@ export default class RegistrationForm extends React.Component {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "match",
|
key: "match",
|
||||||
test({ value }) {
|
test(this: RegistrationForm, { value }) {
|
||||||
return !value || value === this.state.password;
|
return !value || value === this.state.password;
|
||||||
},
|
},
|
||||||
invalid: () => _t("Passwords don't match"),
|
invalid: () => _t("Passwords don't match"),
|
||||||
|
@ -305,33 +325,32 @@ export default class RegistrationForm extends React.Component {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
onPhoneCountryChange = newVal => {
|
private onPhoneCountryChange = newVal => {
|
||||||
this.setState({
|
this.setState({
|
||||||
phoneCountry: newVal.iso2,
|
phoneCountry: newVal.iso2,
|
||||||
phonePrefix: newVal.prefix,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onPhoneNumberChange = ev => {
|
private onPhoneNumberChange = ev => {
|
||||||
this.setState({
|
this.setState({
|
||||||
phoneNumber: ev.target.value,
|
phoneNumber: ev.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onPhoneNumberValidate = async fieldState => {
|
private onPhoneNumberValidate = async fieldState => {
|
||||||
const result = await this.validatePhoneNumberRules(fieldState);
|
const result = await this.validatePhoneNumberRules(fieldState);
|
||||||
this.markFieldValid(FIELD_PHONE_NUMBER, result.valid);
|
this.markFieldValid(RegistrationField.PhoneNumber, result.valid);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
validatePhoneNumberRules = withValidation({
|
private validatePhoneNumberRules = withValidation({
|
||||||
description: () => _t("Other users can invite you to rooms using your contact details"),
|
description: () => _t("Other users can invite you to rooms using your contact details"),
|
||||||
hideDescriptionIfValid: true,
|
hideDescriptionIfValid: true,
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
key: "required",
|
key: "required",
|
||||||
test({ value, allowEmpty }) {
|
test(this: RegistrationForm, { value, allowEmpty }) {
|
||||||
return allowEmpty || !this._authStepIsRequired('m.login.msisdn') || !!value;
|
return allowEmpty || !this.authStepIsRequired('m.login.msisdn') || !!value;
|
||||||
},
|
},
|
||||||
invalid: () => _t("Enter phone number (required on this homeserver)"),
|
invalid: () => _t("Enter phone number (required on this homeserver)"),
|
||||||
},
|
},
|
||||||
|
@ -343,19 +362,19 @@ export default class RegistrationForm extends React.Component {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
onUsernameChange = ev => {
|
private onUsernameChange = ev => {
|
||||||
this.setState({
|
this.setState({
|
||||||
username: ev.target.value,
|
username: ev.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onUsernameValidate = async fieldState => {
|
private onUsernameValidate = async fieldState => {
|
||||||
const result = await this.validateUsernameRules(fieldState);
|
const result = await this.validateUsernameRules(fieldState);
|
||||||
this.markFieldValid(FIELD_USERNAME, result.valid);
|
this.markFieldValid(RegistrationField.Username, result.valid);
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
validateUsernameRules = withValidation({
|
private validateUsernameRules = withValidation({
|
||||||
description: () => _t("Use lowercase letters, numbers, dashes and underscores only"),
|
description: () => _t("Use lowercase letters, numbers, dashes and underscores only"),
|
||||||
hideDescriptionIfValid: true,
|
hideDescriptionIfValid: true,
|
||||||
rules: [
|
rules: [
|
||||||
|
@ -378,7 +397,7 @@ export default class RegistrationForm extends React.Component {
|
||||||
* @param {string} step A stage name to check
|
* @param {string} step A stage name to check
|
||||||
* @returns {boolean} Whether it is required
|
* @returns {boolean} Whether it is required
|
||||||
*/
|
*/
|
||||||
_authStepIsRequired(step) {
|
private authStepIsRequired(step: string) {
|
||||||
return this.props.flows.every((flow) => {
|
return this.props.flows.every((flow) => {
|
||||||
return flow.stages.includes(step);
|
return flow.stages.includes(step);
|
||||||
});
|
});
|
||||||
|
@ -390,46 +409,46 @@ export default class RegistrationForm extends React.Component {
|
||||||
* @param {string} step A stage name to check
|
* @param {string} step A stage name to check
|
||||||
* @returns {boolean} Whether it is used
|
* @returns {boolean} Whether it is used
|
||||||
*/
|
*/
|
||||||
_authStepIsUsed(step) {
|
private authStepIsUsed(step: string) {
|
||||||
return this.props.flows.some((flow) => {
|
return this.props.flows.some((flow) => {
|
||||||
return flow.stages.includes(step);
|
return flow.stages.includes(step);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_showEmail() {
|
private showEmail() {
|
||||||
const haveIs = Boolean(this.props.serverConfig.isUrl);
|
const haveIs = Boolean(this.props.serverConfig.isUrl);
|
||||||
if (
|
if (
|
||||||
(this.props.serverRequiresIdServer && !haveIs) ||
|
(this.props.serverRequiresIdServer && !haveIs) ||
|
||||||
!this._authStepIsUsed('m.login.email.identity')
|
!this.authStepIsUsed('m.login.email.identity')
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_showPhoneNumber() {
|
private showPhoneNumber() {
|
||||||
const threePidLogin = !SdkConfig.get().disable_3pid_login;
|
const threePidLogin = !SdkConfig.get().disable_3pid_login;
|
||||||
const haveIs = Boolean(this.props.serverConfig.isUrl);
|
const haveIs = Boolean(this.props.serverConfig.isUrl);
|
||||||
if (
|
if (
|
||||||
!threePidLogin ||
|
!threePidLogin ||
|
||||||
(this.props.serverRequiresIdServer && !haveIs) ||
|
(this.props.serverRequiresIdServer && !haveIs) ||
|
||||||
!this._authStepIsUsed('m.login.msisdn')
|
!this.authStepIsUsed('m.login.msisdn')
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderEmail() {
|
private renderEmail() {
|
||||||
if (!this._showEmail()) {
|
if (!this.showEmail()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const Field = sdk.getComponent('elements.Field');
|
const Field = sdk.getComponent('elements.Field');
|
||||||
const emailPlaceholder = this._authStepIsRequired('m.login.email.identity') ?
|
const emailPlaceholder = this.authStepIsRequired('m.login.email.identity') ?
|
||||||
_t("Email") :
|
_t("Email") :
|
||||||
_t("Email (optional)");
|
_t("Email (optional)");
|
||||||
return <Field
|
return <Field
|
||||||
ref={field => this[FIELD_EMAIL] = field}
|
ref={field => this[RegistrationField.Email] = field}
|
||||||
type="text"
|
type="text"
|
||||||
label={emailPlaceholder}
|
label={emailPlaceholder}
|
||||||
value={this.state.email}
|
value={this.state.email}
|
||||||
|
@ -440,10 +459,10 @@ export default class RegistrationForm extends React.Component {
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPassword() {
|
private renderPassword() {
|
||||||
return <PassphraseField
|
return <PassphraseField
|
||||||
id="mx_RegistrationForm_password"
|
id="mx_RegistrationForm_password"
|
||||||
fieldRef={field => this[FIELD_PASSWORD] = field}
|
fieldRef={field => this[RegistrationField.Password] = field}
|
||||||
minScore={PASSWORD_MIN_SCORE}
|
minScore={PASSWORD_MIN_SCORE}
|
||||||
value={this.state.password}
|
value={this.state.password}
|
||||||
onChange={this.onPasswordChange}
|
onChange={this.onPasswordChange}
|
||||||
|
@ -457,7 +476,7 @@ export default class RegistrationForm extends React.Component {
|
||||||
const Field = sdk.getComponent('elements.Field');
|
const Field = sdk.getComponent('elements.Field');
|
||||||
return <Field
|
return <Field
|
||||||
id="mx_RegistrationForm_passwordConfirm"
|
id="mx_RegistrationForm_passwordConfirm"
|
||||||
ref={field => this[FIELD_PASSWORD_CONFIRM] = field}
|
ref={field => this[RegistrationField.PasswordConfirm] = field}
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
label={_t("Confirm password")}
|
label={_t("Confirm password")}
|
||||||
|
@ -470,12 +489,12 @@ export default class RegistrationForm extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPhoneNumber() {
|
renderPhoneNumber() {
|
||||||
if (!this._showPhoneNumber()) {
|
if (!this.showPhoneNumber()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
|
const CountryDropdown = sdk.getComponent('views.auth.CountryDropdown');
|
||||||
const Field = sdk.getComponent('elements.Field');
|
const Field = sdk.getComponent('elements.Field');
|
||||||
const phoneLabel = this._authStepIsRequired('m.login.msisdn') ?
|
const phoneLabel = this.authStepIsRequired('m.login.msisdn') ?
|
||||||
_t("Phone") :
|
_t("Phone") :
|
||||||
_t("Phone (optional)");
|
_t("Phone (optional)");
|
||||||
const phoneCountry = <CountryDropdown
|
const phoneCountry = <CountryDropdown
|
||||||
|
@ -485,7 +504,7 @@ export default class RegistrationForm extends React.Component {
|
||||||
onOptionChange={this.onPhoneCountryChange}
|
onOptionChange={this.onPhoneCountryChange}
|
||||||
/>;
|
/>;
|
||||||
return <Field
|
return <Field
|
||||||
ref={field => this[FIELD_PHONE_NUMBER] = field}
|
ref={field => this[RegistrationField.PhoneNumber] = field}
|
||||||
type="text"
|
type="text"
|
||||||
label={phoneLabel}
|
label={phoneLabel}
|
||||||
value={this.state.phoneNumber}
|
value={this.state.phoneNumber}
|
||||||
|
@ -499,7 +518,7 @@ export default class RegistrationForm extends React.Component {
|
||||||
const Field = sdk.getComponent('elements.Field');
|
const Field = sdk.getComponent('elements.Field');
|
||||||
return <Field
|
return <Field
|
||||||
id="mx_RegistrationForm_username"
|
id="mx_RegistrationForm_username"
|
||||||
ref={field => this[FIELD_USERNAME] = field}
|
ref={field => this[RegistrationField.Username] = field}
|
||||||
type="text"
|
type="text"
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
label={_t("Username")}
|
label={_t("Username")}
|
||||||
|
@ -517,8 +536,8 @@ export default class RegistrationForm extends React.Component {
|
||||||
);
|
);
|
||||||
|
|
||||||
let emailHelperText = null;
|
let emailHelperText = null;
|
||||||
if (this._showEmail()) {
|
if (this.showEmail()) {
|
||||||
if (this._showPhoneNumber()) {
|
if (this.showPhoneNumber()) {
|
||||||
emailHelperText = <div>
|
emailHelperText = <div>
|
||||||
{_t(
|
{_t(
|
||||||
"Set an email for account recovery. " +
|
"Set an email for account recovery. " +
|
|
@ -64,7 +64,7 @@ interface IProps {
|
||||||
// All other props pass through to the <input>.
|
// All other props pass through to the <input>.
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
|
export interface IInputProps extends IProps, InputHTMLAttributes<HTMLInputElement> {
|
||||||
// The element to create. Defaults to "input".
|
// The element to create. Defaults to "input".
|
||||||
element?: "input";
|
element?: "input";
|
||||||
// The input's value. This is a controlled component, so the value is required.
|
// The input's value. This is a controlled component, so the value is required.
|
||||||
|
|
|
@ -32,7 +32,7 @@ interface IRule<T, D = void> {
|
||||||
|
|
||||||
interface IArgs<T, D = void> {
|
interface IArgs<T, D = void> {
|
||||||
rules: IRule<T, D>[];
|
rules: IRule<T, D>[];
|
||||||
description(this: T, derivedData: D): React.ReactChild;
|
description?(this: T, derivedData: D): React.ReactChild;
|
||||||
hideDescriptionIfValid?: boolean;
|
hideDescriptionIfValid?: boolean;
|
||||||
deriveData?(data: Data): Promise<D>;
|
deriveData?(data: Data): Promise<D>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2286,10 +2286,11 @@
|
||||||
"Nice, strong password!": "Nice, strong password!",
|
"Nice, strong password!": "Nice, strong password!",
|
||||||
"Password is allowed, but unsafe": "Password is allowed, but unsafe",
|
"Password is allowed, but unsafe": "Password is allowed, but unsafe",
|
||||||
"Keep going...": "Keep going...",
|
"Keep going...": "Keep going...",
|
||||||
"The email field must not be blank.": "The email field must not be blank.",
|
"Enter username": "Enter username",
|
||||||
"The username field must not be blank.": "The username field must not be blank.",
|
"Enter email address": "Enter email address",
|
||||||
"The phone number field must not be blank.": "The phone number field must not be blank.",
|
"Doesn't look like a valid email address": "Doesn't look like a valid email address",
|
||||||
"The password field must not be blank.": "The password field must not be blank.",
|
"Enter phone number": "Enter phone number",
|
||||||
|
"Doesn't look like a valid phone number": "Doesn't look like a valid phone number",
|
||||||
"Email": "Email",
|
"Email": "Email",
|
||||||
"Username": "Username",
|
"Username": "Username",
|
||||||
"Phone": "Phone",
|
"Phone": "Phone",
|
||||||
|
@ -2300,13 +2301,10 @@
|
||||||
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
|
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
|
||||||
"Use an email address to recover your account": "Use an email address to recover your account",
|
"Use an email address to recover your account": "Use an email address to recover your account",
|
||||||
"Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)",
|
"Enter email address (required on this homeserver)": "Enter email address (required on this homeserver)",
|
||||||
"Doesn't look like a valid email address": "Doesn't look like a valid email address",
|
|
||||||
"Passwords don't match": "Passwords don't match",
|
"Passwords don't match": "Passwords don't match",
|
||||||
"Other users can invite you to rooms using your contact details": "Other users can invite you to rooms using your contact details",
|
"Other users can invite you to rooms using your contact details": "Other users can invite you to rooms using your contact details",
|
||||||
"Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)",
|
"Enter phone number (required on this homeserver)": "Enter phone number (required on this homeserver)",
|
||||||
"Doesn't look like a valid phone number": "Doesn't look like a valid phone number",
|
|
||||||
"Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only",
|
"Use lowercase letters, numbers, dashes and underscores only": "Use lowercase letters, numbers, dashes and underscores only",
|
||||||
"Enter username": "Enter username",
|
|
||||||
"Email (optional)": "Email (optional)",
|
"Email (optional)": "Email (optional)",
|
||||||
"Phone (optional)": "Phone (optional)",
|
"Phone (optional)": "Phone (optional)",
|
||||||
"Register": "Register",
|
"Register": "Register",
|
||||||
|
@ -2512,7 +2510,6 @@
|
||||||
"Incorrect username and/or password.": "Incorrect username and/or password.",
|
"Incorrect username and/or password.": "Incorrect username and/or password.",
|
||||||
"Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.",
|
"Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.",
|
||||||
"Failed to perform homeserver discovery": "Failed to perform homeserver discovery",
|
"Failed to perform homeserver discovery": "Failed to perform homeserver discovery",
|
||||||
"The phone number entered looks invalid": "The phone number entered looks invalid",
|
|
||||||
"This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.",
|
"This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.",
|
||||||
"Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.",
|
"Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.",
|
||||||
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.",
|
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.",
|
||||||
|
|
Loading…
Reference in a new issue