diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index 72bc1ffbb112acbb25afe6100046838d32836e71..d0a621d6c514a84ab79e5ab3926a23c48f4ffb35 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -12,10 +12,12 @@ function createRequestTypes(base, types = defaultTypes) { export const LOGIN = createRequestTypes('LOGIN', [ ...defaultTypes, 'SET_TOKEN', + 'RESTORE_TOKEN', 'SUBMIT', 'REGISTER_SUBMIT', 'REGISTER_REQUEST', 'REGISTER_SUCCESS', + 'REGISTER_INCOMPLETE', 'SET_USERNAME_SUBMIT', 'SET_USERNAME_REQUEST', 'SET_USERNAME_SUCCESS' @@ -38,7 +40,13 @@ export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [ 'RESET' ]); export const NAVIGATION = createRequestTypes('NAVIGATION', ['SET']); -export const SERVER = createRequestTypes('SERVER', [...defaultTypes, 'SELECT', 'CHANGED', 'ADD']); +export const SERVER = createRequestTypes('SERVER', [ + ...defaultTypes, + 'SELECT', + 'CHANGED', + 'ADD', + 'GOTO_ADD' +]); export const METEOR = createRequestTypes('METEOR_CONNECT', [...defaultTypes, 'DISCONNECT']); export const LOGOUT = 'LOGOUT'; // logout is always success diff --git a/app/actions/login.js b/app/actions/login.js index 4a67acf84fd9ec14026553955139d86ff6dfb4ea..c603f42bda7a7fe335679c6b061b1e69a53597cd 100644 --- a/app/actions/login.js +++ b/app/actions/login.js @@ -32,6 +32,11 @@ export function registerSuccess(credentials) { credentials }; } +export function registerIncomplete() { + return { + type: types.LOGIN.REGISTER_INCOMPLETE + }; +} export function setUsernameSubmit(credentials) { return { @@ -76,6 +81,13 @@ export function setToken(user = {}) { }; } +export function restoreToken(token) { + return { + type: types.LOGIN.RESTORE_TOKEN, + token + }; +} + export function logout() { return { type: types.LOGOUT diff --git a/app/actions/server.js b/app/actions/server.js index cfc46ad89689b7bef3fe8e2ca7e0776c89ac09d1..b334981b575bfd29f02e9576dcd74fa73673415f 100644 --- a/app/actions/server.js +++ b/app/actions/server.js @@ -41,3 +41,9 @@ export function changedServer(server) { server }; } + +export function gotoAddServer() { + return { + type: SERVER.GOTO_ADD + }; +} diff --git a/app/containers/Routes.js b/app/containers/Routes.js index 83268096c16dbfd4919bce399802199b7f6a8944..e1e57e9509512832031fe47cce255fb1238b615f 100644 --- a/app/containers/Routes.js +++ b/app/containers/Routes.js @@ -7,6 +7,7 @@ import { appInit } from '../actions'; import AuthRoutes from './routes/AuthRoutes'; import PublicRoutes from './routes/PublicRoutes'; import Loading from '../presentation/Loading'; +import * as NavigationService from './routes/NavigationService'; @connect( state => ({ @@ -27,6 +28,11 @@ export default class Routes extends React.Component { componentWillMount() { this.props.appInit(); } + + componentDidUpdate() { + NavigationService.setNavigator(this.navigator); + } + render() { const { login, app } = this.props; @@ -35,9 +41,9 @@ export default class Routes extends React.Component { } if ((login.token && !login.failure && !login.isRegistering) || app.ready) { - return (<AuthRoutes />); + return (<AuthRoutes ref={nav => this.navigator = nav} />); } - return (<PublicRoutes />); + return (<PublicRoutes ref={nav => this.navigator = nav} />); } } diff --git a/app/containers/Sidebar.js b/app/containers/Sidebar.js index 46ece3c41929d9ce6ebddf574e1d8c95fdbe2cbb..4d9f7eb75edf990ea5439b0fc4754e03c96391eb 100644 --- a/app/containers/Sidebar.js +++ b/app/containers/Sidebar.js @@ -5,7 +5,7 @@ import { DrawerItems } from 'react-navigation'; import { connect } from 'react-redux'; import realm from '../lib/realm'; -import { setServer } from '../actions/server'; +import { setServer, gotoAddServer } from '../actions/server'; import { logout } from '../actions/login'; const styles = StyleSheet.create({ @@ -41,14 +41,16 @@ const styles = StyleSheet.create({ server: state.server.server }), dispatch => ({ selectServer: server => dispatch(setServer(server)), - logout: () => dispatch(logout()) + logout: () => dispatch(logout()), + gotoAddServer: () => dispatch(gotoAddServer()) })) export default class Sidebar extends Component { static propTypes = { server: PropTypes.string.isRequired, selectServer: PropTypes.func.isRequired, navigation: PropTypes.object.isRequired, - logout: PropTypes.func.isRequired + logout: PropTypes.func.isRequired, + gotoAddServer: PropTypes.func.isRequired } componentWillMount() { @@ -117,6 +119,15 @@ export default class Sidebar extends Component { </Text> </View> </TouchableHighlight> + <TouchableHighlight + onPress={() => { this.props.gotoAddServer(); }} + > + <View style={styles.serverItem}> + <Text> + Add Server + </Text> + </View> + </TouchableHighlight> </View> </ScrollView> ); diff --git a/app/containers/routes/NavigationService.js b/app/containers/routes/NavigationService.js new file mode 100644 index 0000000000000000000000000000000000000000..0be7f410b3841f3cd3a5cdac8969277fcae6fe7f --- /dev/null +++ b/app/containers/routes/NavigationService.js @@ -0,0 +1,23 @@ +import { NavigationActions } from 'react-navigation'; + +const config = {}; + +export function setNavigator(nav) { + if (nav) { + config.navigator = nav; + } +} + +export function navigate(routeName, params) { + if (config.navigator && routeName) { + const action = NavigationActions.navigate({ routeName, params }); + config.navigator.dispatch(action); + } +} + +export function goBack() { + if (config.navigator) { + const action = NavigationActions.back({}); + config.navigator.dispatch(action); + } +} diff --git a/app/containers/routes/PublicRoutes.js b/app/containers/routes/PublicRoutes.js index 92781c00d471f50bc538561b151d707cb8073c21..41c267173c0de193b965b1b2665bed15a1233545 100644 --- a/app/containers/routes/PublicRoutes.js +++ b/app/containers/routes/PublicRoutes.js @@ -7,6 +7,9 @@ import ListServerView from '../../views/ListServerView'; import NewServerView from '../../views/NewServerView'; import LoginView from '../../views/LoginView'; import RegisterView from '../../views/RegisterView'; + +import TermsServiceView from '../../views/TermsServiceView'; +import PrivacyPolicyView from '../../views/PrivacyPolicyView'; import ForgotPasswordView from '../../views/ForgotPasswordView'; const PublicRoutes = StackNavigator( @@ -45,6 +48,18 @@ const PublicRoutes = StackNavigator( title: 'Register' } }, + TermsService: { + screen: TermsServiceView, + navigationOptions: { + title: 'Terms of service' + } + }, + PrivacyPolicy: { + screen: PrivacyPolicyView, + navigationOptions: { + title: 'Privacy policy' + } + }, ForgotPassword: { screen: ForgotPasswordView, navigationOptions: { diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 81bc55f40955dd28df2e6f1ddf96c292a6ab6c26..22585a6ff52de92ce0a9071d1e8327f11b743fbb 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -23,6 +23,8 @@ const call = (method, ...params) => new Promise((resolve, reject) => { const TOKEN_KEY = 'reactnativemeteor_usertoken'; const RocketChat = { + TOKEN_KEY, + createChannel({ name, users, type }) { return new Promise((resolve, reject) => { Meteor.call(type ? 'createChannel' : 'createPrivateGroup', name, users, type, (err, res) => (err ? reject(err) : resolve(res))); @@ -122,6 +124,17 @@ const RocketChat = { }); }, + me({ server, token, userId }) { + return fetch(`${ server }/api/v1/me`, { + method: 'get', + headers: { + 'Content-Type': 'application/json', + 'X-Auth-Token': token, + 'X-User-Id': userId + } + }).then(response => response.json()); + }, + register({ credentials }) { return new Promise((resolve, reject) => { Meteor.call('registerUser', credentials, (err, userId) => { @@ -438,6 +451,7 @@ const RocketChat = { }, logout({ server }) { Meteor.logout(); + Meteor.disconnect(); AsyncStorage.removeItem(TOKEN_KEY); AsyncStorage.removeItem(`${ TOKEN_KEY }-${ server }`); } diff --git a/app/reducers/login.js b/app/reducers/login.js index 81ff98c3cbab1dc83514835269191b88eb05a52f..e90531ffda6bc94949b13e86a2b61e97f4a8d3b6 100644 --- a/app/reducers/login.js +++ b/app/reducers/login.js @@ -45,6 +45,11 @@ export default function login(state = initialState, action) { token: action.token, user: action.user }; + case types.LOGIN.RESTORE_TOKEN: + return { + ...state, + token: action.token + }; case types.LOGIN.REGISTER_SUBMIT: return { ...state, @@ -73,6 +78,11 @@ export default function login(state = initialState, action) { isFetching: false, isRegistering: false }; + case types.LOGIN.REGISTER_INCOMPLETE: + return { + ...state, + isRegistering: true + }; case types.FORGOT_PASSWORD.INIT: return initialState; case types.FORGOT_PASSWORD.REQUEST: diff --git a/app/sagas/init.js b/app/sagas/init.js index cb53af7de16bbf0edbcf8269695fdaeed1762ba3..c86b5caadafc359b28f4d8600af29e10f16488f7 100644 --- a/app/sagas/init.js +++ b/app/sagas/init.js @@ -2,16 +2,23 @@ import { AsyncStorage } from 'react-native'; import { call, put, take } from 'redux-saga/effects'; import * as actions from '../actions'; import { setServer } from '../actions/server'; +import { restoreToken } from '../actions/login'; import { APP } from '../actions/actionsTypes'; const restore = function* restore() { try { yield take(APP.INIT); + const token = yield call([AsyncStorage, 'getItem'], 'reactnativemeteor_usertoken'); + if (token) { + yield put(restoreToken(token)); + } + const currentServer = yield call([AsyncStorage, 'getItem'], 'currentServer'); - yield put(actions.appReady({})); if (currentServer) { yield put(setServer(currentServer)); } + + yield put(actions.appReady({})); } catch (e) { console.log(e); } diff --git a/app/sagas/login.js b/app/sagas/login.js index c16e9f1b47cbc917510da713fc87b3882b329d40..bd9b5287a72daf666a95acc8d2fabe5e55af9f84 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -5,6 +5,7 @@ import { loginRequest, loginSubmit, registerRequest, + registerIncomplete, loginSuccess, loginFailure, setToken, @@ -16,23 +17,24 @@ import { forgotPasswordFailure } from '../actions/login'; import RocketChat from '../lib/rocketchat'; +import * as NavigationService from '../containers/routes/NavigationService'; -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 logoutCall = args => RocketChat.logout(args); +const meCall = args => RocketChat.me(args); const forgotPasswordCall = args => RocketChat.forgotPassword(args); const getToken = function* getToken() { const currentServer = yield select(getServer); - const user = yield call([AsyncStorage, 'getItem'], `${ TOKEN_KEY }-${ currentServer }`); + const user = yield call([AsyncStorage, 'getItem'], `${ RocketChat.TOKEN_KEY }-${ currentServer }`); if (user) { try { yield put(setToken(JSON.parse(user))); - yield call([AsyncStorage, 'setItem'], TOKEN_KEY, JSON.parse(user).token || ''); + yield call([AsyncStorage, 'setItem'], RocketChat.TOKEN_KEY, JSON.parse(user).token || ''); return JSON.parse(user); } catch (e) { console.log('getTokenerr', e); @@ -43,47 +45,41 @@ const getToken = function* getToken() { }; const handleLoginWhenServerChanges = function* handleLoginWhenServerChanges() { - // do { try { yield take(types.METEOR.SUCCESS); yield call(getToken); - // const { navigator } = yield select(state => state); const user = yield select(getUser); if (user.token) { yield put(loginRequest({ resume: user.token })); - // console.log('AEEEEEEEEOOOOO'); - // // wait for a response - // const { error } = yield race({ - // success: take(types.LOGIN.SUCCESS), - // error: take(types.LOGIN.FAILURE) - // }); - // console.log('AEEEEEEEEOOOOO', error); - // if (!error) { - // navigator.resetTo({ - // screen: 'Rooms' - // }); - // } } - // navigator.resetTo({ - // screen: 'Rooms' - // }); } catch (e) { console.log(e); } - // } while (true); }; const saveToken = function* saveToken() { const [server, user] = yield all([select(getServer), select(getUser)]); - yield AsyncStorage.setItem(TOKEN_KEY, user.token); - yield AsyncStorage.setItem(`${ TOKEN_KEY }-${ server }`, JSON.stringify(user)); + yield AsyncStorage.setItem(RocketChat.TOKEN_KEY, user.token); + yield AsyncStorage.setItem(`${ RocketChat.TOKEN_KEY }-${ server }`, JSON.stringify(user)); }; const handleLoginRequest = function* handleLoginRequest({ credentials }) { try { - const response = yield call(loginCall, credentials); - yield put(loginSuccess(response)); + const server = yield select(getServer); + const user = yield call(loginCall, credentials); + + // GET /me from REST API + const me = yield call(meCall, { server, token: user.token, userId: user.id }); + + // if user has username + if (me.username) { + user.username = me.username; + } else { + yield put(registerIncomplete()); + } + + yield put(loginSuccess(user)); } catch (err) { if (err.error === 403) { yield put(logout()); @@ -98,13 +94,7 @@ const handleLoginSubmit = function* handleLoginSubmit({ credentials }) { }; const handleRegisterSubmit = function* handleRegisterSubmit({ credentials }) { - // put a login request yield put(registerRequest(credentials)); - // wait for a response - // yield race({ - // success: take(types.LOGIN.REGISTER_SUCCESS), - // error: take(types.LOGIN.FAILURE) - // }); }; const handleRegisterRequest = function* handleRegisterRequest({ credentials }) { @@ -141,6 +131,10 @@ const handleLogout = function* handleLogout() { yield call(logoutCall, { server }); }; +const handleRegisterIncomplete = function* handleRegisterIncomplete() { + yield call(NavigationService.navigate, 'Register'); +}; + const handleForgotPasswordRequest = function* handleForgotPasswordRequest({ email }) { try { yield call(forgotPasswordCall, email); @@ -158,6 +152,7 @@ const root = function* root() { 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.REGISTER_INCOMPLETE, handleRegisterIncomplete); yield takeLatest(types.LOGIN.SET_USERNAME_SUBMIT, handleSetUsernameSubmit); yield takeLatest(types.LOGIN.SET_USERNAME_REQUEST, handleSetUsernameRequest); yield takeLatest(types.LOGOUT, handleLogout); diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js index dc9b039ec7ebc72ca957b573daf1049c1368da75..84ad3a3ee2beec0a02044f6eff0ff882147a8dda 100644 --- a/app/sagas/selectServer.js +++ b/app/sagas/selectServer.js @@ -1,12 +1,13 @@ import { put, takeEvery, call, takeLatest, race, take } from 'redux-saga/effects'; import { delay } from 'redux-saga'; import { AsyncStorage } from 'react-native'; -// import { Navigation } from 'react-native-navigation'; import { SERVER } from '../actions/actionsTypes'; import { connectRequest, disconnect } from '../actions/connect'; -import { changedServer, serverSuccess, serverFailure, serverRequest } from '../actions/server'; +import { changedServer, serverSuccess, serverFailure, serverRequest, setServer } from '../actions/server'; +import { logout } from '../actions/login'; import RocketChat from '../lib/rocketchat'; import realm from '../lib/realm'; +import * as NavigationService from '../containers/routes/NavigationService'; const validate = function* validate(server) { return yield RocketChat.testServer(server); @@ -42,13 +43,21 @@ const addServer = function* addServer({ server }) { realm.write(() => { realm.create('servers', { id: server, current: false }, true); }); + yield put(setServer(server)); } }; +const handleGotoAddServer = function* handleGotoAddServer() { + yield put(logout()); + yield call(AsyncStorage.removeItem, RocketChat.TOKEN_KEY); + yield delay(1000); + yield call(NavigationService.navigate, 'AddServer'); +}; const root = function* root() { yield takeLatest(SERVER.REQUEST, validateServer); yield takeEvery(SERVER.SELECT, selectServer); yield takeEvery(SERVER.ADD, addServer); + yield takeEvery(SERVER.GOTO_ADD, handleGotoAddServer); }; export default root; diff --git a/app/views/LoginView.js b/app/views/LoginView.js index a5e1ad9236c9636fcffd8900f4d14d6531759bba..0b508eddfe8d09d5a1829fd4adea7057acc19ce0 100644 --- a/app/views/LoginView.js +++ b/app/views/LoginView.js @@ -1,6 +1,6 @@ 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 { Keyboard, Text, TextInput, View, TouchableOpacity, SafeAreaView } from 'react-native'; @@ -49,6 +49,14 @@ class LoginView extends React.Component { this.props.navigation.navigate('Register'); } + termsService = () => { + this.props.navigation.navigate('TermsService'); + } + + privacyPolicy = () => { + this.props.navigation.navigate('PrivacyPolicy'); + } + forgotPassword = () => { this.props.navigation.navigate('ForgotPassword'); } @@ -78,8 +86,8 @@ class LoginView extends React.Component { contentContainerStyle={styles.container} keyboardVerticalOffset={128} > - <SafeAreaView> - <View style={styles.loginView}> + <View style={styles.loginView}> + <SafeAreaView> <View style={styles.formContainer}> <TextInput style={styles.input_white} @@ -107,23 +115,32 @@ class LoginView extends React.Component { {this.renderTOTP()} - <TouchableOpacity style={styles.buttonContainer}> - <Text style={styles.button} onPress={this.submit}>LOGIN</Text> + <TouchableOpacity + style={styles.buttonContainer} + onPress={this.submit} + > + <Text style={styles.button}>LOGIN</Text> </TouchableOpacity> - <TouchableOpacity style={styles.buttonContainer}> - <Text style={styles.button} onPress={this.register}>REGISTER</Text> + <TouchableOpacity style={styles.buttonContainer} onPress={this.register}> + <Text style={styles.button}>REGISTER</Text> </TouchableOpacity> + <TouchableOpacity style={styles.buttonContainer} onPress={this.termsService}> + <Text style={styles.button}>TERMS OF SERVICE</Text> + </TouchableOpacity> + + <TouchableOpacity style={styles.buttonContainer} onPress={this.privacyPolicy}> + <Text style={styles.button}>PRIVACY POLICY</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' }} /> - </View> - </SafeAreaView> + </SafeAreaView> + </View> </KeyboardView> ); } diff --git a/app/views/PrivacyPolicyView.js b/app/views/PrivacyPolicyView.js new file mode 100644 index 0000000000000000000000000000000000000000..e4f96abc18701cbae0866c7758225a9d379de10f --- /dev/null +++ b/app/views/PrivacyPolicyView.js @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { WebView } from 'react-native'; +import { connect } from 'react-redux'; + +class PrivacyPolicyView extends React.Component { + static propTypes = { + privacyPolicy: PropTypes.string + } + + static navigationOptions = () => ({ + title: 'Terms of service' + }); + + render() { + return ( + <WebView source={{ html: this.props.privacyPolicy }} /> + ); + } +} + +function mapStateToProps(state) { + return { + privacyPolicy: state.settings.Layout_Privacy_Policy + }; +} + +export default connect(mapStateToProps)(PrivacyPolicyView); diff --git a/app/views/RegisterView.js b/app/views/RegisterView.js index 5ddd9668263d3a09492e12faae59ecddc32826b8..ed2625869f9b8328d8d64270ce2e381428bc8810 100644 --- a/app/views/RegisterView.js +++ b/app/views/RegisterView.js @@ -125,12 +125,14 @@ class RegisterView extends React.Component { placeholder={this.props.Accounts_RepeatPasswordPlaceholder || 'Repeat Password'} /> - <TouchableOpacity style={[styles.buttonContainer, styles.registerContainer]}> + <TouchableOpacity + style={[styles.buttonContainer, styles.registerContainer]} + onPress={this.submit} + > <Text style={[styles.button, this._valid() ? {} : { color: placeholderTextColor } ]} - onPress={this.submit} >REGISTER </Text> </TouchableOpacity> @@ -158,12 +160,11 @@ class RegisterView extends React.Component { placeholder={this.props.Accounts_UsernamePlaceholder || 'Username'} /> - <TouchableOpacity style={[styles.buttonContainer, styles.registerContainer]}> - <Text - style={styles.button} - onPress={this.usernameSubmit} - >REGISTER - </Text> + <TouchableOpacity + style={[styles.buttonContainer, styles.registerContainer]} + onPress={this.usernameSubmit} + > + <Text style={styles.button}>REGISTER</Text> </TouchableOpacity> {this.props.login.failure && <Text style={styles.error}>{this.props.login.error.reason}</Text>} diff --git a/app/views/TermsServiceView.js b/app/views/TermsServiceView.js new file mode 100644 index 0000000000000000000000000000000000000000..b56abca08a0b2b711af7fe127a0d1bd6d653750c --- /dev/null +++ b/app/views/TermsServiceView.js @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { WebView } from 'react-native'; +import { connect } from 'react-redux'; + +class TermsServiceView extends React.Component { + static propTypes = { + termsService: PropTypes.string + } + + static navigationOptions = () => ({ + title: 'Terms of service' + }); + + render() { + return ( + <WebView source={{ html: this.props.termsService }} /> + ); + } +} + +function mapStateToProps(state) { + return { + termsService: state.settings.Layout_Terms_of_Service + }; +} + +export default connect(mapStateToProps)(TermsServiceView);