MM-10924 Add forgot password screen (#1878)

* MM-10924 Add forgot password screen
 * Add a new screen for the user to reset password.
   but can only verify the link through web UI

* Update mattermost-redux version to include isEmail helper
This commit is contained in:
Sudheer 2018-07-18 14:16:02 +05:30 committed by GitHub
parent 92363dea5a
commit e27ed497b1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 3699 additions and 3 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,198 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import Button from 'react-native-button';
import {intlShape} from 'react-intl';
import {
Image,
StyleSheet,
Text,
TextInput,
TouchableWithoutFeedback,
View,
} from 'react-native';
import {isEmail} from 'mattermost-redux/utils/helpers';
import {GlobalStyles} from 'app/styles';
import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
import StatusBar from 'app/components/status_bar';
export default class ForgotPassword extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
sendPasswordResetEmail: PropTypes.func.isRequired,
}),
}
static contextTypes = {
intl: intlShape.isRequired,
};
constructor(props) {
super(props);
this.state = {
error: null,
email: '',
sentPasswordLink: false,
};
}
changeEmail = (email) => {
this.setState({
email,
});
}
submitResetPassword = async () => {
if (!this.state.email || !isEmail(this.state.email)) {
const {formatMessage} = this.context.intl;
this.setState({
error: formatMessage({id: 'password_send.error', defaultMessage: 'Please enter a valid email address.'}),
});
return;
}
const {data, error} = await this.props.actions.sendPasswordResetEmail(this.state.email);
if (error) {
this.setState({error});
} else if (this.state.error) {
this.setState({error: ''});
}
if (data) {
this.setState({sentPasswordLink: true});
}
}
emailIdRef = (ref) => {
this.emailId = ref;
};
blur = () => {
if (this.emailId) {
this.emailId.blur();
}
}
render() {
const {formatMessage} = this.context.intl;
let displayError;
if (this.state.error) {
displayError = (
<ErrorText
error={this.state.error}
textStyle={style.errorText}
/>
);
}
let passwordFormView;
if (this.state.sentPasswordLink) {
passwordFormView = (
<View style={style.resetSuccessContainer}>
<FormattedText
style={style.successTxtColor}
id='password_send.link'
defaultMessage='If the account exists, a password reset email will be sent to:'
/>
<Text style={[style.successTxtColor, style.emailId]}>
{this.state.email}
</Text>
<FormattedText
style={[style.successTxtColor, style.defaultTopPadding]}
id='password_send.checkInbox'
defaultMessage='Please check your inbox.'
/>
</View>
);
} else {
passwordFormView = (
<View>
<FormattedText
style={[GlobalStyles.subheader, style.defaultTopPadding]}
id='password_send.description'
defaultMessage='To reset your password, enter the email address you used to sign up'
/>
<TextInput
ref={this.emailIdRef}
style={GlobalStyles.inputBox}
onChangeText={this.changeEmail}
placeholder={formatMessage({id: 'login.email', defaultMessage: 'Email'})}
autoCorrect={false}
autoCapitalize='none'
keyboardType='email-address'
underlineColorAndroid='transparent'
blurOnSubmit={false}
disableFullscreenUI={true}
/>
<Button
containerStyle={GlobalStyles.signupButton}
disabled={!this.state.email}
onPress={this.submitResetPassword}
>
<FormattedText
id='password_send.reset'
defaultMessage='Reset my password'
style={[GlobalStyles.signupButtonText]}
/>
</Button>
</View>
);
}
return (
<View style={style.container}>
<StatusBar/>
<TouchableWithoutFeedback
onPress={this.blur}
>
<View style={style.innerContainer}>
<Image
source={require('assets/images/logo.png')}
/>
{displayError}
{passwordFormView}
</View>
</TouchableWithoutFeedback>
</View>
);
}
}
const style = StyleSheet.create({
container: {
backgroundColor: '#FFFFFF',
flex: 1,
},
innerContainer: {
alignItems: 'center',
flexDirection: 'column',
justifyContent: 'center',
paddingHorizontal: 15,
paddingVertical: 50,
},
forgotPasswordBtn: {
borderColor: 'transparent',
marginTop: 15,
},
resetSuccessContainer: {
marginTop: 15,
padding: 10,
backgroundColor: '#dff0d8',
borderColor: '#d6e9c6',
},
emailId: {
fontWeight: 'bold',
},
successTxtColor: {
color: '#3c763d',
},
defaultTopPadding: {
paddingTop: 15,
},
});

View file

