From 7027656f4bf6fe2142f94461d0753a93743e1f27 Mon Sep 17 00:00:00 2001
From: Diego Mello <diegolmello@gmail.com>
Date: Fri, 10 Nov 2017 11:42:02 -0200
Subject: [PATCH] Forgot my password (#62)

* Forgot password working
---
 app/actions/actionsTypes.js           |   4 +
 app/actions/login.js                  |  26 ++++++
 app/containers/routes/PublicRoutes.js |   7 ++
 app/lib/rocketchat.js                 |  11 +++
 app/reducers/login.js                 |  22 +++++
 app/sagas/login.js                    |  15 +++-
 app/views/ForgotPasswordView.js       | 116 ++++++++++++++++++++++++++
 app/views/LoginView.js                |  10 ++-
 app/views/Styles.js                   |   3 -
 9 files changed, 209 insertions(+), 5 deletions(-)
 create mode 100644 app/views/ForgotPasswordView.js

diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js
index 308c4baa6..72bc1ffbb 100644
--- a/app/actions/actionsTypes.js
+++ b/app/actions/actionsTypes.js
@@ -20,6 +20,10 @@ export const LOGIN = createRequestTypes('LOGIN', [
 	'SET_USERNAME_REQUEST',
 	'SET_USERNAME_SUCCESS'
 ]);
+export const FORGOT_PASSWORD = createRequestTypes('FORGOT_PASSWORD', [
+	...defaultTypes,
+	'INIT'
+]);
 export const ROOMS = createRequestTypes('ROOMS');
 export const APP = createRequestTypes('APP', ['READY', 'INIT']);
 export const MESSAGES = createRequestTypes('MESSAGES');
diff --git a/app/actions/login.js b/app/actions/login.js
index 1a8f753e8..4a67acf84 100644
--- a/app/actions/login.js
+++ b/app/actions/login.js
@@ -81,3 +81,29 @@ export function logout() {
 		type: types.LOGOUT
 	};
 }
+
+export function forgotPasswordInit() {
+	return {
+		type: types.FORGOT_PASSWORD.INIT
+	};
+}
+
+export function forgotPasswordRequest(email) {
+	return {
+		type: types.FORGOT_PASSWORD.REQUEST,
+		email
+	};
+}
+
+export function forgotPasswordSuccess() {
+	return {
+		type: types.FORGOT_PASSWORD.SUCCESS
+	};
+}
+
+export function forgotPasswordFailure(err) {
+	return {
+		type: types.FORGOT_PASSWORD.FAILURE,
+		err
+	};
+}
diff --git a/app/containers/routes/PublicRoutes.js b/app/containers/routes/PublicRoutes.js
index a99583cd2..92781c00d 100644
--- a/app/containers/routes/PublicRoutes.js
+++ b/app/containers/routes/PublicRoutes.js
@@ -7,6 +7,7 @@ import ListServerView from '../../views/ListServerView';
 import NewServerView from '../../views/NewServerView';
 import LoginView from '../../views/LoginView';
 import RegisterView from '../../views/RegisterView';
+import ForgotPasswordView from '../../views/ForgotPasswordView';
 
 const PublicRoutes = StackNavigator(
 	{
@@ -43,6 +44,12 @@ const PublicRoutes = StackNavigator(
 			navigationOptions: {
 				title: 'Register'
 			}
+		},
+		ForgotPassword: {
+			screen: ForgotPasswordView,
+			navigationOptions: {
+				title: 'Forgot my password'
+			}
 		}
 	},
 	{
diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js
index 139d94db8..81bc55f40 100644
--- a/app/lib/rocketchat.js
+++ b/app/lib/rocketchat.js
@@ -144,6 +144,17 @@ const RocketChat = {
 		});
 	},
 
