From dbf7c18b4e76ada98e38d6f32e26efb1a2c96f5e Mon Sep 17 00:00:00 2001 From: Gilmar Quinelato Date: Thu, 21 Sep 2017 23:24:33 -0300 Subject: [PATCH] Added feature to register a new user --- app/actions/actionsTypes.js | 2 +- app/actions/login.js | 19 +++ app/containers/routes/PublicRoutes.js | 7 + app/lib/realm.js | 2 +- app/lib/rocketchat.js | 15 +++ app/presentation/KeyboardView.js | 15 ++- app/reducers/login.js | 14 ++ app/sagas/login.js | 24 +++- app/views/LoginView.js | 17 ++- app/views/RegisterView.js | 184 ++++++++++++++++++++++++++ app/views/Styles.js | 6 + package.json | 1 + yarn.lock | 12 +- 13 files changed, 306 insertions(+), 12 deletions(-) create mode 100644 app/views/RegisterView.js diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index 0aca14405..afd8531a4 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -10,7 +10,7 @@ function createRequestTypes(base, types = defaultTypes) { } // Login events -export const LOGIN = createRequestTypes('LOGIN', [...defaultTypes, 'SET_TOKEN', 'SUBMIT']); +export const LOGIN = createRequestTypes('LOGIN', [...defaultTypes, 'SET_TOKEN', 'SUBMIT', 'REGISTER_SUBMIT', 'REGISTER_REQUEST', 'REGISTER_SUCCESS']); 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 7969c8090..a9ee6147d 100644 --- a/app/actions/login.js +++ b/app/actions/login.js @@ -13,6 +13,25 @@ export function loginRequest(credentials) { }; } + +export function registerSubmit(credentials) { + return { + type: types.LOGIN.REGISTER_SUBMIT, + credentials + }; +} +export function registerRequest(credentials) { + return { + type: types.LOGIN.REGISTER_REQUEST, + credentials + }; +} +export function registerSuccess() { + return { + type: types.LOGIN.REGISTER_SUCCESS + }; +} + export function loginSuccess(user) { return { type: types.LOGIN.SUCCESS, diff --git a/app/containers/routes/PublicRoutes.js b/app/containers/routes/PublicRoutes.js index 3cf851f5c..a99583cd2 100644 --- a/app/containers/routes/PublicRoutes.js +++ b/app/containers/routes/PublicRoutes.js @@ -6,6 +6,7 @@ import Icon from 'react-native-vector-icons/FontAwesome'; import ListServerView from '../../views/ListServerView'; import NewServerView from '../../views/NewServerView'; import LoginView from '../../views/LoginView'; +import RegisterView from '../../views/RegisterView'; const PublicRoutes = StackNavigator( { @@ -36,6 +37,12 @@ const PublicRoutes = StackNavigator( navigationOptions: { title: 'Login' } + }, + Register: { + screen: RegisterView, + navigationOptions: { + title: 'Register' + } } }, { diff --git a/app/lib/realm.js b/app/lib/realm.js index 5ae585024..fb82651be 100644 --- a/app/lib/realm.js +++ b/app/lib/realm.js @@ -1,5 +1,5 @@ import Realm from 'realm'; -// import { AsyncStorage } from 'react-native'; +import { AsyncStorage } from 'react-native'; const serversSchema = { name: 'servers', diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 71ddb447d..a189fdd1b 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -122,6 +122,21 @@ const RocketChat = { }); }, + register(params, callback) { + return new Promise((resolve, reject) => { + return Meteor.call('registerUser', params, (err, result) => { + if (err) { + reject(err); + } else { + resolve(result); + } + if (typeof callback === 'function') { + callback(err, result); + } + }); + }); + }, + loginWithPassword({ username, password, code }, callback) { let params = {}; const state = reduxStore.getState(); diff --git a/app/presentation/KeyboardView.js b/app/presentation/KeyboardView.js index 3a6ea5f66..56d70988b 100644 --- a/app/presentation/KeyboardView.js +++ b/app/presentation/KeyboardView.js @@ -1,10 +1,11 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { KeyboardAvoidingView } from 'react-native'; +import { ViewPropTypes } from 'react-native'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; export default class KeyboardView extends React.PureComponent { static propTypes = { - style: KeyboardAvoidingView.propTypes.style, + style: ViewPropTypes.style, keyboardVerticalOffset: PropTypes.number, children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.node), @@ -14,9 +15,15 @@ export default class KeyboardView extends React.PureComponent { render() { return ( - + {this.props.children} - + ); } } diff --git a/app/reducers/login.js b/app/reducers/login.js index 72f36d525..02dd72fd6 100644 --- a/app/reducers/login.js +++ b/app/reducers/login.js @@ -40,6 +40,20 @@ export default function login(state = initialState, action) { token: action.token, user: action.user }; + case types.LOGIN.REGISTER_SUBMIT: + return { + ...state, + isFetching: true, + isAuthenticated: false, + failure: false + }; + case types.LOGIN.REGISTER_SUCCESS: + return { + ...state, + isFetching: false, + isAuthenticated: false, + failure: false + }; default: return state; } diff --git a/app/sagas/login.js b/app/sagas/login.js index 780655e7e..1298e249c 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -1,13 +1,14 @@ import { AsyncStorage } from 'react-native'; import { take, put, call, takeEvery, select, all, race } from 'redux-saga/effects'; import * as types from '../actions/actionsTypes'; -import { loginRequest, loginSuccess, loginFailure, setToken, logout } from '../actions/login'; +import { loginRequest, registerRequest, loginSuccess, registerSuccess, loginFailure, setToken, logout } from '../actions/login'; import RocketChat from '../lib/rocketchat'; const TOKEN_KEY = 'reactnativemeteor_usertoken'; const getUser = state => state.login; const getServer = state => state.server.server; const loginCall = args => (args.resume ? RocketChat.login(args) : RocketChat.loginWithPassword(args)); +const registerCall = args => RocketChat.register(args); const getToken = function* getToken() { const currentServer = yield select(getServer); @@ -86,10 +87,31 @@ const handleLoginSubmit = function* handleLoginSubmit({ credentials }) { }); }; +const handleRegisterRequest = function* handleRegisterRequest({ credentials }) { + try { + yield call(registerCall, credentials); + yield put(registerSuccess()); + } catch (err) { + yield put(loginFailure(err)); + } +}; + +const handleRegisterSubmit = function* handleRegisterSubmit({ credentials }) { + // put a login request + yield put(registerRequest(credentials)); + // wait for a response + yield race({ + success: take(types.LOGIN.SUCCESS), + error: take(types.LOGIN.FAILURE) + }); +}; + const root = function* root() { yield takeEvery(types.SERVER.CHANGED, handleLoginWhenServerChanges); yield takeEvery(types.LOGIN.REQUEST, handleLoginRequest); yield takeEvery(types.LOGIN.SUCCESS, saveToken); yield takeEvery(types.LOGIN.SUBMIT, handleLoginSubmit); + yield takeEvery(types.LOGIN.REGISTER_REQUEST, handleRegisterRequest); + yield takeEvery(types.LOGIN.REGISTER_SUBMIT, handleRegisterSubmit); }; export default root; diff --git a/app/views/LoginView.js b/app/views/LoginView.js index 7f182b7cb..637bf5fd2 100644 --- a/app/views/LoginView.js +++ b/app/views/LoginView.js @@ -18,7 +18,8 @@ class LoginView extends React.Component { loginSubmit: PropTypes.func.isRequired, Accounts_EmailOrUsernamePlaceholder: PropTypes.string, Accounts_PasswordPlaceholder: PropTypes.string, - login: PropTypes.object + login: PropTypes.object, + navigation: PropTypes.object.isRequired } static navigationOptions = () => ({ @@ -44,6 +45,10 @@ class LoginView extends React.Component { Keyboard.dismiss(); } + register = () => { + this.props.navigation.navigate('Register'); + } + renderTOTP = () => { if (this.props.login.errorMessage && this.props.login.errorMessage.error === 'totp-required') { return ( @@ -68,7 +73,7 @@ class LoginView extends React.Component { @@ -82,7 +87,6 @@ class LoginView extends React.Component { autoCorrect={false} returnKeyType='next' autoCapitalize='none' - autoFocus underlineColorAndroid='transparent' onSubmitEditing={() => { this.password.focus(); }} @@ -102,10 +106,17 @@ class LoginView extends React.Component { onSubmitEditing={this.submit} placeholder={this.props.Accounts_PasswordPlaceholder || 'Password'} /> + {this.renderTOTP()} + LOGIN + + + REGISTER + + {this.props.login.error && {this.props.login.error}} diff --git a/app/views/RegisterView.js b/app/views/RegisterView.js new file mode 100644 index 000000000..90a0801dc --- /dev/null +++ b/app/views/RegisterView.js @@ -0,0 +1,184 @@ +import React from 'react'; + +import Spinner from 'react-native-loading-spinner-overlay'; + +import PropTypes from 'prop-types'; +import { Keyboard, Text, TextInput, View, Image, TouchableOpacity } from 'react-native'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +// import * as actions from '../actions'; +import * as loginActions from '../actions/login'; +import KeyboardView from '../presentation/KeyboardView'; +// import { Keyboard } from 'react-native' + +import styles from './Styles'; + +class RegisterView extends React.Component { + static propTypes = { + registerSubmit: PropTypes.func.isRequired, + Accounts_NamePlaceholder: PropTypes.string, + Accounts_EmailOrUsernamePlaceholder: PropTypes.string, + Accounts_PasswordPlaceholder: PropTypes.string, + Accounts_UsernamePlaceholder: PropTypes.string, + Accounts_RepeatPasswordPlaceholder: PropTypes.string, + login: PropTypes.object, + navigation: PropTypes.object.isRequired + } + + constructor(props) { + super(props); + + this.state = { + name: '', + username: '', + email: '', + password: '', + confirmPassword: '' + }; + } + + componentDidUpdate(prevProps) { + if (!this.props.login.isFetching && prevProps.login.isFetching && + !this.props.login.failure) { + this.props.navigation.goBack(); + } + } + + submit = () => { + const { name, username, email, password, confirmPassword, code } = this.state; + if (name.trim() === '' || email.trim() === '' || username.trim() === '' || + password.trim() === '' || confirmPassword.trim() === '') { + return; + } + + this.props.registerSubmit({ name, username, email, pass: password, code }); + Keyboard.dismiss(); + } + + renderTOTP = () => { + if (this.props.login.errorMessage && this.props.login.errorMessage.error === 'totp-required') { + return ( + this.codeInput = ref} + style={styles.input} + onChangeText={code => this.setState({ code })} + keyboardType='numeric' + autoCorrect={false} + returnKeyType='done' + autoCapitalize='none' + onSubmitEditing={this.submit} + placeholder='Code' + /> + ); + } + } + + // {this.props.login.isFetching && LOGANDO} + render() { + return ( + + + + { this.name = e; }} + placeholderTextColor={'rgba(255,255,255,.2)'} + style={styles.input} + onChangeText={name => this.setState({ name })} + autoCorrect={false} + returnKeyType='next' + autoCapitalize='words' + + underlineColorAndroid='transparent' + onSubmitEditing={() => { this.username.focus(); }} + placeholder={this.props.Accounts_NamePlaceholder || 'Name'} + /> + + { this.username = e; }} + placeholderTextColor={'rgba(255,255,255,.2)'} + style={styles.input} + onChangeText={username => this.setState({ username })} + autoCorrect={false} + returnKeyType='next' + autoCapitalize='none' + + underlineColorAndroid='transparent' + onSubmitEditing={() => { this.email.focus(); }} + placeholder={this.props.Accounts_UsernamePlaceholder || 'Username'} + /> + + { this.email = e; }} + placeholderTextColor={'rgba(255,255,255,.2)'} + style={styles.input} + onChangeText={email => this.setState({ email })} + keyboardType='email-address' + autoCorrect={false} + returnKeyType='next' + autoCapitalize='none' + + underlineColorAndroid='transparent' + onSubmitEditing={() => { this.password.focus(); }} + placeholder={this.props.Accounts_EmailOrUsernamePlaceholder || 'Email'} + /> + { this.password = e; }} + placeholderTextColor={'rgba(255,255,255,.2)'} + style={styles.input} + onChangeText={password => this.setState({ password })} + secureTextEntry + autoCorrect={false} + returnKeyType='next' + autoCapitalize='none' + + underlineColorAndroid='transparent' + onSubmitEditing={() => { this.confirmPassword.focus(); }} + placeholder={this.props.Accounts_PasswordPlaceholder || 'Password'} + /> + { this.confirmPassword = e; }} + placeholderTextColor={'rgba(255,255,255,.2)'} + style={styles.input} + onChangeText={confirmPassword => this.setState({ confirmPassword })} + secureTextEntry + autoCorrect={false} + returnKeyType='done' + autoCapitalize='none' + + underlineColorAndroid='transparent' + onSubmitEditing={this.submit} + placeholder={this.props.Accounts_RepeatPasswordPlaceholder || 'Repeat Password'} + /> + + {this.renderTOTP()} + + + REGISTER + + + {this.props.login.error && {this.props.login.error}} + + + + + ); + } +} + +function mapStateToProps(state) { + // console.log(Object.keys(state)); + return { + server: state.server.server, + Accounts_UsernamePlaceholder: state.settings.Accounts_UsernamePlaceholder, + Accounts_EmailOrUsernamePlaceholder: state.settings.Accounts_EmailOrUsernamePlaceholder, + Accounts_PasswordPlaceholder: state.settings.Accounts_PasswordPlaceholder, + Accounts_RepeatPasswordPlaceholder: state.settings.Accounts_RepeatPasswordPlaceholder, + login: state.login + }; +} + +function mapDispatchToProps(dispatch) { + return bindActionCreators(loginActions, dispatch); +} + +export default connect(mapStateToProps, mapDispatchToProps)(RegisterView); diff --git a/app/views/Styles.js b/app/views/Styles.js index c4c040baa..db4792a50 100644 --- a/app/views/Styles.js +++ b/app/views/Styles.js @@ -39,6 +39,12 @@ export default StyleSheet.create({ borderRadius: 5, resizeMode: 'contain' }, + loginLogo: { + width: Dimensions.get('window').width - 80, + height: Dimensions.get('window').width - 80, + borderRadius: 5, + resizeMode: 'contain' + }, formContainer: { // marginBottom: 20 }, diff --git a/package.json b/package.json index 65074e984..dc38caca9 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "react-native-fetch-blob": "^0.10.8", "react-native-image-picker": "^0.26.4", "react-native-img-cache": "^1.4.0", + "react-native-keyboard-aware-scroll-view": "^0.3.0", "react-native-loading-spinner-overlay": "^0.5.2", "react-native-meteor": "^1.1.0", "react-native-modal": "^3.1.0", diff --git a/yarn.lock b/yarn.lock index 8ad749b55..aa5ba2d71 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2055,7 +2055,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: safe-buffer "^5.0.1" sha.js "^2.4.8" -create-react-class@^15.5.2: +create-react-class@^15.5.2, create-react-class@^15.6.0: version "15.6.0" resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.0.tgz#ab448497c26566e1e29413e883207d57cfe7bed4" dependencies: @@ -5729,6 +5729,14 @@ react-native-img-cache@^1.4.0: dependencies: crypto-js "^3.1.9-1" +react-native-keyboard-aware-scroll-view@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/react-native-keyboard-aware-scroll-view/-/react-native-keyboard-aware-scroll-view-0.3.0.tgz#b9d7b0d5b47d2bb4285fe50a3d274b10a3b5e1a7" + dependencies: + create-react-class "^15.6.0" + prop-types "^15.5.10" + react-timer-mixin "^0.13.3" + react-native-loading-spinner-overlay@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/react-native-loading-spinner-overlay/-/react-native-loading-spinner-overlay-0.5.2.tgz#b7bcd277476d596615fd7feee601789f9bdc7acc" @@ -5943,7 +5951,7 @@ react-test-renderer@16.0.0-alpha.12: fbjs "^0.8.9" object-assign "^4.1.0" -react-timer-mixin@^0.13.2: +react-timer-mixin@^0.13.2, react-timer-mixin@^0.13.3: version "0.13.3" resolved "https://registry.yarnpkg.com/react-timer-mixin/-/react-timer-mixin-0.13.3.tgz#0da8b9f807ec07dc3e854d082c737c65605b3d22"