@ -0,0 +1,78 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {configure, shallow} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
configure({adapter: new Adapter()});
import ForgotPassword from './forgot_password.js';
jest.mock('react-intl');
describe('ForgotPassword', () => {
const actions = {
sendPasswordResetEmail: jest.fn(),
};
const baseProps = {
actions,
};
const formatMessage = jest.fn();
test('should match snapshot', () => {
const wrapper = shallow(
<ForgotPassword {...baseProps}/>,
{context: {intl: {formatMessage}}},
);
expect(wrapper).toMatchSnapshot();
});
test('snapshot for error on failure of email regex', () => {
const wrapper = shallow(
<ForgotPassword {...baseProps}/>,
{context: {intl: {formatMessage}}},
);
wrapper.setState({email: 'bar'});
wrapper.instance().submitResetPassword();
wrapper.update();
expect(wrapper).toMatchSnapshot();
});
test('Should call sendPasswordResetEmail', () => {
const wrapper = shallow(
<ForgotPassword {...baseProps}/>,
{context: {intl: {formatMessage}}},
);
wrapper.setState({email: 'test@test.com'});
wrapper.instance().submitResetPassword();
expect(actions.sendPasswordResetEmail).toBeCalledWith('test@test.com');
});
test('match snapshot after success of sendPasswordResetEmail', async () => {
const sendPasswordResetEmail = async () => {
return {
data: {},
};
};
const wrapper = shallow(
<ForgotPassword
{...baseProps}
actions={{
...actions,
sendPasswordResetEmail,
}}
/>,
{context: {intl: {formatMessage}}},
);
wrapper.setState({email: 'test@test.com'});
wrapper.instance().submitResetPassword();
await sendPasswordResetEmail();
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -0,0 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {sendPasswordResetEmail} from 'mattermost-redux/actions/users';
import ForgotPassword from './forgot_password';
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
sendPasswordResetEmail,
}, dispatch),
};
}
export default connect(null, mapDispatchToProps)(ForgotPassword);

View file

@ -28,6 +28,7 @@ export function registerScreens(store, Provider) {
Navigation.registerComponent('Entry', () => Entry, store, Provider);
Navigation.registerComponent('ExpandedAnnouncementBanner', () => wrapWithContextProvider(require('app/screens/expanded_announcement_banner').default), store, Provider);
Navigation.registerComponent('FlaggedPosts', () => wrapWithContextProvider(require('app/screens/flagged_posts').default), store, Provider);
Navigation.registerComponent('ForgotPassword', () => wrapWithContextProvider(require('app/screens/forgot_password').default), store, Provider);
Navigation.registerComponent('ImagePreview', () => wrapWithContextProvider(require('app/screens/image_preview').default), store, Provider);
Navigation.registerComponent('Login', () => wrapWithContextProvider(require('app/screens/login').default), store, Provider);
Navigation.registerComponent('LoginOptions', () => wrapWithContextProvider(require('app/screens/login_options').default), store, Provider);

View file

@ -300,6 +300,23 @@ export default class Login extends PureComponent {
this.scroll = ref;
};
forgotPassword = () => {
const {intl} = this.context;
const {navigator, theme} = this.props;
navigator.push({
screen: 'ForgotPassword',
title: intl.formatMessage({id: 'password_form.title', defaultMessage: 'Password Reset'}),
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg,
},
});
}
render() {
const isLoading = this.props.loginRequest.status === RequestStatus.STARTED || this.state.isLoading;
@ -394,6 +411,16 @@ export default class Login extends PureComponent {
disableFullscreenUI={true}
/>
{proceed}
<Button
onPress={this.forgotPassword}
containerStyle={[style.forgotPasswordBtn]}
>
<FormattedText
id='login.forgot'
defaultMessage='I forgot my password'
style={style.forgotPasswordTxt}
/>
</Button>
</KeyboardAwareScrollView>
</TouchableWithoutFeedback>
</View>
@ -413,4 +440,11 @@ const style = StyleSheet.create({
paddingHorizontal: 15,
paddingVertical: 50,
},
forgotPasswordBtn: {
borderColor: 'transparent',
marginTop: 15,
},
forgotPasswordTxt: {
color: '#2389D7',
},
});

4
package-lock.json generated
View file

@ -10100,8 +10100,8 @@
}
},
"mattermost-redux": {
"version": "github:mattermost/mattermost-redux#ade5924a6e509ea76757bd78a2cc193043a584ce",
"from": "github:mattermost/mattermost-redux#ade5924a6e509ea76757bd78a2cc193043a584ce",
"version": "github:mattermost/mattermost-redux#1bc11c578b1dc2e5a508a00de65e4e9a549d72b1",
"from": "github:mattermost/mattermost-redux#1bc11c578b1dc2e5a508a00de65e4e9a549d72b1",
"requires": {
"deep-equal": "1.0.1",
"eslint-plugin-header": "1.2.0",

View file

@ -15,7 +15,7 @@
"intl": "1.2.5",
"jail-monkey": "1.0.0",
"jsc-android": "216113.0.3",
"mattermost-redux": "github:mattermost/mattermost-redux#ade5924a6e509ea76757bd78a2cc193043a584ce",
"mattermost-redux": "github:mattermost/mattermost-redux#1bc11c578b1dc2e5a508a00de65e4e9a549d72b1",
"mime-db": "1.33.0",
"prop-types": "15.6.1",
"react": "16.3.2",