+	forgotPassword(email) {
+		return new Promise((resolve, reject) => {
+			Meteor.call('sendForgotPasswordEmail', email, (err, result) => {
+				if (err) {
+					reject(err);
+				}
+				resolve(result);
+			});
+		});
+	},
+
 	loginWithPassword({ username, password, code }, callback) {
 		let params = {};
 		const state = reduxStore.getState();
diff --git a/app/reducers/login.js b/app/reducers/login.js
index 9a3c33de2..fc0d94227 100644
--- a/app/reducers/login.js
+++ b/app/reducers/login.js
@@ -69,6 +69,28 @@ export default function login(state = initialState, action) {
 				isFetching: false,
 				isRegistering: false
 			};
+		case types.FORGOT_PASSWORD.INIT:
+			return initialState;
+		case types.FORGOT_PASSWORD.REQUEST:
+			return {
+				...state,
+				isFetching: true,
+				failure: false,
+				success: false
+			};
+		case types.FORGOT_PASSWORD.SUCCESS:
+			return {
+				...state,
+				isFetching: false,
+				success: true
+			};
+		case types.FORGOT_PASSWORD.FAILURE:
+			return {
+				...state,
+				isFetching: false,
+				failure: true,
+				error: action.err
+			};
 		default:
 			return state;
 	}
diff --git a/app/sagas/login.js b/app/sagas/login.js
index 90bee4648..c16e9f1b4 100644
--- a/app/sagas/login.js
+++ b/app/sagas/login.js
@@ -11,7 +11,9 @@ import {
 	logout,
 	registerSuccess,
 	setUsernameRequest,
-	setUsernameSuccess
+	setUsernameSuccess,
+	forgotPasswordSuccess,
+	forgotPasswordFailure
 } from '../actions/login';
 import RocketChat from '../lib/rocketchat';
 
@@ -22,6 +24,7 @@ const loginCall = args => (args.resume ? RocketChat.login(args) : RocketChat.log
 const registerCall = args => RocketChat.register(args);
 const setUsernameCall = args => RocketChat.setUsername(args);
 const logoutCall = args => RocketChat.logout(args);
+const forgotPasswordCall = args => RocketChat.forgotPassword(args);
 
 const getToken = function* getToken() {
 	const currentServer = yield select(getServer);
@@ -138,6 +141,15 @@ const handleLogout = function* handleLogout() {
 	yield call(logoutCall, { server });
 };
 
+const handleForgotPasswordRequest = function* handleForgotPasswordRequest({ email }) {
+	try {
+		yield call(forgotPasswordCall, email);
+		yield put(forgotPasswordSuccess());
+	} catch (err) {
+		yield put(forgotPasswordFailure(err));
+	}
+};
+
 const root = function* root() {
 	yield takeEvery(types.SERVER.CHANGED, handleLoginWhenServerChanges);
 	yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest);
@@ -149,5 +161,6 @@ const root = function* root() {
 	yield takeLatest(types.LOGIN.SET_USERNAME_SUBMIT, handleSetUsernameSubmit);
 	yield takeLatest(types.LOGIN.SET_USERNAME_REQUEST, handleSetUsernameRequest);
 	yield takeLatest(types.LOGOUT, handleLogout);
+	yield takeLatest(types.FORGOT_PASSWORD.REQUEST, handleForgotPasswordRequest);
 };
 export default root;
diff --git a/app/views/ForgotPasswordView.js b/app/views/ForgotPasswordView.js
new file mode 100644
index 000000000..ae26fbd2a
--- /dev/null
+++ b/app/views/ForgotPasswordView.js
@@ -0,0 +1,116 @@
+import React from 'react';
+import Spinner from 'react-native-loading-spinner-overlay';
+import PropTypes from 'prop-types';
+import { Text, TextInput, View, TouchableOpacity, Alert } from 'react-native';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+import * as loginActions from '../actions/login';
+import KeyboardView from '../presentation/KeyboardView';
+
+import styles from './Styles';
+
+class ForgotPasswordView extends React.Component {
+	static propTypes = {
+		forgotPasswordInit: PropTypes.func.isRequired,
+		forgotPasswordRequest: PropTypes.func.isRequired,
+		login: PropTypes.object,
+		navigation: PropTypes.object.isRequired
+	}
+
+	constructor(props) {
+		super(props);
+
+		this.state = {
+			email: '',
+			invalidEmail: false
+		};
+	}
+
+	componentWillMount() {
+		this.props.forgotPasswordInit();
+	}
+
+	componentDidUpdate() {
+		const { login } = this.props;
+		if (login.success) {
+			this.props.navigation.goBack();
+			setTimeout(() => {
+				Alert.alert(
+					'Alert',
+					'If this email is registered, ' +
+					'we\'ll send instructions on how to reset your password. ' +
+					'If you do not receive an email shortly, please come back and try again.'
+				);
+			});
+		}
+	}
+
+	validate = (email) => {
+		/* eslint-disable no-useless-escape */
+		const reg = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+		if (!reg.test(email)) {
+			this.setState({ invalidEmail: true });
+			return;
+		}
+		this.setState({ email, invalidEmail: false });
+	}
+
+	resetPassword = () => {
+		if (this.state.invalidEmail) {
+			return;
+		}
+		this.props.forgotPasswordRequest(this.state.email);
+	}
+
+	backLogin = () => {
+		this.props.navigation.goBack();
+	}
+
+	render() {
+		return (
+			<KeyboardView
+				contentContainerStyle={styles.container}
+				keyboardVerticalOffset={128}
+			>
+				<View style={styles.loginView}>
+					<View style={styles.formContainer}>
+						<TextInput
+							style={[styles.input_white, this.state.invalidEmail ? { borderColor: 'red' } : {}]}
+							onChangeText={email => this.validate(email)}
+							keyboardType='email-address'
+							autoCorrect={false}
+							returnKeyType='next'
+							autoCapitalize='none'
+							underlineColorAndroid='transparent'
+							onSubmitEditing={() => this.resetPassword()}
+							placeholder='Email'
+						/>
+
+						<TouchableOpacity style={styles.buttonContainer} onPress={this.resetPassword}>
+							<Text style={styles.button}>RESET PASSWORD</Text>
+						</TouchableOpacity>
+
+						<TouchableOpacity style={styles.buttonContainer} onPress={this.backLogin}>
+							<Text style={styles.button}>BACK TO LOGIN</Text>
+						</TouchableOpacity>
+
+						{this.props.login.failure && <Text style={styles.error}>{this.props.login.error.reason}</Text>}
+					</View>
+					<Spinner visible={this.props.login.isFetching} textContent={'Loading...'} textStyle={{ color: '#FFF' }} />
+				</View>
+			</KeyboardView>
+		);
+	}
+}
+
+function mapStateToProps(state) {
+	return {
+		login: state.login
+	};
+}
+
+function mapDispatchToProps(dispatch) {
+	return bindActionCreators(loginActions, dispatch);
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ForgotPasswordView);
diff --git a/app/views/LoginView.js b/app/views/LoginView.js
index 2f259c3dc..f27741bc3 100644
--- a/app/views/LoginView.js
+++ b/app/views/LoginView.js
@@ -49,6 +49,10 @@ class LoginView extends React.Component {
 		this.props.navigation.navigate('Register');
 	}
 
+	forgotPassword = () => {
+		this.props.navigation.navigate('ForgotPassword');
+	}
+
 	renderTOTP = () => {
 		if (this.props.login.errorMessage && this.props.login.errorMessage.error === 'totp-required') {
 			return (
@@ -106,10 +110,14 @@ class LoginView extends React.Component {
 							<Text style={styles.button} onPress={this.submit}>LOGIN</Text>
 						</TouchableOpacity>
 
-						<TouchableOpacity style={[styles.buttonContainer, styles.registerContainer]}>
+						<TouchableOpacity style={styles.buttonContainer}>
 							<Text style={styles.button} onPress={this.register}>REGISTER</Text>
 						</TouchableOpacity>
 
+						<TouchableOpacity style={styles.buttonContainer} onPress={this.forgotPassword}>
+							<Text style={styles.button}>FORGOT MY PASSWORD</Text>
+						</TouchableOpacity>
+
 						{this.props.login.failure && <Text style={styles.error}>{this.props.login.error.reason}</Text>}
 					</View>
 					<Spinner visible={this.props.login.isFetching} textContent={'Loading...'} textStyle={{ color: '#FFF' }} />
diff --git a/app/views/Styles.js b/app/views/Styles.js
index 6fb77cb66..299739353 100644
--- a/app/views/Styles.js
+++ b/app/views/Styles.js
@@ -93,9 +93,6 @@ export default StyleSheet.create({
 		backgroundColor: '#1d74f5',
 		marginBottom: 20
 	},
-	registerContainer: {
-		marginBottom: 0
-	},
 	button: {
 		textAlign: 'center',
 		color: 'white',
-- 
GitLab