Register user (#44)

* Added feature to register a new user

* login after register working

* Removed username from register and placed on a new view

* loading indicator on username submit

* register/username logo layout issue

* - login and register background white

* - logo removed from logo and register
This commit is contained in:
gilmarsquinelato 2017-11-07 14:28:02 -02:00 committed by Guilherme Gazzo
parent 335f299d6d
commit 125b880760
20 changed files with 580 additions and 131 deletions

View File

@ -9,7 +9,17 @@ function createRequestTypes(base, types = defaultTypes) {
} }
// Login events // 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',
'SET_USERNAME_SUBMIT',
'SET_USERNAME_REQUEST',
'SET_USERNAME_SUCCESS'
]);
export const ROOMS = createRequestTypes('ROOMS'); export const ROOMS = createRequestTypes('ROOMS');
export const APP = createRequestTypes('APP', ['READY', 'INIT']); export const APP = createRequestTypes('APP', ['READY', 'INIT']);
export const MESSAGES = createRequestTypes('MESSAGES'); export const MESSAGES = createRequestTypes('MESSAGES');

View File

@ -13,6 +13,46 @@ 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(credentials) {
return {
type: types.LOGIN.REGISTER_SUCCESS,
credentials
};
}
export function setUsernameSubmit(credentials) {
return {
type: types.LOGIN.SET_USERNAME_SUBMIT,
credentials
};
}
export function setUsernameRequest(credentials) {
return {
type: types.LOGIN.SET_USERNAME_REQUEST,
credentials
};
}
export function setUsernameSuccess() {
return {
type: types.LOGIN.SET_USERNAME_SUCCESS
};
}
export function loginSuccess(user) { export function loginSuccess(user) {
return { return {
type: types.LOGIN.SUCCESS, type: types.LOGIN.SUCCESS,

View File

@ -1,15 +1,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { View, Image } from 'react-native';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import * as Animatable from 'react-native-animatable';
import { appInit } from '../actions'; import { appInit } from '../actions';
import styles from '../views/Styles';
import AuthRoutes from './routes/AuthRoutes'; import AuthRoutes from './routes/AuthRoutes';
import PublicRoutes from './routes/PublicRoutes'; import PublicRoutes from './routes/PublicRoutes';
import Loading from '../presentation/Loading';
@connect( @connect(
state => ({ state => ({
@ -34,21 +31,10 @@ export default class Routes extends React.Component {
const { login, app } = this.props; const { login, app } = this.props;
if (app.starting) { if (app.starting) {
return ( return (<Loading />);
<View style={styles.logoContainer}>
<Animatable.Text
animation='pulse'
easing='ease-out'
iterationCount='infinite'
style={{ textAlign: 'center' }}
>
<Image style={styles.logo} source={require('../images/logo.png')} />
</Animatable.Text>
</View>
);
} }
if ((login.token && !login.failure) || app.ready) { if ((login.token && !login.failure && !login.isRegistering) || app.ready) {
return (<AuthRoutes />); return (<AuthRoutes />);
} }

View File

@ -1,7 +1,5 @@
import React from 'react'; import React from 'react';
import { Button } from 'react-native'; import { StackNavigator, DrawerNavigator, NavigationActions, HeaderBackButton } from 'react-navigation';
import { StackNavigator, DrawerNavigator, NavigationActions } from 'react-navigation';
// import { Platform } from 'react-native';
import Sidebar from '../../containers/Sidebar'; import Sidebar from '../../containers/Sidebar';
import DrawerMenuButton from '../../presentation/DrawerMenuButton'; import DrawerMenuButton from '../../presentation/DrawerMenuButton';
@ -39,7 +37,10 @@ const AuthRoutes = StackNavigator(
return { return {
title: navigation.state.params.title || 'Room', title: navigation.state.params.title || 'Room',
headerLeft: ( headerLeft: (
<Button title={'Back'} onPress={() => backToScreen(navigation, 'RoomsList')} /> <HeaderBackButton
title={'Back'}
onPress={() => backToScreen(navigation, 'RoomsList')}
/>
) )
// [drawerIconPosition]: (<DrawerMenuButton navigation={navigation} />)÷ // [drawerIconPosition]: (<DrawerMenuButton navigation={navigation} />)÷
}; };

View File

@ -6,6 +6,7 @@ import Icon from 'react-native-vector-icons/FontAwesome';
import ListServerView from '../../views/ListServerView'; import ListServerView from '../../views/ListServerView';
import NewServerView from '../../views/NewServerView'; import NewServerView from '../../views/NewServerView';
import LoginView from '../../views/LoginView'; import LoginView from '../../views/LoginView';
import RegisterView from '../../views/RegisterView';
const PublicRoutes = StackNavigator( const PublicRoutes = StackNavigator(
{ {
@ -36,6 +37,12 @@ const PublicRoutes = StackNavigator(
navigationOptions: { navigationOptions: {
title: 'Login' title: 'Login'
} }
},
Register: {
screen: RegisterView,
navigationOptions: {
title: 'Register'
}
} }
}, },
{ {

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -122,6 +122,28 @@ const RocketChat = {
}); });
}, },
register({ credentials }) {
return new Promise((resolve, reject) => {
Meteor.call('registerUser', credentials, (err, userId) => {
if (err) {
reject(err);
}
resolve(userId);
});
});
},
setUsername({ credentials }) {
return new Promise((resolve, reject) => {
Meteor.call('setUsername', credentials.username, (err, result) => {
if (err) {
reject(err);
}
resolve(result);
});
});
},
loginWithPassword({ username, password, code }, callback) { loginWithPassword({ username, password, code }, callback) {
let params = {}; let params = {};
const state = reduxStore.getState(); const state = reduxStore.getState();

View File

@ -1,11 +1,14 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; 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 { export default class KeyboardView extends React.PureComponent {
static propTypes = { static propTypes = {
style: KeyboardAvoidingView.propTypes.style, style: ViewPropTypes.style,
contentContainerStyle: ViewPropTypes.style,
keyboardVerticalOffset: PropTypes.number, keyboardVerticalOffset: PropTypes.number,
scrollEnabled: PropTypes.bool,
children: PropTypes.oneOfType([ children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node), PropTypes.arrayOf(PropTypes.node),
PropTypes.node PropTypes.node
@ -14,9 +17,18 @@ export default class KeyboardView extends React.PureComponent {
render() { render() {
return ( return (
<KeyboardAvoidingView style={this.props.style} behavior='padding' keyboardVerticalOffset={this.props.keyboardVerticalOffset}> <KeyboardAwareScrollView
keyboardDismissMode='interactive'
keyboardShouldPersistTaps='always'
style={this.props.style}
contentContainerStyle={this.props.contentContainerStyle}
scrollEnabled={this.props.scrollEnabled}
alwaysBounceVertical={false}
extraHeight={this.props.keyboardVerticalOffset}
behavior='position'
>
{this.props.children} {this.props.children}
</KeyboardAvoidingView> </KeyboardAwareScrollView>
); );
} }
} }

View File

@ -0,0 +1,77 @@
import React, { Component } from 'react';
import { View, StyleSheet, Animated, Dimensions } from 'react-native';
const logo = require('../images/logo.png');
const styles = StyleSheet.create({
container: {
flex: 1,
width: '100%',
alignItems: 'center',
justifyContent: 'center'
},
background: {
width: Dimensions.get('window').width,
height: Dimensions.get('window').height,
alignItems: 'center',
justifyContent: 'center'
},
logo: {
width: Dimensions.get('window').width - 100,
height: Dimensions.get('window').width - 100,
resizeMode: 'contain'
}
});
export default class Loading extends Component {
constructor(props) {
super(props);
this.scale = new Animated.Value(1.0);
}
componentDidMount() {
requestAnimationFrame(() => {
this.animate();
});
}
animate = () => {
Animated.sequence([
Animated.timing(
this.scale,
{
toValue: 0.8,
duration: 1000,
useNativeDriver: true
}),
Animated.timing(
this.scale,
{
toValue: 1,
duration: 1000,
useNativeDriver: true
})
]).start(() => {
this.animate();
});
}
render() {
return (
<View style={styles.container}>
<Animated.Image
style={[
styles.logo,
{
transform: [
{ scale: this.scale }
]
}]}
source={logo}
/>
</View>
);
}
}

View File

@ -3,19 +3,20 @@ import * as types from '../actions/actionsTypes';
const initialState = { const initialState = {
isAuthenticated: false, isAuthenticated: false,
isFetching: false, isFetching: false,
isRegistering: false,
token: '', token: '',
user: {}, user: {},
errorMessage: '' error: ''
}; };
export default function login(state = initialState, action) { export default function login(state = initialState, action) {
switch (action.type) { switch (action.type) {
case types.LOGIN.REQUEST: case types.LOGIN.REQUEST:
console.log('types.LOGIN.REQUEST', action);
return { ...state, return { ...state,
isFetching: true, isFetching: true,
isAuthenticated: false, isAuthenticated: false,
failure: false failure: false,
error: ''
}; };
case types.LOGIN.SUCCESS: case types.LOGIN.SUCCESS:
return { ...state, return { ...state,
@ -23,15 +24,15 @@ export default function login(state = initialState, action) {
isAuthenticated: true, isAuthenticated: true,
user: action.user, user: action.user,
token: action.user.token, token: action.user.token,
failure: false failure: false,
// user: action.user error: ''
}; };
case types.LOGIN.FAILURE: case types.LOGIN.FAILURE:
return { ...state, return { ...state,
isFetching: false, isFetching: false,
isAuthenticated: false, isAuthenticated: false,
failure: true, failure: true,
errorMessage: action.err error: action.err
}; };
case types.LOGOUT: case types.LOGOUT:
return initialState; return initialState;
@ -40,6 +41,34 @@ export default function login(state = initialState, action) {
token: action.token, token: action.token,
user: action.user user: action.user
}; };
case types.LOGIN.REGISTER_SUBMIT:
return {
...state,
isFetching: true,
isAuthenticated: false,
isRegistering: true,
failure: false,
error: ''
};
case types.LOGIN.REGISTER_SUCCESS:
return {
...state,
isFetching: false,
isAuthenticated: false,
failure: false,
error: ''
};
case types.LOGIN.SET_USERNAME_SUBMIT:
return {
...state,
isFetching: true
};
case types.LOGIN.SET_USERNAME_SUCCESS:
return {
...state,
isFetching: false,
isRegistering: false
};
default: default:
return state; return state;
} }

View File

@ -1,13 +1,26 @@
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import { take, put, call, takeEvery, select, all, race } from 'redux-saga/effects'; import { take, put, call, takeEvery, takeLatest, select, all } from 'redux-saga/effects';
import * as types from '../actions/actionsTypes'; import * as types from '../actions/actionsTypes';
import { loginRequest, loginSuccess, loginFailure, setToken, logout } from '../actions/login'; import {
loginRequest,
loginSubmit,
registerRequest,
loginSuccess,
loginFailure,
setToken,
logout,
registerSuccess,
setUsernameRequest,
setUsernameSuccess
} from '../actions/login';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
const TOKEN_KEY = 'reactnativemeteor_usertoken'; const TOKEN_KEY = 'reactnativemeteor_usertoken';
const getUser = state => state.login; const getUser = state => state.login;
const getServer = state => state.server.server; const getServer = state => state.server.server;
const loginCall = args => (args.resume ? RocketChat.login(args) : RocketChat.loginWithPassword(args)); const loginCall = args => (args.resume ? RocketChat.login(args) : RocketChat.loginWithPassword(args));
const registerCall = args => RocketChat.register(args);
const setUsernameCall = args => RocketChat.setUsername(args);
const getToken = function* getToken() { const getToken = function* getToken() {
const currentServer = yield select(getServer); const currentServer = yield select(getServer);
@ -77,19 +90,57 @@ const handleLoginRequest = function* handleLoginRequest({ credentials }) {
}; };
const handleLoginSubmit = function* handleLoginSubmit({ credentials }) { const handleLoginSubmit = function* handleLoginSubmit({ credentials }) {
// put a login request
yield put(loginRequest(credentials)); yield put(loginRequest(credentials));
};
const handleRegisterSubmit = function* handleRegisterSubmit({ credentials }) {
// put a login request
yield put(registerRequest(credentials));
// wait for a response // wait for a response
yield race({ // yield race({
success: take(types.LOGIN.SUCCESS), // success: take(types.LOGIN.REGISTER_SUCCESS),
error: take(types.LOGIN.FAILURE) // error: take(types.LOGIN.FAILURE)
}); // });
};
const handleRegisterRequest = function* handleRegisterRequest({ credentials }) {
try {
yield call(registerCall, { credentials });
yield put(registerSuccess(credentials));
} catch (err) {
yield put(loginFailure(err));
}
};
const handleRegisterSuccess = function* handleRegisterSuccess({ credentials }) {
yield put(loginSubmit({
username: credentials.email,
password: credentials.pass
}));
};
const handleSetUsernameSubmit = function* handleSetUsernameSubmit({ credentials }) {
yield put(setUsernameRequest(credentials));
};
const handleSetUsernameRequest = function* handleSetUsernameRequest({ credentials }) {
try {
yield call(setUsernameCall, { credentials });
yield put(setUsernameSuccess());
} catch (err) {
yield put(loginFailure(err));
}
}; };
const root = function* root() { const root = function* root() {
yield takeEvery(types.SERVER.CHANGED, handleLoginWhenServerChanges); yield takeEvery(types.SERVER.CHANGED, handleLoginWhenServerChanges);
yield takeEvery(types.LOGIN.REQUEST, handleLoginRequest); yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest);
yield takeEvery(types.LOGIN.SUCCESS, saveToken); yield takeLatest(types.LOGIN.SUCCESS, saveToken);
yield takeEvery(types.LOGIN.SUBMIT, handleLoginSubmit); yield takeLatest(types.LOGIN.SUBMIT, handleLoginSubmit);
yield takeLatest(types.LOGIN.REGISTER_REQUEST, handleRegisterRequest);
yield takeLatest(types.LOGIN.REGISTER_SUBMIT, handleRegisterSubmit);
yield takeLatest(types.LOGIN.REGISTER_SUCCESS, handleRegisterSuccess);
yield takeLatest(types.LOGIN.SET_USERNAME_SUBMIT, handleSetUsernameSubmit);
yield takeLatest(types.LOGIN.SET_USERNAME_REQUEST, handleSetUsernameRequest);
}; };
export default root; export default root;

View File

@ -32,7 +32,7 @@ const validateServer = function* validateServer({ server }) {
}; };
const addServer = function* addServer({ server }) { const addServer = function* addServer({ server }) {
yield call(serverRequest, server); yield put(serverRequest(server));
const { error } = yield race({ const { error } = yield race({
error: take(SERVER.FAILURE), error: take(SERVER.FAILURE),

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { TextInput, View, Text, Switch, TouchableOpacity, ScrollView } from 'react-native'; import { TextInput, View, Text, Switch, TouchableOpacity } from 'react-native';
import { createChannelRequest } from '../actions/createChannel'; import { createChannelRequest } from '../actions/createChannel';
import styles from './Styles'; import styles from './Styles';
import KeyboardView from '../presentation/KeyboardView'; import KeyboardView from '../presentation/KeyboardView';
@ -90,55 +90,56 @@ export default class CreateChannelView extends React.Component {
render() { render() {
return ( return (
<KeyboardView style={[styles.view_white, { flex: 1, justifyContent: 'flex-start' }]}> <KeyboardView
<ScrollView> style={[styles.defaultViewBackground, { flex: 1 }]}
<View style={styles.formContainer}> contentContainerStyle={styles.defaultView}
<Text style={styles.label_white}>Channel Name</Text> >
<TextInput <View style={styles.formContainer}>
value={this.state.channelName} <Text style={styles.label_white}>Channel Name</Text>
style={styles.input_white} <TextInput
onChangeText={channelName => this.setState({ channelName })} value={this.state.channelName}
autoCorrect={false} style={styles.input_white}
returnKeyType='done' onChangeText={channelName => this.setState({ channelName })}
autoCapitalize='none' autoCorrect={false}
autoFocus returnKeyType='done'
// onSubmitEditing={() => this.textInput.focus()} autoCapitalize='none'
placeholder='Type the channel name here' autoFocus
/> // onSubmitEditing={() => this.textInput.focus()}
{this.renderChannelNameError()} placeholder='Type the channel name here'
{this.renderTypeSwitch()} />
<Text {this.renderChannelNameError()}
style={[ {this.renderTypeSwitch()}
styles.label_white, <Text
{ style={[
color: '#9ea2a8', styles.label_white,
flexGrow: 1, {
paddingHorizontal: 0, color: '#9ea2a8',
marginBottom: 20 flexGrow: 1,
} paddingHorizontal: 0,
]} marginBottom: 20
> }
{this.state.type ? ( ]}
'Everyone can access this channel' >
) : ( {this.state.type ? (
'Just invited people can access this channel' 'Everyone can access this channel'
)} ) : (
'Just invited people can access this channel'
)}
</Text>
<TouchableOpacity
onPress={() => this.submit()}
style={[
styles.buttonContainer_white,
this.state.channelName.length === 0 || this.props.result.isFetching
? styles.disabledButton
: styles.enabledButton
]}
>
<Text style={styles.button_white}>
{this.props.result.isFetching ? 'LOADING' : 'CREATE'}!
</Text> </Text>
<TouchableOpacity </TouchableOpacity>
onPress={() => this.submit()} </View>
style={[
styles.buttonContainer_white,
this.state.channelName.length === 0 || this.props.result.isFetching
? styles.disabledButton
: styles.enabledButton
]}
>
<Text style={styles.button_white}>
{this.props.result.isFetching ? 'LOADING' : 'CREATE'}!
</Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardView> </KeyboardView>
); );
} }

View File

@ -3,7 +3,7 @@ import React from 'react';
import Spinner from 'react-native-loading-spinner-overlay'; import Spinner from 'react-native-loading-spinner-overlay';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Keyboard, Text, TextInput, View, Image, TouchableOpacity } from 'react-native'; import { Keyboard, Text, TextInput, View, TouchableOpacity } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import { bindActionCreators } from 'redux';
// import * as actions from '../actions'; // import * as actions from '../actions';
@ -18,7 +18,8 @@ class LoginView extends React.Component {
loginSubmit: PropTypes.func.isRequired, loginSubmit: PropTypes.func.isRequired,
Accounts_EmailOrUsernamePlaceholder: PropTypes.string, Accounts_EmailOrUsernamePlaceholder: PropTypes.string,
Accounts_PasswordPlaceholder: PropTypes.string, Accounts_PasswordPlaceholder: PropTypes.string,
login: PropTypes.object login: PropTypes.object,
navigation: PropTypes.object.isRequired
} }
static navigationOptions = () => ({ static navigationOptions = () => ({
@ -44,6 +45,10 @@ class LoginView extends React.Component {
Keyboard.dismiss(); Keyboard.dismiss();
} }
register = () => {
this.props.navigation.navigate('Register');
}
renderTOTP = () => { renderTOTP = () => {
if (this.props.login.errorMessage && this.props.login.errorMessage.error === 'totp-required') { if (this.props.login.errorMessage && this.props.login.errorMessage.error === 'totp-required') {
return ( return (
@ -65,48 +70,47 @@ class LoginView extends React.Component {
// {this.props.login.isFetching && <Text> LOGANDO</Text>} // {this.props.login.isFetching && <Text> LOGANDO</Text>}
render() { render() {
return ( return (
<KeyboardView style={styles.container} keyboardVerticalOffset={128}> <KeyboardView
<View style={{ alignItems: 'center' }}> contentContainerStyle={styles.container}
<Image keyboardVerticalOffset={128}
style={styles.logo} >
source={require('../images/logo.png')}
/>
</View>
<View style={styles.loginView}> <View style={styles.loginView}>
<View style={styles.formContainer}> <View style={styles.formContainer}>
<TextInput <TextInput
placeholderTextColor={'rgba(255,255,255,.2)'} style={styles.input_white}
style={styles.input}
onChangeText={username => this.setState({ username })} onChangeText={username => this.setState({ username })}
keyboardType='email-address' keyboardType='email-address'
autoCorrect={false} autoCorrect={false}
returnKeyType='next' returnKeyType='next'
autoCapitalize='none' autoCapitalize='none'
autoFocus
underlineColorAndroid='transparent' underlineColorAndroid='transparent'
onSubmitEditing={() => { this.password.focus(); }} onSubmitEditing={() => { this.password.focus(); }}
placeholder={this.props.Accounts_EmailOrUsernamePlaceholder || 'Email or username'} placeholder={this.props.Accounts_EmailOrUsernamePlaceholder || 'Email or username'}
/> />
<TextInput <TextInput
ref={(e) => { this.password = e; }} ref={(e) => { this.password = e; }}
placeholderTextColor={'rgba(255,255,255,.2)'} style={styles.input_white}
style={styles.input}
onChangeText={password => this.setState({ password })} onChangeText={password => this.setState({ password })}
secureTextEntry secureTextEntry
autoCorrect={false} autoCorrect={false}
returnKeyType='done' returnKeyType='done'
autoCapitalize='none' autoCapitalize='none'
underlineColorAndroid='transparent' underlineColorAndroid='transparent'
onSubmitEditing={this.submit} onSubmitEditing={this.submit}
placeholder={this.props.Accounts_PasswordPlaceholder || 'Password'} placeholder={this.props.Accounts_PasswordPlaceholder || 'Password'}
/> />
{this.renderTOTP()} {this.renderTOTP()}
<TouchableOpacity style={styles.buttonContainer}> <TouchableOpacity style={styles.buttonContainer}>
<Text style={styles.button} onPress={this.submit}>LOGIN</Text> <Text style={styles.button} onPress={this.submit}>LOGIN</Text>
</TouchableOpacity> </TouchableOpacity>
{this.props.login.error && <Text style={styles.error}>{this.props.login.error}</Text>}
<TouchableOpacity style={[styles.buttonContainer, styles.registerContainer]}>
<Text style={styles.button} onPress={this.register}>REGISTER</Text>
</TouchableOpacity>
{this.props.login.failure && <Text style={styles.error}>{this.props.login.error.reason}</Text>}
</View> </View>
<Spinner visible={this.props.login.isFetching} textContent={'Loading...'} textStyle={{ color: '#FFF' }} /> <Spinner visible={this.props.login.isFetching} textContent={'Loading...'} textStyle={{ color: '#FFF' }} />
</View> </View>

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Text, TextInput, View, StyleSheet } from 'react-native'; import { Text, TextInput, View, StyleSheet, Dimensions } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { serverRequest, addServer } from '../actions/server'; import { serverRequest, addServer } from '../actions/server';
import KeyboardView from '../presentation/KeyboardView'; import KeyboardView from '../presentation/KeyboardView';
@ -148,7 +148,11 @@ export default class NewServerView extends React.Component {
render() { render() {
return ( return (
<KeyboardView style={styles.view} keyboardVerticalOffset={64}> <KeyboardView
scrollEnabled={false}
contentContainerStyle={[styles.view, { height: Dimensions.get('window').height }]}
keyboardVerticalOffset={128}
>
<View style={styles.spaceView} /> <View style={styles.spaceView} />
<TextInput <TextInput
ref={ref => this.inputElement = ref} ref={ref => this.inputElement = ref}

194
app/views/RegisterView.js Normal file
View File

@ -0,0 +1,194 @@
import React from 'react';
import Spinner from 'react-native-loading-spinner-overlay';
import PropTypes from 'prop-types';
import { Keyboard, Text, TextInput, View, TouchableOpacity } 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';
const placeholderTextColor = 'rgba(255,255,255,.2)';
class RegisterView extends React.Component {
static propTypes = {
registerSubmit: PropTypes.func.isRequired,
setUsernameSubmit: PropTypes.func,
Accounts_UsernamePlaceholder: PropTypes.string,
Accounts_NamePlaceholder: PropTypes.string,
Accounts_EmailOrUsernamePlaceholder: PropTypes.string,
Accounts_PasswordPlaceholder: PropTypes.string,
Accounts_RepeatPasswordPlaceholder: PropTypes.string,
login: PropTypes.object
}
constructor(props) {
super(props);
this.state = {
name: '',
email: '',
password: '',
confirmPassword: ''
};
}
_valid() {
const { name, email, password, confirmPassword } = this.state;
return name.trim() && email.trim() &&
password && confirmPassword && password === confirmPassword;
}
_invalidEmail() {
return this.props.login.failure && /Email/.test(this.props.login.error.reason);
}
submit = () => {
const { name, email, password, code } = this.state;
if (!this._valid()) {
return;
}
this.props.registerSubmit({ name, email, pass: password, code });
Keyboard.dismiss();
}
usernameSubmit = () => {
const { username } = this.state;
if (!username) {
return;
}
this.props.setUsernameSubmit({ username });
Keyboard.dismiss();
}
_renderRegister() {
if (this.props.login.token) {
return null;
}
return (
<View style={styles.formContainer}>
<TextInput
ref={(e) => { this.name = e; }}
style={styles.input_white}
onChangeText={name => this.setState({ name })}
autoCorrect={false}
autoFocus
returnKeyType='next'
autoCapitalize='none'
underlineColorAndroid='transparent'
onSubmitEditing={() => { this.email.focus(); }}
placeholder={this.props.Accounts_NamePlaceholder || 'Name'}
/>
<TextInput
ref={(e) => { this.email = e; }}
style={[styles.input_white, this._invalidEmail() ? { borderColor: 'red' } : {}]}
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'}
/>
<TextInput
ref={(e) => { this.password = e; }}
style={styles.input_white}
onChangeText={password => this.setState({ password })}
secureTextEntry
autoCorrect={false}
returnKeyType='next'
autoCapitalize='none'
underlineColorAndroid='transparent'
onSubmitEditing={() => { this.confirmPassword.focus(); }}
placeholder={this.props.Accounts_PasswordPlaceholder || 'Password'}
/>
<TextInput
ref={(e) => { this.confirmPassword = e; }}
style={[styles.input_white, this.state.password && this.state.confirmPassword && this.state.confirmPassword !== this.state.password ? { borderColor: 'red' } : {}]}
onChangeText={confirmPassword => this.setState({ confirmPassword })}
secureTextEntry
autoCorrect={false}
returnKeyType='done'
autoCapitalize='none'
underlineColorAndroid='transparent'
onSubmitEditing={this.submit}
placeholder={this.props.Accounts_RepeatPasswordPlaceholder || 'Repeat Password'}
/>
<TouchableOpacity style={[styles.buttonContainer, styles.registerContainer]}>
<Text
style={[styles.button, this._valid() ? {}
: { color: placeholderTextColor }
]}
onPress={this.submit}
>REGISTER</Text>
</TouchableOpacity>
{this.props.login.failure && <Text style={styles.error}>{this.props.login.error.reason}</Text>}
</View>
);
}
_renderUsername() {
if (!this.props.login.token) {
return null;
}
return (
<View style={styles.formContainer}>
<TextInput
ref={(e) => { this.username = e; }}
style={styles.input_white}
onChangeText={username => this.setState({ username })}
autoCorrect={false}
returnKeyType='next'
autoCapitalize='none'
underlineColorAndroid='transparent'
onSubmitEditing={() => { this.usernameSubmit(); }}
placeholder={this.props.Accounts_UsernamePlaceholder || 'Username'}
/>
<TouchableOpacity style={[styles.buttonContainer, styles.registerContainer]}>
<Text
style={styles.button}
onPress={this.usernameSubmit}
>REGISTER</Text>
</TouchableOpacity>
{this.props.login.failure && <Text style={styles.error}>{this.props.login.error.reason}</Text>}
</View>
);
}
render() {
return (
<KeyboardView contentContainerStyle={styles.container}>
<View style={styles.loginView}>
{this._renderRegister()}
{this._renderUsername()}
<Spinner visible={this.props.login.isFetching} textContent={'Loading...'} textStyle={{ color: '#FFF' }} />
</View>
</KeyboardView>
);
}
}
function mapStateToProps(state) {
return {
server: state.server.server,
Accounts_NamePlaceholder: state.settings.Accounts_NamePlaceholder,
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);

View File

@ -193,7 +193,7 @@ export default class RoomView extends React.Component {
render() { render() {
return ( return (
<KeyboardView style={styles.container} keyboardVerticalOffset={64}> <KeyboardView contentContainerStyle={styles.container} keyboardVerticalOffset={64}>
{this.renderBanner()} {this.renderBanner()}
<ListView <ListView
enableEmptySections enableEmptySections

View File

@ -2,11 +2,8 @@ import { StyleSheet, Dimensions } from 'react-native';
export default StyleSheet.create({ export default StyleSheet.create({
container: { container: {
flex: 1, backgroundColor: 'white',
backgroundColor: '#2f343d', flex: 1
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'stretch'
}, },
loginView: { loginView: {
padding: 20 padding: 20
@ -19,29 +16,31 @@ export default StyleSheet.create({
alignItems: 'stretch', alignItems: 'stretch',
backgroundColor: '#2f343d' backgroundColor: '#2f343d'
}, },
view_white: { defaultView: {
flex: 1,
flexDirection: 'column', flexDirection: 'column',
justifyContent: 'center', justifyContent: 'center',
padding: 20, padding: 20,
alignItems: 'stretch', alignItems: 'stretch'
},
defaultViewBackground: {
backgroundColor: '#fff' backgroundColor: '#fff'
}, },
logoContainer: { logoContainer: {
flex: 1,
alignItems: 'center', alignItems: 'center',
flexGrow: 1, justifyContent: 'center',
justifyContent: 'center' flex: 1
}, },
logo: { loginLogo: {
width: Dimensions.get('window').width - 30, width: Dimensions.get('window').width - 150,
height: Dimensions.get('window').width - 30, height: Dimensions.get('window').width - 150,
borderRadius: 5,
resizeMode: 'contain' resizeMode: 'contain'
}, },
formContainer: { registerLogo: {
// marginBottom: 20 width: Dimensions.get('window').width - 40,
height: 100,
resizeMode: 'contain'
}, },
formContainer: {},
label: { label: {
lineHeight: 40, lineHeight: 40,
height: 40, height: 40,
@ -94,6 +93,9 @@ export default StyleSheet.create({
backgroundColor: '#1d74f5', backgroundColor: '#1d74f5',
marginBottom: 20 marginBottom: 20
}, },
registerContainer: {
marginBottom: 0
},
button: { button: {
textAlign: 'center', textAlign: 'center',
color: 'white', color: 'white',

View File

@ -28,6 +28,7 @@
"react-native-fetch-blob": "^0.10.8", "react-native-fetch-blob": "^0.10.8",
"react-native-image-picker": "^0.26.4", "react-native-image-picker": "^0.26.4",
"react-native-img-cache": "^1.4.0", "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-loading-spinner-overlay": "^0.5.2",
"react-native-meteor": "^1.1.0", "react-native-meteor": "^1.1.0",
"react-native-modal": "^3.1.0", "react-native-modal": "^3.1.0",

View File

@ -2055,7 +2055,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
sha.js "^2.4.8" 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" version "15.6.0"
resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.0.tgz#ab448497c26566e1e29413e883207d57cfe7bed4" resolved "https://registry.yarnpkg.com/create-react-class/-/create-react-class-15.6.0.tgz#ab448497c26566e1e29413e883207d57cfe7bed4"
dependencies: dependencies:
@ -5729,6 +5729,14 @@ react-native-img-cache@^1.4.0:
dependencies: dependencies:
crypto-js "^3.1.9-1" 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: react-native-loading-spinner-overlay@^0.5.2:
version "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" 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" fbjs "^0.8.9"
object-assign "^4.1.0" 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" version "0.13.3"
resolved "https://registry.yarnpkg.com/react-timer-mixin/-/react-timer-mixin-0.13.3.tgz#0da8b9f807ec07dc3e854d082c737c65605b3d22" resolved "https://registry.yarnpkg.com/react-timer-mixin/-/react-timer-mixin-0.13.3.tgz#0da8b9f807ec07dc3e854d082c737c65605b3d22"