diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index ef66104a60847cf913c9aaae4b208bd48601b06e..308c4baa6f373b7482b749bd61151a52487f8af8 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -9,7 +9,17 @@ 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', + 'SET_USERNAME_SUBMIT', + 'SET_USERNAME_REQUEST', + 'SET_USERNAME_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 7969c80904daa767ba454905b339a668d27264b2..1a8f753e82111b6141b23dc1f4fa996c92c9215b 100644 --- a/app/actions/login.js +++ b/app/actions/login.js @@ -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) { return { type: types.LOGIN.SUCCESS, diff --git a/app/containers/Routes.js b/app/containers/Routes.js index 485dd389d4e1dd5afeb7fa817aa049641a3d9030..83268096c16dbfd4919bce399802199b7f6a8944 100644 --- a/app/containers/Routes.js +++ b/app/containers/Routes.js @@ -1,15 +1,12 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { View, Image } from 'react-native'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import * as Animatable from 'react-native-animatable'; import { appInit } from '../actions'; -import styles from '../views/Styles'; - import AuthRoutes from './routes/AuthRoutes'; import PublicRoutes from './routes/PublicRoutes'; +import Loading from '../presentation/Loading'; @connect( state => ({ @@ -34,21 +31,10 @@ export default class Routes extends React.Component { const { login, app } = this.props; if (app.starting) { - return ( - <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> - ); + return (<Loading />); } - if ((login.token && !login.failure) || app.ready) { + if ((login.token && !login.failure && !login.isRegistering) || app.ready) { return (<AuthRoutes />); } diff --git a/app/containers/routes/AuthRoutes.js b/app/containers/routes/AuthRoutes.js index 6bef854552e4a286be1c8f513e076e04ea9d5389..2c1a9f35e08e41c3f957999c9d522f2f1222aa18 100644 --- a/app/containers/routes/AuthRoutes.js +++ b/app/containers/routes/AuthRoutes.js @@ -1,7 +1,5 @@ import React from 'react'; -import { Button } from 'react-native'; -import { StackNavigator, DrawerNavigator, NavigationActions } from 'react-navigation'; -// import { Platform } from 'react-native'; +import { StackNavigator, DrawerNavigator, NavigationActions, HeaderBackButton } from 'react-navigation'; import Sidebar from '../../containers/Sidebar'; import DrawerMenuButton from '../../presentation/DrawerMenuButton'; @@ -39,7 +37,10 @@ const AuthRoutes = StackNavigator( return { title: navigation.state.params.title || 'Room', headerLeft: ( - <Button title={'Back'} onPress={() => backToScreen(navigation, 'RoomsList')} /> + <HeaderBackButton + title={'Back'} + onPress={() => backToScreen(navigation, 'RoomsList')} + /> ) // [drawerIconPosition]: (<DrawerMenuButton navigation={navigation} />)÷ }; diff --git a/app/containers/routes/PublicRoutes.js b/app/containers/routes/PublicRoutes.js index 3cf851f5cb35453aae285e59e300aa2782fcfc87..a99583cd22a5d86546e98b4b46292cedb7b16f92 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/images/logo_with_text.png b/app/images/logo_with_text.png new file mode 100644 index 0000000000000000000000000000000000000000..623349a06ad0f2735cb64e6601481882ff4aa153 Binary files /dev/null and b/app/images/logo_with_text.png differ diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 71ddb447d8d71a115a19c4b837d92b6c2427766d..352eb00bbe60deccd48899a60a0a0d52982c5af2 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -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) { let params = {}; const state = reduxStore.getState(); diff --git a/app/presentation/KeyboardView.js b/app/presentation/KeyboardView.js index 3a6ea5f66d8b770d6510a20f232a1ec767e8791f..f3e29d9c2c16f178985c2ad3cd7e21fb90b2b1fb 100644 --- a/app/presentation/KeyboardView.js +++ b/app/presentation/KeyboardView.js @@ -1,11 +1,14 @@ 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, + contentContainerStyle: ViewPropTypes.style, keyboardVerticalOffset: PropTypes.number, + scrollEnabled: PropTypes.bool, children: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.node), PropTypes.node @@ -14,9 +17,18 @@ export default class KeyboardView extends React.PureComponent { render() { 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} - </KeyboardAvoidingView> + </KeyboardAwareScrollView> ); } } diff --git a/app/presentation/Loading.js b/app/presentation/Loading.js new file mode 100644 index 0000000000000000000000000000000000000000..ed92f1192169eb6279e33b5c11f457daaec50c17 --- /dev/null +++ b/app/presentation/Loading.js @@ -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> + ); + } +} diff --git a/app/reducers/login.js b/app/reducers/login.js index 72f36d52552cdcc51ae27508844d4cf878f08035..9a3c33de2457398444e09a3ed28cbcda99a3509f 100644 --- a/app/reducers/login.js +++ b/app/reducers/login.js @@ -3,19 +3,20 @@ import * as types from '../actions/actionsTypes'; const initialState = { isAuthenticated: false, isFetching: false, + isRegistering: false, token: '', user: {}, - errorMessage: '' + error: '' }; export default function login(state = initialState, action) { switch (action.type) { case types.LOGIN.REQUEST: - console.log('types.LOGIN.REQUEST', action); return { ...state, isFetching: true, isAuthenticated: false, - failure: false + failure: false, + error: '' }; case types.LOGIN.SUCCESS: return { ...state, @@ -23,15 +24,15 @@ export default function login(state = initialState, action) { isAuthenticated: true, user: action.user, token: action.user.token, - failure: false - // user: action.user + failure: false, + error: '' }; case types.LOGIN.FAILURE: return { ...state, isFetching: false, isAuthenticated: false, failure: true, - errorMessage: action.err + error: action.err }; case types.LOGOUT: return initialState; @@ -40,6 +41,34 @@ export default function login(state = initialState, action) { token: action.token, 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: return state; } diff --git a/app/sagas/login.js b/app/sagas/login.js index 780655e7e2c9d9fdc2ed71cc605456705f658aae..a8608e79d847cbaed8b29f0cb8ac2b8c791e2b10 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -1,13 +1,26 @@ 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 { 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'; 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 setUsernameCall = args => RocketChat.setUsername(args); const getToken = function* getToken() { const currentServer = yield select(getServer); @@ -77,19 +90,57 @@ const handleLoginRequest = function* handleLoginRequest({ credentials }) { }; const handleLoginSubmit = function* handleLoginSubmit({ credentials }) { - // put a login request yield put(loginRequest(credentials)); +}; + +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) - }); + // yield race({ + // success: take(types.LOGIN.REGISTER_SUCCESS), + // 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() { 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 takeLatest(types.LOGIN.REQUEST, handleLoginRequest); + yield takeLatest(types.LOGIN.SUCCESS, saveToken); + 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; diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js index f03f2985f27332c6b15954fb6c68334bdd36bde7..dc9b039ec7ebc72ca957b573daf1049c1368da75 100644 --- a/app/sagas/selectServer.js +++ b/app/sagas/selectServer.js @@ -32,7 +32,7 @@ const validateServer = function* validateServer({ server }) { }; const addServer = function* addServer({ server }) { - yield call(serverRequest, server); + yield put(serverRequest(server)); const { error } = yield race({ error: take(SERVER.FAILURE), diff --git a/app/views/CreateChannelView.js b/app/views/CreateChannelView.js index 01e5bb374d9b44392680b092cc182721f86fbda2..2041f8ab66d4a151e9e751aa5eb247b929da4c7c 100644 --- a/app/views/CreateChannelView.js +++ b/app/views/CreateChannelView.js @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; 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 styles from './Styles'; import KeyboardView from '../presentation/KeyboardView'; @@ -90,55 +90,56 @@ export default class CreateChannelView extends React.Component { render() { return ( - <KeyboardView style={[styles.view_white, { flex: 1, justifyContent: 'flex-start' }]}> - <ScrollView> - <View style={styles.formContainer}> - <Text style={styles.label_white}>Channel Name</Text> - <TextInput - value={this.state.channelName} - style={styles.input_white} - onChangeText={channelName => this.setState({ channelName })} - autoCorrect={false} - returnKeyType='done' - autoCapitalize='none' - autoFocus - // onSubmitEditing={() => this.textInput.focus()} - placeholder='Type the channel name here' - /> - {this.renderChannelNameError()} - {this.renderTypeSwitch()} - <Text - style={[ - styles.label_white, - { - color: '#9ea2a8', - flexGrow: 1, - paddingHorizontal: 0, - marginBottom: 20 - } - ]} - > - {this.state.type ? ( - 'Everyone can access this channel' - ) : ( - 'Just invited people can access this channel' - )} + <KeyboardView + style={[styles.defaultViewBackground, { flex: 1 }]} + contentContainerStyle={styles.defaultView} + > + <View style={styles.formContainer}> + <Text style={styles.label_white}>Channel Name</Text> + <TextInput + value={this.state.channelName} + style={styles.input_white} + onChangeText={channelName => this.setState({ channelName })} + autoCorrect={false} + returnKeyType='done' + autoCapitalize='none' + autoFocus + // onSubmitEditing={() => this.textInput.focus()} + placeholder='Type the channel name here' + /> + {this.renderChannelNameError()} + {this.renderTypeSwitch()} + <Text + style={[ + styles.label_white, + { + color: '#9ea2a8', + flexGrow: 1, + paddingHorizontal: 0, + marginBottom: 20 + } + ]} + > + {this.state.type ? ( + '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> - <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> - </TouchableOpacity> - </View> - </ScrollView> + </TouchableOpacity> + </View> </KeyboardView> ); } diff --git a/app/views/LoginView.js b/app/views/LoginView.js index 7f182b7cbeb76506507771894a5201bf69d5ffd6..2f259c3dc7165d19fe66b515619f22f6086debd1 100644 --- a/app/views/LoginView.js +++ b/app/views/LoginView.js @@ -3,7 +3,7 @@ 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 { Keyboard, Text, TextInput, View, TouchableOpacity } from 'react-native'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; // import * as actions from '../actions'; @@ -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 ( @@ -65,48 +70,47 @@ class LoginView extends React.Component { // {this.props.login.isFetching && <Text> LOGANDO</Text>} render() { return ( - <KeyboardView style={styles.container} keyboardVerticalOffset={128}> - <View style={{ alignItems: 'center' }}> - <Image - style={styles.logo} - source={require('../images/logo.png')} - /> - </View> + <KeyboardView + contentContainerStyle={styles.container} + keyboardVerticalOffset={128} + > <View style={styles.loginView}> <View style={styles.formContainer}> <TextInput - placeholderTextColor={'rgba(255,255,255,.2)'} - style={styles.input} + style={styles.input_white} onChangeText={username => this.setState({ username })} keyboardType='email-address' autoCorrect={false} returnKeyType='next' autoCapitalize='none' - autoFocus - underlineColorAndroid='transparent' onSubmitEditing={() => { this.password.focus(); }} placeholder={this.props.Accounts_EmailOrUsernamePlaceholder || 'Email or username'} /> <TextInput ref={(e) => { this.password = e; }} - placeholderTextColor={'rgba(255,255,255,.2)'} - style={styles.input} + style={styles.input_white} onChangeText={password => this.setState({ password })} secureTextEntry autoCorrect={false} returnKeyType='done' autoCapitalize='none' - underlineColorAndroid='transparent' onSubmitEditing={this.submit} placeholder={this.props.Accounts_PasswordPlaceholder || 'Password'} /> + {this.renderTOTP()} + <TouchableOpacity style={styles.buttonContainer}> <Text style={styles.button} onPress={this.submit}>LOGIN</Text> </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> <Spinner visible={this.props.login.isFetching} textContent={'Loading...'} textStyle={{ color: '#FFF' }} /> </View> diff --git a/app/views/NewServerView.js b/app/views/NewServerView.js index 62ce32643f194f66bb2af8f4dfa40db7ea3e62fb..f022bc4eedc46185cfedef88d64b3833084b3bc5 100644 --- a/app/views/NewServerView.js +++ b/app/views/NewServerView.js @@ -1,6 +1,6 @@ import React from 'react'; 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 { serverRequest, addServer } from '../actions/server'; import KeyboardView from '../presentation/KeyboardView'; @@ -148,7 +148,11 @@ export default class NewServerView extends React.Component { render() { return ( - <KeyboardView style={styles.view} keyboardVerticalOffset={64}> + <KeyboardView + scrollEnabled={false} + contentContainerStyle={[styles.view, { height: Dimensions.get('window').height }]} + keyboardVerticalOffset={128} + > <View style={styles.spaceView} /> <TextInput ref={ref => this.inputElement = ref} diff --git a/app/views/RegisterView.js b/app/views/RegisterView.js new file mode 100644 index 0000000000000000000000000000000000000000..656605727ecf4bf3154d016d40cd838dfda27b48 --- /dev/null +++ b/app/views/RegisterView.js @@ -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); diff --git a/app/views/RoomView.js b/app/views/RoomView.js index 3f6b473cc70e1d818211d9e4fdad002d7be6b168..6d1ee4f98c19867b556f009b8d1e10862d835466 100644 --- a/app/views/RoomView.js +++ b/app/views/RoomView.js @@ -193,7 +193,7 @@ export default class RoomView extends React.Component { render() { return ( - <KeyboardView style={styles.container} keyboardVerticalOffset={64}> + <KeyboardView contentContainerStyle={styles.container} keyboardVerticalOffset={64}> {this.renderBanner()} <ListView enableEmptySections diff --git a/app/views/Styles.js b/app/views/Styles.js index c4c040baa6de6753575baf3fd0a02e338d4887dd..6fb77cb66538d880c3987cdbf608531eddabd694 100644 --- a/app/views/Styles.js +++ b/app/views/Styles.js @@ -2,11 +2,8 @@ import { StyleSheet, Dimensions } from 'react-native'; export default StyleSheet.create({ container: { - flex: 1, - backgroundColor: '#2f343d', - flexDirection: 'column', - justifyContent: 'center', - alignItems: 'stretch' + backgroundColor: 'white', + flex: 1 }, loginView: { padding: 20 @@ -19,29 +16,31 @@ export default StyleSheet.create({ alignItems: 'stretch', backgroundColor: '#2f343d' }, - view_white: { - flex: 1, + defaultView: { flexDirection: 'column', justifyContent: 'center', padding: 20, - alignItems: 'stretch', + alignItems: 'stretch' + }, + defaultViewBackground: { backgroundColor: '#fff' }, logoContainer: { - flex: 1, alignItems: 'center', - flexGrow: 1, - justifyContent: 'center' + justifyContent: 'center', + flex: 1 }, - logo: { - width: Dimensions.get('window').width - 30, - height: Dimensions.get('window').width - 30, - borderRadius: 5, + loginLogo: { + width: Dimensions.get('window').width - 150, + height: Dimensions.get('window').width - 150, resizeMode: 'contain' }, - formContainer: { - // marginBottom: 20 + registerLogo: { + width: Dimensions.get('window').width - 40, + height: 100, + resizeMode: 'contain' }, + formContainer: {}, label: { lineHeight: 40, height: 40, @@ -94,6 +93,9 @@ export default StyleSheet.create({ backgroundColor: '#1d74f5', marginBottom: 20 }, + registerContainer: { + marginBottom: 0 + }, button: { textAlign: 'center', color: 'white', diff --git a/package.json b/package.json index 4a1688e05876bba54cb6158d1ec8b3e655ff44da..2e1dbb94409e7e6943c646f511fb1558c3f89f81 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 8ad749b554319281f3b1f371386bf4a2df2f0ea2..aa5ba2d71262ec38aadece42261906ad935f2c88 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"