Support registration & login with phone number (#742)
* WIP msisdn sign in * A mostly working country picker * Fix bug where you'dbe logged out after registering Stop the guest sync, otherwise it gets 401ed for using a guest access token for a non-guest, causing us to beliebe we've been logged out. * Use InteractiveAuth component for registration * Fix tests * Remove old signup code * Signup -> Login Now that Signup contains no code whatsoever related to signing up, rename it to Login. Get rid of the Signup class. * Stray newline * Fix more merge failing * Get phone country & number to the right place * More-or-less working msisdn auth component * Send the bind_msisdn param on registration * Refinements to country dropdown Rendering the whole lot when the component was rendered just makes the page load really slow, so just show 2 at a time and rely on type-to-search. Make type-to-search always display an exact iso2 match first * Propagate initial inputs to the phone input * Support msisdn login * semicolon * Fix PropTypes * Oops, use the 1qst element of the array Not the array of object keys which has no particular order * Make dropdown/countrydropdown controlled * Unused line * Add note on DOM layout * onOptionChange is required * More docs * Add missing propTypes * Don't resume promise on error * Use React.Children to manipulate children * Make catch less weird * Fix null dereference Assuming [0] of an empty list == undefined doesn't work if you're then taking a property of it.pull/21833/head
parent
676c5c21c1
commit
0269562383
|
@ -58,6 +58,22 @@ export function unicodeToImage(str) {
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given one or more unicode characters (represented by unicode
|
||||||
|
* character number), return an image node with the corresponding
|
||||||
|
* emoji.
|
||||||
|
*
|
||||||
|
* @param alt {string} String to use for the image alt text
|
||||||
|
* @param unicode {integer} One or more integers representing unicode characters
|
||||||
|
* @returns A img node with the corresponding emoji
|
||||||
|
*/
|
||||||
|
export function charactersToImageNode(alt, ...unicode) {
|
||||||
|
const fileName = unicode.map((u) => {
|
||||||
|
return u.toString(16);
|
||||||
|
}).join('-');
|
||||||
|
return <img alt={alt} src={`${emojione.imagePathSVG}${fileName}.svg${emojione.cacheBustParam}`}/>;
|
||||||
|
}
|
||||||
|
|
||||||
export function stripParagraphs(html: string): string {
|
export function stripParagraphs(html: string): string {
|
||||||
const contentDiv = document.createElement('div');
|
const contentDiv = document.createElement('div');
|
||||||
contentDiv.innerHTML = html;
|
contentDiv.innerHTML = html;
|
||||||
|
|
41
src/Login.js
41
src/Login.js
|
@ -105,21 +105,38 @@ export default class Login {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loginViaPassword(username, pass) {
|
loginViaPassword(username, phoneCountry, phoneNumber, pass) {
|
||||||
var self = this;
|
const self = this;
|
||||||
var isEmail = username.indexOf("@") > 0;
|
|
||||||
var loginParams = {
|
const isEmail = username.indexOf("@") > 0;
|
||||||
password: pass,
|
|
||||||
initial_device_display_name: this._defaultDeviceDisplayName,
|
let identifier;
|
||||||
};
|
if (phoneCountry && phoneNumber) {
|
||||||
if (isEmail) {
|
identifier = {
|
||||||
loginParams.medium = 'email';
|
type: 'm.id.phone',
|
||||||
loginParams.address = username;
|
country: phoneCountry,
|
||||||
|
number: phoneNumber,
|
||||||
|
};
|
||||||
|
} else if (isEmail) {
|
||||||
|
identifier = {
|
||||||
|
type: 'm.id.thirdparty',
|
||||||
|
medium: 'email',
|
||||||
|
address: username,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
loginParams.user = username;
|
identifier = {
|
||||||
|
type: 'm.id.user',
|
||||||
|
user: username,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var client = this._createTemporaryClient();
|
const loginParams = {
|
||||||
|
password: pass,
|
||||||
|
identifier: identifier,
|
||||||
|
initial_device_display_name: this._defaultDeviceDisplayName,
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = this._createTemporaryClient();
|
||||||
return client.login('m.login.password', loginParams).then(function(data) {
|
return client.login('m.login.password', loginParams).then(function(data) {
|
||||||
return q({
|
return q({
|
||||||
homeserverUrl: self._hsUrl,
|
homeserverUrl: self._hsUrl,
|
||||||
|
|
|
@ -109,6 +109,8 @@ import views$elements$DeviceVerifyButtons from './components/views/elements/Devi
|
||||||
views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons);
|
views$elements$DeviceVerifyButtons && (module.exports.components['views.elements.DeviceVerifyButtons'] = views$elements$DeviceVerifyButtons);
|
||||||
import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox';
|
import views$elements$DirectorySearchBox from './components/views/elements/DirectorySearchBox';
|
||||||
views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = views$elements$DirectorySearchBox);
|
views$elements$DirectorySearchBox && (module.exports.components['views.elements.DirectorySearchBox'] = views$elements$DirectorySearchBox);
|
||||||
|
import views$elements$Dropdown from './components/views/elements/Dropdown';
|
||||||
|
views$elements$Dropdown && (module.exports.components['views.elements.Dropdown'] = views$elements$Dropdown);
|
||||||
import views$elements$EditableText from './components/views/elements/EditableText';
|
import views$elements$EditableText from './components/views/elements/EditableText';
|
||||||
views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText);
|
views$elements$EditableText && (module.exports.components['views.elements.EditableText'] = views$elements$EditableText);
|
||||||
import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer';
|
import views$elements$EditableTextContainer from './components/views/elements/EditableTextContainer';
|
||||||
|
@ -131,6 +133,8 @@ import views$login$CaptchaForm from './components/views/login/CaptchaForm';
|
||||||
views$login$CaptchaForm && (module.exports.components['views.login.CaptchaForm'] = views$login$CaptchaForm);
|
views$login$CaptchaForm && (module.exports.components['views.login.CaptchaForm'] = views$login$CaptchaForm);
|
||||||
import views$login$CasLogin from './components/views/login/CasLogin';
|
import views$login$CasLogin from './components/views/login/CasLogin';
|
||||||
views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = views$login$CasLogin);
|
views$login$CasLogin && (module.exports.components['views.login.CasLogin'] = views$login$CasLogin);
|
||||||
|
import views$login$CountryDropdown from './components/views/login/CountryDropdown';
|
||||||
|
views$login$CountryDropdown && (module.exports.components['views.login.CountryDropdown'] = views$login$CountryDropdown);
|
||||||
import views$login$CustomServerDialog from './components/views/login/CustomServerDialog';
|
import views$login$CustomServerDialog from './components/views/login/CustomServerDialog';
|
||||||
views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog);
|
views$login$CustomServerDialog && (module.exports.components['views.login.CustomServerDialog'] = views$login$CustomServerDialog);
|
||||||
import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents';
|
import views$login$InteractiveAuthEntryComponents from './components/views/login/InteractiveAuthEntryComponents';
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations 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.
|
||||||
|
@ -64,8 +65,10 @@ module.exports = React.createClass({
|
||||||
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||||
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||||
|
|
||||||
// used for preserving username when changing homeserver
|
// used for preserving form values when changing homeserver
|
||||||
username: "",
|
username: "",
|
||||||
|
phoneCountry: null,
|
||||||
|
phoneNumber: "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -73,20 +76,21 @@ module.exports = React.createClass({
|
||||||
this._initLoginLogic();
|
this._initLoginLogic();
|
||||||
},
|
},
|
||||||
|
|
||||||
onPasswordLogin: function(username, password) {
|
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||||
var self = this;
|
this.setState({
|
||||||
self.setState({
|
|
||||||
busy: true,
|
busy: true,
|
||||||
errorText: null,
|
errorText: null,
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
this._loginLogic.loginViaPassword(username, password).then(function(data) {
|
this._loginLogic.loginViaPassword(
|
||||||
self.props.onLoggedIn(data);
|
username, phoneCountry, phoneNumber, password,
|
||||||
}, function(error) {
|
).then((data) => {
|
||||||
self._setStateFromError(error, true);
|
this.props.onLoggedIn(data);
|
||||||
}).finally(function() {
|
}, (error) => {
|
||||||
self.setState({
|
this._setStateFromError(error, true);
|
||||||
|
}).finally(() => {
|
||||||
|
this.setState({
|
||||||
busy: false
|
busy: false
|
||||||
});
|
});
|
||||||
}).done();
|
}).done();
|
||||||
|
@ -119,6 +123,14 @@ module.exports = React.createClass({
|
||||||
this.setState({ username: username });
|
this.setState({ username: username });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onPhoneCountryChanged: function(phoneCountry) {
|
||||||
|
this.setState({ phoneCountry: phoneCountry });
|
||||||
|
},
|
||||||
|
|
||||||
|
onPhoneNumberChanged: function(phoneNumber) {
|
||||||
|
this.setState({ phoneNumber: phoneNumber });
|
||||||
|
},
|
||||||
|
|
||||||
onHsUrlChanged: function(newHsUrl) {
|
onHsUrlChanged: function(newHsUrl) {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -225,7 +237,11 @@ module.exports = React.createClass({
|
||||||
<PasswordLogin
|
<PasswordLogin
|
||||||
onSubmit={this.onPasswordLogin}
|
onSubmit={this.onPasswordLogin}
|
||||||
initialUsername={this.state.username}
|
initialUsername={this.state.username}
|
||||||
|
initialPhoneCountry={this.state.phoneCountry}
|
||||||
|
initialPhoneNumber={this.state.phoneNumber}
|
||||||
onUsernameChanged={this.onUsernameChanged}
|
onUsernameChanged={this.onUsernameChanged}
|
||||||
|
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
||||||
|
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||||
loginIncorrect={this.state.loginIncorrect}
|
loginIncorrect={this.state.loginIncorrect}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -262,6 +262,9 @@ module.exports = React.createClass({
|
||||||
case "RegistrationForm.ERR_EMAIL_INVALID":
|
case "RegistrationForm.ERR_EMAIL_INVALID":
|
||||||
errMsg = "This doesn't look like a valid email address";
|
errMsg = "This doesn't look like a valid email address";
|
||||||
break;
|
break;
|
||||||
|
case "RegistrationForm.ERR_PHONE_NUMBER_INVALID":
|
||||||
|
errMsg = "This doesn't look like a valid phone number";
|
||||||
|
break;
|
||||||
case "RegistrationForm.ERR_USERNAME_INVALID":
|
case "RegistrationForm.ERR_USERNAME_INVALID":
|
||||||
errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores.";
|
errMsg = "User names may only contain letters, numbers, dots, hyphens and underscores.";
|
||||||
break;
|
break;
|
||||||
|
@ -296,15 +299,20 @@ module.exports = React.createClass({
|
||||||
guestAccessToken = null;
|
guestAccessToken = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only send the bind params if we're sending username / pw params
|
||||||
|
// (Since we need to send no params at all to use the ones saved in the
|
||||||
|
// session).
|
||||||
|
const bindThreepids = this.state.formVals.password ? {
|
||||||
|
email: true,
|
||||||
|
msisdn: true,
|
||||||
|
} : {};
|
||||||
|
|
||||||
return this._matrixClient.register(
|
return this._matrixClient.register(
|
||||||
this.state.formVals.username,
|
this.state.formVals.username,
|
||||||
this.state.formVals.password,
|
this.state.formVals.password,
|
||||||
undefined, // session id: included in the auth dict already
|
undefined, // session id: included in the auth dict already
|
||||||
auth,
|
auth,
|
||||||
// Only send the bind_email param if we're sending username / pw params
|
bindThreepids,
|
||||||
// (Since we need to send no params at all to use the ones saved in the
|
|
||||||
// session).
|
|
||||||
Boolean(this.state.formVals.username) || undefined,
|
|
||||||
guestAccessToken,
|
guestAccessToken,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -355,6 +363,8 @@ module.exports = React.createClass({
|
||||||
<RegistrationForm
|
<RegistrationForm
|
||||||
defaultUsername={this.state.formVals.username}
|
defaultUsername={this.state.formVals.username}
|
||||||
defaultEmail={this.state.formVals.email}
|
defaultEmail={this.state.formVals.email}
|
||||||
|
defaultPhoneCountry={this.state.formVals.phoneCountry}
|
||||||
|
defaultPhoneNumber={this.state.formVals.phoneNumber}
|
||||||
defaultPassword={this.state.formVals.password}
|
defaultPassword={this.state.formVals.password}
|
||||||
teamsConfig={this.state.teamsConfig}
|
teamsConfig={this.state.teamsConfig}
|
||||||
guestUsername={guestUsername}
|
guestUsername={guestUsername}
|
||||||
|
|
|
@ -27,8 +27,8 @@ import React from 'react';
|
||||||
export default function AccessibleButton(props) {
|
export default function AccessibleButton(props) {
|
||||||
const {element, onClick, children, ...restProps} = props;
|
const {element, onClick, children, ...restProps} = props;
|
||||||
restProps.onClick = onClick;
|
restProps.onClick = onClick;
|
||||||
restProps.onKeyDown = function(e) {
|
restProps.onKeyUp = function(e) {
|
||||||
if (e.keyCode == 13 || e.keyCode == 32) return onClick();
|
if (e.keyCode == 13 || e.keyCode == 32) return onClick(e);
|
||||||
};
|
};
|
||||||
restProps.tabIndex = restProps.tabIndex || "0";
|
restProps.tabIndex = restProps.tabIndex || "0";
|
||||||
restProps.role = "button";
|
restProps.role = "button";
|
||||||
|
|
|
@ -0,0 +1,324 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations 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 AccessibleButton from './AccessibleButton';
|
||||||
|
|
||||||
|
class MenuOption extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this._onMouseEnter = this._onMouseEnter.bind(this);
|
||||||
|
this._onClick = this._onClick.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMouseEnter() {
|
||||||
|
this.props.onMouseEnter(this.props.dropdownKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.props.onClick(this.props.dropdownKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const optClasses = classnames({
|
||||||
|
mx_Dropdown_option: true,
|
||||||
|
mx_Dropdown_option_highlight: this.props.highlighted,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className={optClasses}
|
||||||
|
onClick={this._onClick} onKeyPress={this._onKeyPress}
|
||||||
|
onMouseEnter={this._onMouseEnter}
|
||||||
|
>
|
||||||
|
{this.props.children}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
MenuOption.propTypes = {
|
||||||
|
children: React.PropTypes.oneOfType([
|
||||||
|
React.PropTypes.arrayOf(React.PropTypes.node),
|
||||||
|
React.PropTypes.node
|
||||||
|
]),
|
||||||
|
highlighted: React.PropTypes.bool,
|
||||||
|
dropdownKey: React.PropTypes.string,
|
||||||
|
onClick: React.PropTypes.func.isRequired,
|
||||||
|
onMouseEnter: React.PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Reusable dropdown select control, akin to react-select,
|
||||||
|
* but somewhat simpler as react-select is 79KB of minified
|
||||||
|
* javascript.
|
||||||
|
*
|
||||||
|
* TODO: Port NetworkDropdown to use this.
|
||||||
|
*/
|
||||||
|
export default class Dropdown extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.dropdownRootElement = null;
|
||||||
|
this.ignoreEvent = null;
|
||||||
|
|
||||||
|
this._onInputClick = this._onInputClick.bind(this);
|
||||||
|
this._onRootClick = this._onRootClick.bind(this);
|
||||||
|
this._onDocumentClick = this._onDocumentClick.bind(this);
|
||||||
|
this._onMenuOptionClick = this._onMenuOptionClick.bind(this);
|
||||||
|
this._onInputKeyPress = this._onInputKeyPress.bind(this);
|
||||||
|
this._onInputKeyUp = this._onInputKeyUp.bind(this);
|
||||||
|
this._onInputChange = this._onInputChange.bind(this);
|
||||||
|
this._collectRoot = this._collectRoot.bind(this);
|
||||||
|
this._collectInputTextBox = this._collectInputTextBox.bind(this);
|
||||||
|
this._setHighlightedOption = this._setHighlightedOption.bind(this);
|
||||||
|
|
||||||
|
this.inputTextBox = null;
|
||||||
|
|
||||||
|
this._reindexChildren(this.props.children);
|
||||||
|
|
||||||
|
const firstChild = React.Children.toArray(props.children)[0];
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
// True if the menu is dropped-down
|
||||||
|
expanded: false,
|
||||||
|
// The key of the highlighted option
|
||||||
|
// (the option that would become selected if you pressed enter)
|
||||||
|
highlightedOption: firstChild ? firstChild.key : null,
|
||||||
|
// the current search query
|
||||||
|
searchQuery: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
// Listen for all clicks on the document so we can close the
|
||||||
|
// menu when the user clicks somewhere else
|
||||||
|
document.addEventListener('click', this._onDocumentClick, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('click', this._onDocumentClick, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps(nextProps) {
|
||||||
|
this._reindexChildren(nextProps.children);
|
||||||
|
const firstChild = React.Children.toArray(nextProps.children)[0];
|
||||||
|
this.setState({
|
||||||
|
highlightedOption: firstChild ? firstChild.key : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_reindexChildren(children) {
|
||||||
|
this.childrenByKey = {};
|
||||||
|
React.Children.forEach(children, (child) => {
|
||||||
|
this.childrenByKey[child.key] = child;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_onDocumentClick(ev) {
|
||||||
|
// Close the dropdown if the user clicks anywhere that isn't
|
||||||
|
// within our root element
|
||||||
|
if (ev !== this.ignoreEvent) {
|
||||||
|
this.setState({
|
||||||
|
expanded: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onRootClick(ev) {
|
||||||
|
// This captures any clicks that happen within our elements,
|
||||||
|
// such that we can then ignore them when they're seen by the
|
||||||
|
// click listener on the document handler, ie. not close the
|
||||||
|
// dropdown immediately after opening it.
|
||||||
|
// NB. We can't just stopPropagation() because then the event
|
||||||
|
// doesn't reach the React onClick().
|
||||||
|
this.ignoreEvent = ev;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onInputClick(ev) {
|
||||||
|
this.setState({
|
||||||
|
expanded: !this.state.expanded,
|
||||||
|
});
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
_onMenuOptionClick(dropdownKey) {
|
||||||
|
this.setState({
|
||||||
|
expanded: false,
|
||||||
|
});
|
||||||
|
this.props.onOptionChange(dropdownKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
_onInputKeyPress(e) {
|
||||||
|
// This needs to be on the keypress event because otherwise
|
||||||
|
// it can't cancel the form submission
|
||||||
|
if (e.key == 'Enter') {
|
||||||
|
this.setState({
|
||||||
|
expanded: false,
|
||||||
|
});
|
||||||
|
this.props.onOptionChange(this.state.highlightedOption);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onInputKeyUp(e) {
|
||||||
|
// These keys don't generate keypress events and so needs to
|
||||||
|
// be on keyup
|
||||||
|
if (e.key == 'Escape') {
|
||||||
|
this.setState({
|
||||||
|
expanded: false,
|
||||||
|
});
|
||||||
|
} else if (e.key == 'ArrowDown') {
|
||||||
|
this.setState({
|
||||||
|
highlightedOption: this._nextOption(this.state.highlightedOption),
|
||||||
|
});
|
||||||
|
} else if (e.key == 'ArrowUp') {
|
||||||
|
this.setState({
|
||||||
|
highlightedOption: this._prevOption(this.state.highlightedOption),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onInputChange(e) {
|
||||||
|
this.setState({
|
||||||
|
searchQuery: e.target.value,
|
||||||
|
});
|
||||||
|
if (this.props.onSearchChange) {
|
||||||
|
this.props.onSearchChange(e.target.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_collectRoot(e) {
|
||||||
|
if (this.dropdownRootElement) {
|
||||||
|
this.dropdownRootElement.removeEventListener(
|
||||||
|
'click', this._onRootClick, false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (e) {
|
||||||
|
e.addEventListener('click', this._onRootClick, false);
|
||||||
|
}
|
||||||
|
this.dropdownRootElement = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
_collectInputTextBox(e) {
|
||||||
|
this.inputTextBox = e;
|
||||||
|
if (e) e.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
_setHighlightedOption(optionKey) {
|
||||||
|
this.setState({
|
||||||
|
highlightedOption: optionKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_nextOption(optionKey) {
|
||||||
|
const keys = Object.keys(this.childrenByKey);
|
||||||
|
const index = keys.indexOf(optionKey);
|
||||||
|
return keys[(index + 1) % keys.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
_prevOption(optionKey) {
|
||||||
|
const keys = Object.keys(this.childrenByKey);
|
||||||
|
const index = keys.indexOf(optionKey);
|
||||||
|
return keys[(index - 1) % keys.length];
|
||||||
|
}
|
||||||
|
|
||||||
|
_getMenuOptions() {
|
||||||
|
const options = React.Children.map(this.props.children, (child) => {
|
||||||
|
return (
|
||||||
|
<MenuOption key={child.key} dropdownKey={child.key}
|
||||||
|
highlighted={this.state.highlightedOption == child.key}
|
||||||
|
onMouseEnter={this._setHighlightedOption}
|
||||||
|
onClick={this._onMenuOptionClick}
|
||||||
|
>
|
||||||
|
{child}
|
||||||
|
</MenuOption>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this.state.searchQuery) {
|
||||||
|
options.push(
|
||||||
|
<div key="_searchprompt" className="mx_Dropdown_searchPrompt">
|
||||||
|
Type to search...
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let currentValue;
|
||||||
|
|
||||||
|
const menuStyle = {};
|
||||||
|
if (this.props.menuWidth) menuStyle.width = this.props.menuWidth;
|
||||||
|
|
||||||
|
let menu;
|
||||||
|
if (this.state.expanded) {
|
||||||
|
currentValue = <input type="text" className="mx_Dropdown_option"
|
||||||
|
ref={this._collectInputTextBox} onKeyPress={this._onInputKeyPress}
|
||||||
|
onKeyUp={this._onInputKeyUp}
|
||||||
|
onChange={this._onInputChange}
|
||||||
|
value={this.state.searchQuery}
|
||||||
|
/>;
|
||||||
|
menu = <div className="mx_Dropdown_menu" style={menuStyle}>
|
||||||
|
{this._getMenuOptions()}
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
const selectedChild = this.props.getShortOption ?
|
||||||
|
this.props.getShortOption(this.props.value) :
|
||||||
|
this.childrenByKey[this.props.value];
|
||||||
|
currentValue = <div className="mx_Dropdown_option">
|
||||||
|
{selectedChild}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropdownClasses = {
|
||||||
|
mx_Dropdown: true,
|
||||||
|
};
|
||||||
|
if (this.props.className) {
|
||||||
|
dropdownClasses[this.props.className] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note the menu sits inside the AccessibleButton div so it's anchored
|
||||||
|
// to the input, but overflows below it. The root contains both.
|
||||||
|
return <div className={classnames(dropdownClasses)} ref={this._collectRoot}>
|
||||||
|
<AccessibleButton className="mx_Dropdown_input" onClick={this._onInputClick}>
|
||||||
|
{currentValue}
|
||||||
|
<span className="mx_Dropdown_arrow"></span>
|
||||||
|
{menu}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Dropdown.propTypes = {
|
||||||
|
// The width that the dropdown should be. If specified,
|
||||||
|
// the dropped-down part of the menu will be set to this
|
||||||
|
// width.
|
||||||
|
menuWidth: React.PropTypes.number,
|
||||||
|
// Called when the selected option changes
|
||||||
|
onOptionChange: React.PropTypes.func.isRequired,
|
||||||
|
// Called when the value of the search field changes
|
||||||
|
onSearchChange: React.PropTypes.func,
|
||||||
|
// Function that, given the key of an option, returns
|
||||||
|
// a node representing that option to be displayed in the
|
||||||
|
// box itself as the currently-selected option (ie. as
|
||||||
|
// opposed to in the actual dropped-down part). If
|
||||||
|
// unspecified, the appropriate child element is used as
|
||||||
|
// in the dropped-down menu.
|
||||||
|
getShortOption: React.PropTypes.func,
|
||||||
|
value: React.PropTypes.string,
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations 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 sdk from '../../../index';
|
||||||
|
|
||||||
|
import { COUNTRIES } from '../../../phonenumber';
|
||||||
|
import { charactersToImageNode } from '../../../HtmlUtils';
|
||||||
|
|
||||||
|
const COUNTRIES_BY_ISO2 = new Object(null);
|
||||||
|
for (const c of COUNTRIES) {
|
||||||
|
COUNTRIES_BY_ISO2[c.iso2] = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function countryMatchesSearchQuery(query, country) {
|
||||||
|
if (country.name.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
|
||||||
|
if (country.iso2 == query.toUpperCase()) return true;
|
||||||
|
if (country.prefix == query) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_DISPLAYED_ROWS = 2;
|
||||||
|
|
||||||
|
export default class CountryDropdown extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this._onSearchChange = this._onSearchChange.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
searchQuery: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.value) {
|
||||||
|
// If no value is given, we start with the first
|
||||||
|
// country selected, but our parent component
|
||||||
|
// doesn't know this, therefore we do this.
|
||||||
|
this.props.onOptionChange(COUNTRIES[0].iso2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_onSearchChange(search) {
|
||||||
|
this.setState({
|
||||||
|
searchQuery: search,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_flagImgForIso2(iso2) {
|
||||||
|
// Unicode Regional Indicator Symbol letter 'A'
|
||||||
|
const RIS_A = 0x1F1E6;
|
||||||
|
const ASCII_A = 65;
|
||||||
|
return charactersToImageNode(iso2,
|
||||||
|
RIS_A + (iso2.charCodeAt(0) - ASCII_A),
|
||||||
|
RIS_A + (iso2.charCodeAt(1) - ASCII_A),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||||
|
|
||||||
|
let displayedCountries;
|
||||||
|
if (this.state.searchQuery) {
|
||||||
|
displayedCountries = COUNTRIES.filter(
|
||||||
|
countryMatchesSearchQuery.bind(this, this.state.searchQuery),
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
this.state.searchQuery.length == 2 &&
|
||||||
|
COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()]
|
||||||
|
) {
|
||||||
|
// exact ISO2 country name match: make the first result the matches ISO2
|
||||||
|
const matched = COUNTRIES_BY_ISO2[this.state.searchQuery.toUpperCase()];
|
||||||
|
displayedCountries = displayedCountries.filter((c) => {
|
||||||
|
return c.iso2 != matched.iso2;
|
||||||
|
});
|
||||||
|
displayedCountries.unshift(matched);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
displayedCountries = COUNTRIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayedCountries.length > MAX_DISPLAYED_ROWS) {
|
||||||
|
displayedCountries = displayedCountries.slice(0, MAX_DISPLAYED_ROWS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = displayedCountries.map((country) => {
|
||||||
|
return <div key={country.iso2}>
|
||||||
|
{this._flagImgForIso2(country.iso2)}
|
||||||
|
{country.name}
|
||||||
|
</div>;
|
||||||
|
});
|
||||||
|
|
||||||
|
// default value here too, otherwise we need to handle null / undefined
|
||||||
|
// values between mounting and the initial value propgating
|
||||||
|
const value = this.props.value || COUNTRIES[0].iso2;
|
||||||
|
|
||||||
|
return <Dropdown className={this.props.className}
|
||||||
|
onOptionChange={this.props.onOptionChange} onSearchChange={this._onSearchChange}
|
||||||
|
menuWidth={298} getShortOption={this._flagImgForIso2}
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
{options}
|
||||||
|
</Dropdown>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CountryDropdown.propTypes = {
|
||||||
|
className: React.PropTypes.string,
|
||||||
|
onOptionChange: React.PropTypes.func.isRequired,
|
||||||
|
value: React.PropTypes.string,
|
||||||
|
};
|
|
@ -16,6 +16,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import url from 'url';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
|
||||||
|
@ -255,6 +257,137 @@ export const EmailIdentityAuthEntry = React.createClass({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const MsisdnAuthEntry = React.createClass({
|
||||||
|
displayName: 'MsisdnAuthEntry',
|
||||||
|
|
||||||
|
statics: {
|
||||||
|
LOGIN_TYPE: "m.login.msisdn",
|
||||||
|
},
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
inputs: React.PropTypes.shape({
|
||||||
|
phoneCountry: React.PropTypes.string,
|
||||||
|
phoneNumber: React.PropTypes.string,
|
||||||
|
}),
|
||||||
|
fail: React.PropTypes.func,
|
||||||
|
clientSecret: React.PropTypes.func,
|
||||||
|
submitAuthDict: React.PropTypes.func.isRequired,
|
||||||
|
matrixClient: React.PropTypes.object,
|
||||||
|
submitAuthDict: React.PropTypes.func,
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
token: '',
|
||||||
|
requestingToken: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this._sid = null;
|
||||||
|
this._msisdn = null;
|
||||||
|
this._tokenBox = null;
|
||||||
|
|
||||||
|
this.setState({requestingToken: true});
|
||||||
|
this._requestMsisdnToken().catch((e) => {
|
||||||
|
this.props.fail(e);
|
||||||
|
}).finally(() => {
|
||||||
|
this.setState({requestingToken: false});
|
||||||
|
}).done();
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Requests a verification token by SMS.
|
||||||
|
*/
|
||||||
|
_requestMsisdnToken: function() {
|
||||||
|
return this.props.matrixClient.requestRegisterMsisdnToken(
|
||||||
|
this.props.inputs.phoneCountry,
|
||||||
|
this.props.inputs.phoneNumber,
|
||||||
|
this.props.clientSecret,
|
||||||
|
1, // TODO: Multiple send attempts?
|
||||||
|
).then((result) => {
|
||||||
|
this._sid = result.sid;
|
||||||
|
this._msisdn = result.msisdn;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onTokenChange: function(e) {
|
||||||
|
this.setState({
|
||||||
|
token: e.target.value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_onFormSubmit: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (this.state.token == '') return;
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
errorText: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.matrixClient.submitMsisdnToken(
|
||||||
|
this._sid, this.props.clientSecret, this.state.token
|
||||||
|
).then((result) => {
|
||||||
|
if (result.success) {
|
||||||
|
const idServerParsedUrl = url.parse(
|
||||||
|
this.props.matrixClient.getIdentityServerUrl(),
|
||||||
|
)
|
||||||
|
this.props.submitAuthDict({
|
||||||
|
type: MsisdnAuthEntry.LOGIN_TYPE,
|
||||||
|
threepid_creds: {
|
||||||
|
sid: this._sid,
|
||||||
|
client_secret: this.props.clientSecret,
|
||||||
|
id_server: idServerParsedUrl.host,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({
|
||||||
|
errorText: "Token incorrect",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch((e) => {
|
||||||
|
this.props.fail(e);
|
||||||
|
console.log("Failed to submit msisdn token");
|
||||||
|
}).done();
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
if (this.state.requestingToken) {
|
||||||
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
|
return <Loader />;
|
||||||
|
} else {
|
||||||
|
const enableSubmit = Boolean(this.state.token);
|
||||||
|
const submitClasses = classnames({
|
||||||
|
mx_InteractiveAuthEntryComponents_msisdnSubmit: true,
|
||||||
|
mx_UserSettings_button: true, // XXX button classes
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>A text message has been sent to +<i>{this._msisdn}</i></p>
|
||||||
|
<p>Please enter the code it contains:</p>
|
||||||
|
<div className="mx_InteractiveAuthEntryComponents_msisdnWrapper">
|
||||||
|
<form onSubmit={this._onFormSubmit}>
|
||||||
|
<input type="text"
|
||||||
|
className="mx_InteractiveAuthEntryComponents_msisdnEntry"
|
||||||
|
value={this.state.token}
|
||||||
|
onChange={this._onTokenChange}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<input type="submit" value="Submit"
|
||||||
|
className={submitClasses}
|
||||||
|
disabled={!enableSubmit}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<div className="error">
|
||||||
|
{this.state.errorText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const FallbackAuthEntry = React.createClass({
|
export const FallbackAuthEntry = React.createClass({
|
||||||
displayName: 'FallbackAuthEntry',
|
displayName: 'FallbackAuthEntry',
|
||||||
|
|
||||||
|
@ -313,6 +446,7 @@ const AuthEntryComponents = [
|
||||||
PasswordAuthEntry,
|
PasswordAuthEntry,
|
||||||
RecaptchaAuthEntry,
|
RecaptchaAuthEntry,
|
||||||
EmailIdentityAuthEntry,
|
EmailIdentityAuthEntry,
|
||||||
|
MsisdnAuthEntry,
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getEntryComponentForLoginType(loginType) {
|
export function getEntryComponentForLoginType(loginType) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 Vector Creations 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.
|
||||||
|
@ -17,6 +18,7 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import sdk from '../../../index';
|
||||||
import {field_input_incorrect} from '../../../UiEffects';
|
import {field_input_incorrect} from '../../../UiEffects';
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,8 +30,12 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
onSubmit: React.PropTypes.func.isRequired, // fn(username, password)
|
||||||
onForgotPasswordClick: React.PropTypes.func, // fn()
|
onForgotPasswordClick: React.PropTypes.func, // fn()
|
||||||
initialUsername: React.PropTypes.string,
|
initialUsername: React.PropTypes.string,
|
||||||
|
initialPhoneCountry: React.PropTypes.string,
|
||||||
|
initialPhoneNumber: React.PropTypes.string,
|
||||||
initialPassword: React.PropTypes.string,
|
initialPassword: React.PropTypes.string,
|
||||||
onUsernameChanged: React.PropTypes.func,
|
onUsernameChanged: React.PropTypes.func,
|
||||||
|
onPhoneCountryChanged: React.PropTypes.func,
|
||||||
|
onPhoneNumberChanged: React.PropTypes.func,
|
||||||
onPasswordChanged: React.PropTypes.func,
|
onPasswordChanged: React.PropTypes.func,
|
||||||
loginIncorrect: React.PropTypes.bool,
|
loginIncorrect: React.PropTypes.bool,
|
||||||
},
|
},
|
||||||
|
@ -38,7 +44,11 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
return {
|
return {
|
||||||
onUsernameChanged: function() {},
|
onUsernameChanged: function() {},
|
||||||
onPasswordChanged: function() {},
|
onPasswordChanged: function() {},
|
||||||
|
onPhoneCountryChanged: function() {},
|
||||||
|
onPhoneNumberChanged: function() {},
|
||||||
initialUsername: "",
|
initialUsername: "",
|
||||||
|
initialPhoneCountry: "",
|
||||||
|
initialPhoneNumber: "",
|
||||||
initialPassword: "",
|
initialPassword: "",
|
||||||
loginIncorrect: false,
|
loginIncorrect: false,
|
||||||
};
|
};
|
||||||
|
@ -48,6 +58,8 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
return {
|
return {
|
||||||
username: this.props.initialUsername,
|
username: this.props.initialUsername,
|
||||||
password: this.props.initialPassword,
|
password: this.props.initialPassword,
|
||||||
|
phoneCountry: this.props.initialPhoneCountry,
|
||||||
|
phoneNumber: this.props.initialPhoneNumber,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -63,7 +75,12 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
|
|
||||||
onSubmitForm: function(ev) {
|
onSubmitForm: function(ev) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.props.onSubmit(this.state.username, this.state.password);
|
this.props.onSubmit(
|
||||||
|
this.state.username,
|
||||||
|
this.state.phoneCountry,
|
||||||
|
this.state.phoneNumber,
|
||||||
|
this.state.password,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
onUsernameChanged: function(ev) {
|
onUsernameChanged: function(ev) {
|
||||||
|
@ -71,6 +88,16 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
this.props.onUsernameChanged(ev.target.value);
|
this.props.onUsernameChanged(ev.target.value);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onPhoneCountryChanged: function(country) {
|
||||||
|
this.setState({phoneCountry: country});
|
||||||
|
this.props.onPhoneCountryChanged(country);
|
||||||
|
},
|
||||||
|
|
||||||
|
onPhoneNumberChanged: function(ev) {
|
||||||
|
this.setState({phoneNumber: ev.target.value});
|
||||||
|
this.props.onPhoneNumberChanged(ev.target.value);
|
||||||
|
},
|
||||||
|
|
||||||
onPasswordChanged: function(ev) {
|
onPasswordChanged: function(ev) {
|
||||||
this.setState({password: ev.target.value});
|
this.setState({password: ev.target.value});
|
||||||
this.props.onPasswordChanged(ev.target.value);
|
this.props.onPasswordChanged(ev.target.value);
|
||||||
|
@ -92,13 +119,28 @@ module.exports = React.createClass({displayName: 'PasswordLogin',
|
||||||
error: this.props.loginIncorrect,
|
error: this.props.loginIncorrect,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={this.onSubmitForm}>
|
<form onSubmit={this.onSubmitForm}>
|
||||||
<input className="mx_Login_field" type="text"
|
<input className="mx_Login_field mx_Login_username" type="text"
|
||||||
name="username" // make it a little easier for browser's remember-password
|
name="username" // make it a little easier for browser's remember-password
|
||||||
value={this.state.username} onChange={this.onUsernameChanged}
|
value={this.state.username} onChange={this.onUsernameChanged}
|
||||||
placeholder="Email or user name" autoFocus />
|
placeholder="Email or user name" autoFocus />
|
||||||
|
or
|
||||||
|
<div className="mx_Login_phoneSection">
|
||||||
|
<CountryDropdown ref="phone_country" onOptionChange={this.onPhoneCountryChanged}
|
||||||
|
className="mx_Login_phoneCountry"
|
||||||
|
value={this.state.phoneCountry}
|
||||||
|
/>
|
||||||
|
<input type="text" ref="phoneNumber"
|
||||||
|
onChange={this.onPhoneNumberChanged}
|
||||||
|
placeholder="Mobile phone number"
|
||||||
|
className="mx_Login_phoneNumberField mx_Login_field"
|
||||||
|
value={this.state.phoneNumber}
|
||||||
|
name="phoneNumber"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
|
||||||
name="password"
|
name="password"
|
||||||
|
|
|
@ -19,9 +19,12 @@ import React from 'react';
|
||||||
import { field_input_incorrect } from '../../../UiEffects';
|
import { field_input_incorrect } from '../../../UiEffects';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import Email from '../../../email';
|
import Email from '../../../email';
|
||||||
|
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
|
|
||||||
const FIELD_EMAIL = 'field_email';
|
const FIELD_EMAIL = 'field_email';
|
||||||
|
const FIELD_PHONE_COUNTRY = 'field_phone_country';
|
||||||
|
const FIELD_PHONE_NUMBER = 'field_phone_number';
|
||||||
const FIELD_USERNAME = 'field_username';
|
const FIELD_USERNAME = 'field_username';
|
||||||
const FIELD_PASSWORD = 'field_password';
|
const FIELD_PASSWORD = 'field_password';
|
||||||
const FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
|
const FIELD_PASSWORD_CONFIRM = 'field_password_confirm';
|
||||||
|
@ -35,6 +38,8 @@ module.exports = React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
// Values pre-filled in the input boxes when the component loads
|
// Values pre-filled in the input boxes when the component loads
|
||||||
defaultEmail: React.PropTypes.string,
|
defaultEmail: React.PropTypes.string,
|
||||||
|
defaultPhoneCountry: React.PropTypes.string,
|
||||||
|
defaultPhoneNumber: React.PropTypes.string,
|
||||||
defaultUsername: React.PropTypes.string,
|
defaultUsername: React.PropTypes.string,
|
||||||
defaultPassword: React.PropTypes.string,
|
defaultPassword: React.PropTypes.string,
|
||||||
teamsConfig: React.PropTypes.shape({
|
teamsConfig: React.PropTypes.shape({
|
||||||
|
@ -71,6 +76,8 @@ module.exports = React.createClass({
|
||||||
return {
|
return {
|
||||||
fieldValid: {},
|
fieldValid: {},
|
||||||
selectedTeam: null,
|
selectedTeam: null,
|
||||||
|
// The ISO2 country code selected in the phone number entry
|
||||||
|
phoneCountry: this.props.defaultPhoneCountry,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -85,6 +92,7 @@ module.exports = React.createClass({
|
||||||
this.validateField(FIELD_PASSWORD_CONFIRM);
|
this.validateField(FIELD_PASSWORD_CONFIRM);
|
||||||
this.validateField(FIELD_PASSWORD);
|
this.validateField(FIELD_PASSWORD);
|
||||||
this.validateField(FIELD_USERNAME);
|
this.validateField(FIELD_USERNAME);
|
||||||
|
this.validateField(FIELD_PHONE_NUMBER);
|
||||||
this.validateField(FIELD_EMAIL);
|
this.validateField(FIELD_EMAIL);
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -118,6 +126,8 @@ module.exports = React.createClass({
|
||||||
username: this.refs.username.value.trim() || this.props.guestUsername,
|
username: this.refs.username.value.trim() || this.props.guestUsername,
|
||||||
password: this.refs.password.value.trim(),
|
password: this.refs.password.value.trim(),
|
||||||
email: email,
|
email: email,
|
||||||
|
phoneCountry: this.state.phoneCountry,
|
||||||
|
phoneNumber: this.refs.phoneNumber.value.trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (promise) {
|
if (promise) {
|
||||||
|
@ -174,6 +184,11 @@ module.exports = React.createClass({
|
||||||
const emailValid = email === '' || Email.looksValid(email);
|
const emailValid = email === '' || Email.looksValid(email);
|
||||||
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
|
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
|
||||||
break;
|
break;
|
||||||
|
case FIELD_PHONE_NUMBER:
|
||||||
|
const phoneNumber = this.refs.phoneNumber.value;
|
||||||
|
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
|
||||||
|
this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
|
||||||
|
break;
|
||||||
case FIELD_USERNAME:
|
case FIELD_USERNAME:
|
||||||
// XXX: SPEC-1
|
// XXX: SPEC-1
|
||||||
var username = this.refs.username.value.trim() || this.props.guestUsername;
|
var username = this.refs.username.value.trim() || this.props.guestUsername;
|
||||||
|
@ -233,6 +248,8 @@ module.exports = React.createClass({
|
||||||
switch (field_id) {
|
switch (field_id) {
|
||||||
case FIELD_EMAIL:
|
case FIELD_EMAIL:
|
||||||
return this.refs.email;
|
return this.refs.email;
|
||||||
|
case FIELD_PHONE_NUMBER:
|
||||||
|
return this.refs.phoneNumber;
|
||||||
case FIELD_USERNAME:
|
case FIELD_USERNAME:
|
||||||
return this.refs.username;
|
return this.refs.username;
|
||||||
case FIELD_PASSWORD:
|
case FIELD_PASSWORD:
|
||||||
|
@ -251,6 +268,12 @@ module.exports = React.createClass({
|
||||||
return cls;
|
return cls;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_onPhoneCountryChange(newVal) {
|
||||||
|
this.setState({
|
||||||
|
phoneCountry: newVal,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
@ -286,6 +309,25 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||||
|
const phoneSection = (
|
||||||
|
<div className="mx_Login_phoneSection">
|
||||||
|
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
|
||||||
|
className="mx_Login_phoneCountry"
|
||||||
|
value={this.state.phoneCountry}
|
||||||
|
/>
|
||||||
|
<input type="text" ref="phoneNumber"
|
||||||
|
placeholder="Mobile phone number (optional)"
|
||||||
|
defaultValue={this.props.defaultPhoneNumber}
|
||||||
|
className={this._classForField(
|
||||||
|
FIELD_PHONE_NUMBER, 'mx_Login_phoneNumberField', 'mx_Login_field'
|
||||||
|
)}
|
||||||
|
onBlur={function() {self.validateField(FIELD_PHONE_NUMBER);}}
|
||||||
|
value={self.state.phoneNumber}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const registerButton = (
|
const registerButton = (
|
||||||
<input className="mx_Login_submit" type="submit" value="Register" />
|
<input className="mx_Login_submit" type="submit" value="Register" />
|
||||||
);
|
);
|
||||||
|
@ -300,6 +342,7 @@ module.exports = React.createClass({
|
||||||
<form onSubmit={this.onSubmit}>
|
<form onSubmit={this.onSubmit}>
|
||||||
{emailSection}
|
{emailSection}
|
||||||
{belowEmailSection}
|
{belowEmailSection}
|
||||||
|
{phoneSection}
|
||||||
<input type="text" ref="username"
|
<input type="text" ref="username"
|
||||||
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
placeholder={ placeholderUserName } defaultValue={this.props.defaultUsername}
|
||||||
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
className={this._classForField(FIELD_USERNAME, 'mx_Login_field')}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue