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:
parent
92363dea5a
commit
e27ed497b1
8 changed files with 3699 additions and 3 deletions
File diff suppressed because it is too large
Load diff
198
app/screens/forgot_password/forgot_password.js
Normal file
198
app/screens/forgot_password/forgot_password.js
Normal 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,
|
||||
},
|
||||
});
|
||||
78
app/screens/forgot_password/forgot_password.test.js
Normal file
78
app/screens/forgot_password/forgot_password.test.js
Normal 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();
|
||||
});
|
||||
});
|
||||
18
app/screens/forgot_password/index.js
Normal file
18
app/screens/forgot_password/index.js
Normal 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);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
4
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue