[NEW] OAuth (#241)

* Layout

* tmp

* test iscordova

* Webview redirecting

* Open and Close login actions

* Login services saved on redux

* OAuth Github

* Server regex fix

* OAuth modal style

* - Twitter login
- Remove services from redux
- Open login saga fix

* - Facebook login
- Fixed user agent
- Reactions fix
- Message url unique key fix

* Google login

* Email keyboard removed from messagebox

* - Login buttons refactored
- RoomList header

* Layout improvements

* Meteor login redirect_uri changed

* fix

* Random credentialToken state
This commit is contained in:
Diego Mello 2018-02-23 17:29:06 -03:00 committed by Rodrigo Nascimento
parent dba0e16da7
commit 477609375c
14 changed files with 439 additions and 99 deletions

View File

@ -20,7 +20,11 @@ export const LOGIN = createRequestTypes('LOGIN', [
'REGISTER_INCOMPLETE', 'REGISTER_INCOMPLETE',
'SET_USERNAME_SUBMIT', 'SET_USERNAME_SUBMIT',
'SET_USERNAME_REQUEST', 'SET_USERNAME_REQUEST',
'SET_USERNAME_SUCCESS' 'SET_USERNAME_SUCCESS',
'OPEN',
'CLOSE',
'SET_SERVICES',
'REMOVE_SERVICES'
]); ]);
export const FORGOT_PASSWORD = createRequestTypes('FORGOT_PASSWORD', [ export const FORGOT_PASSWORD = createRequestTypes('FORGOT_PASSWORD', [
...defaultTypes, ...defaultTypes,

View File

@ -13,7 +13,6 @@ export function loginRequest(credentials) {
}; };
} }
export function registerSubmit(credentials) { export function registerSubmit(credentials) {
return { return {
type: types.LOGIN.REGISTER_SUBMIT, type: types.LOGIN.REGISTER_SUBMIT,
@ -125,3 +124,28 @@ export function setUser(action) {
...action ...action
}; };
} }
export function open() {
return {
type: types.LOGIN.OPEN
};
}
export function close() {
return {
type: types.LOGIN.CLOSE
};
}
export function setLoginServices(data) {
return {
type: types.LOGIN.SET_SERVICES,
data
};
}
export function removeLoginServices() {
return {
type: types.LOGIN.REMOVE_SERVICES
};
}

View File

@ -460,7 +460,6 @@ export default class MessageBox extends React.PureComponent {
<TextInput <TextInput
ref={component => this.component = component} ref={component => this.component = component}
style={styles.textBoxInput} style={styles.textBoxInput}
keyboardType='email-address'
returnKeyType='default' returnKeyType='default'
blurOnSubmit={false} blurOnSubmit={false}
placeholder='New Message' placeholder='New Message'

View File

@ -165,7 +165,7 @@ export default class Message extends React.Component {
} }
return this.props.item.urls.map(url => ( return this.props.item.urls.map(url => (
<Url url={url} key={url._id} /> <Url url={url} key={url.url} />
)); ));
} }

View File

@ -10,7 +10,7 @@ import messagesStatus from '../constants/messagesStatus';
import database from './realm'; import database from './realm';
import * as actions from '../actions'; import * as actions from '../actions';
import { someoneTyping, roomMessageReceived } from '../actions/room'; import { someoneTyping, roomMessageReceived } from '../actions/room';
import { setUser } from '../actions/login'; import { setUser, setLoginServices, removeLoginServices } from '../actions/login';
import { disconnect, disconnect_by_user, connectSuccess, connectFailure } from '../actions/connect'; import { disconnect, disconnect_by_user, connectSuccess, connectFailure } from '../actions/connect';
import { requestActiveUser } from '../actions/activeUsers'; import { requestActiveUser } from '../actions/activeUsers';
import { starredMessageReceived, starredMessageUnstarred } from '../actions/starredMessages'; import { starredMessageReceived, starredMessageUnstarred } from '../actions/starredMessages';
@ -27,6 +27,8 @@ const SERVER_TIMEOUT = 30000;
const normalizeMessage = (lastMessage) => { const normalizeMessage = (lastMessage) => {
if (lastMessage) { if (lastMessage) {
lastMessage.attachments = lastMessage.attachments || []; lastMessage.attachments = lastMessage.attachments || [];
lastMessage.reactions = _.map(lastMessage.reactions, (value, key) =>
({ emoji: key, usernames: value.usernames.map(username => ({ value: username })) }));
} }
return lastMessage; return lastMessage;
}; };
@ -95,10 +97,11 @@ const RocketChat = {
this.ddp.on('disconnected', () => { this.ddp.on('disconnected', () => {
reduxStore.dispatch(disconnect()); reduxStore.dispatch(disconnect());
}); });
this.ddp.on('open', async() => { // this.ddp.on('open', async() => {
resolve(reduxStore.dispatch(connectSuccess())); // resolve(reduxStore.dispatch(connectSuccess()));
}); // });
this.ddp.on('connected', () => { this.ddp.on('connected', () => {
resolve(reduxStore.dispatch(connectSuccess()));
RocketChat.getSettings(); RocketChat.getSettings();
RocketChat.getPermissions(); RocketChat.getPermissions();
RocketChat.getCustomEmoji(); RocketChat.getCustomEmoji();
@ -171,6 +174,28 @@ const RocketChat = {
return reduxStore.dispatch(pinnedMessageUnpinned(ddpMessage.id)); return reduxStore.dispatch(pinnedMessageUnpinned(ddpMessage.id));
} }
}); });
this.ddp.on('meteor_accounts_loginServiceConfiguration', (ddpMessage) => {
if (ddpMessage.msg === 'added') {
this.loginServices = this.loginServices || {};
if (this.loginServiceTimer) {
clearTimeout(this.loginServiceTimer);
this.loginServiceTimer = null;
}
this.loginServiceTimer = setTimeout(() => {
reduxStore.dispatch(setLoginServices(this.loginServices));
this.loginServiceTimer = null;
return this.loginServices = {};
}, 1000);
this.loginServices[ddpMessage.fields.service] = { ...ddpMessage.fields };
delete this.loginServices[ddpMessage.fields.service].service;
} else if (ddpMessage.msg === 'removed') {
if (this.loginServiceTimer) {
clearTimeout(this.loginServiceTimer);
}
this.loginServiceTimer = setTimeout(() => reduxStore.dispatch(removeLoginServices()), 1000);
}
});
}).catch(console.log); }).catch(console.log);
}, },
@ -302,8 +327,6 @@ const RocketChat = {
// loadHistory returns message.starred as object // loadHistory returns message.starred as object
// stream-room-messages returns message.starred as an array // stream-room-messages returns message.starred as an array
message.starred = message.starred && (Array.isArray(message.starred) ? message.starred.length > 0 : !!message.starred); message.starred = message.starred && (Array.isArray(message.starred) ? message.starred.length > 0 : !!message.starred);
message.reactions = _.map(message.reactions, (value, key) =>
({ emoji: key, usernames: value.usernames.map(username => ({ value: username })) }));
return message; return message;
}, },
loadMessagesForRoom(rid, end, cb) { loadMessagesForRoom(rid, end, cb) {

View File

@ -6,7 +6,8 @@ const initialState = {
isRegistering: false, isRegistering: false,
token: '', token: '',
user: {}, user: {},
error: '' error: '',
services: {}
}; };
export default function login(state = initialState, action) { export default function login(state = initialState, action) {
@ -115,6 +116,19 @@ export default function login(state = initialState, action) {
...action ...action
} }
}; };
case types.LOGIN.SET_SERVICES:
return {
...state,
services: {
...state.services,
...action.data
}
};
case types.LOGIN.REMOVE_SERVICES:
return {
...state,
services: {}
};
default: default:
return state; return state;
} }

View File

@ -1,5 +1,5 @@
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import { put, call, takeLatest, select, all } from 'redux-saga/effects'; import { put, call, takeLatest, select, all, take } from 'redux-saga/effects';
import * as types from '../actions/actionsTypes'; import * as types from '../actions/actionsTypes';
import { import {
loginRequest, loginRequest,
@ -21,7 +21,8 @@ import * as NavigationService from '../containers/routes/NavigationService';
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 getIsConnected = state => state.meteor.connected;
const loginCall = args => ((args.resume || args.oauth) ? RocketChat.login(args) : RocketChat.loginWithPassword(args));
const registerCall = args => RocketChat.register(args); const registerCall = args => RocketChat.register(args);
const setUsernameCall = args => RocketChat.setUsername(args); const setUsernameCall = args => RocketChat.setUsername(args);
const logoutCall = args => RocketChat.logout(args); const logoutCall = args => RocketChat.logout(args);
@ -148,6 +149,16 @@ const handleForgotPasswordRequest = function* handleForgotPasswordRequest({ emai
} }
}; };
const watchLoginOpen = function* watchLoginOpen() {
const isConnected = yield select(getIsConnected);
if (!isConnected) {
yield take(types.METEOR.SUCCESS);
}
const sub = yield RocketChat.subscribe('meteor.loginServiceConfiguration');
yield take(types.LOGIN.CLOSE);
sub.unsubscribe().catch(e => alert(e));
};
const root = function* root() { const root = function* root() {
yield takeLatest(types.METEOR.SUCCESS, handleLoginWhenServerChanges); yield takeLatest(types.METEOR.SUCCESS, handleLoginWhenServerChanges);
yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest); yield takeLatest(types.LOGIN.REQUEST, handleLoginRequest);
@ -161,5 +172,6 @@ const root = function* root() {
yield takeLatest(types.LOGIN.SET_USERNAME_REQUEST, handleSetUsernameRequest); yield takeLatest(types.LOGIN.SET_USERNAME_REQUEST, handleSetUsernameRequest);
yield takeLatest(types.LOGOUT, handleLogout); yield takeLatest(types.LOGOUT, handleLogout);
yield takeLatest(types.FORGOT_PASSWORD.REQUEST, handleForgotPasswordRequest); yield takeLatest(types.FORGOT_PASSWORD.REQUEST, handleForgotPasswordRequest);
yield takeLatest(types.LOGIN.OPEN, watchLoginOpen);
}; };
export default root; export default root;

8
app/utils/random.js Normal file
View File

@ -0,0 +1,8 @@
export default function random(length) {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < length; i += 1) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}

View File

@ -1,23 +1,57 @@
import React from 'react'; 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, ScrollView, TouchableOpacity, SafeAreaView } from 'react-native'; import { Keyboard, Text, TextInput, View, ScrollView, TouchableOpacity, SafeAreaView, WebView, Platform, LayoutAnimation } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { bindActionCreators } from 'redux'; import Icon from 'react-native-vector-icons/FontAwesome';
import * as loginActions from '../actions/login'; import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
import { Base64 } from 'js-base64';
import Modal from 'react-native-modal';
import { loginSubmit, open, close } from '../actions/login';
import KeyboardView from '../presentation/KeyboardView'; import KeyboardView from '../presentation/KeyboardView';
import styles from './Styles'; import styles from './Styles';
import scrollPersistTaps from '../utils/scrollPersistTaps'; import scrollPersistTaps from '../utils/scrollPersistTaps';
import { showToast } from '../utils/info'; import { showToast } from '../utils/info';
import random from '../utils/random';
class LoginView extends React.Component { @connect(state => ({
server: state.server.server,
login: state.login,
Accounts_EmailOrUsernamePlaceholder: state.settings.Accounts_EmailOrUsernamePlaceholder,
Accounts_PasswordPlaceholder: state.settings.Accounts_PasswordPlaceholder,
Accounts_OAuth_Facebook: state.settings.Accounts_OAuth_Facebook,
Accounts_OAuth_Github: state.settings.Accounts_OAuth_Github,
Accounts_OAuth_Gitlab: state.settings.Accounts_OAuth_Gitlab,
Accounts_OAuth_Google: state.settings.Accounts_OAuth_Google,
Accounts_OAuth_Linkedin: state.settings.Accounts_OAuth_Linkedin,
Accounts_OAuth_Meteor: state.settings.Accounts_OAuth_Meteor,
Accounts_OAuth_Twitter: state.settings.Accounts_OAuth_Twitter,
services: state.login.services
}), dispatch => ({
loginSubmit: params => dispatch(loginSubmit(params)),
open: () => dispatch(open()),
close: () => dispatch(close())
}))
export default class LoginView extends React.Component {
static propTypes = { static propTypes = {
loginSubmit: PropTypes.func.isRequired, loginSubmit: PropTypes.func.isRequired,
Accounts_EmailOrUsernamePlaceholder: PropTypes.string, open: PropTypes.func.isRequired,
Accounts_PasswordPlaceholder: PropTypes.string, close: PropTypes.func.isRequired,
navigation: PropTypes.object.isRequired,
login: PropTypes.object, login: PropTypes.object,
navigation: PropTypes.object.isRequired server: PropTypes.string,
Accounts_EmailOrUsernamePlaceholder: PropTypes.bool,
Accounts_PasswordPlaceholder: PropTypes.string,
Accounts_OAuth_Facebook: PropTypes.bool,
Accounts_OAuth_Github: PropTypes.bool,
Accounts_OAuth_Gitlab: PropTypes.bool,
Accounts_OAuth_Google: PropTypes.bool,
Accounts_OAuth_Linkedin: PropTypes.bool,
Accounts_OAuth_Meteor: PropTypes.bool,
Accounts_OAuth_Twitter: PropTypes.bool,
services: PropTypes.object
} }
static navigationOptions = () => ({ static navigationOptions = () => ({
@ -29,8 +63,99 @@ class LoginView extends React.Component {
this.state = { this.state = {
username: '', username: '',
password: '' password: '',
modalVisible: false,
oAuthUrl: ''
}; };
this.redirectRegex = new RegExp(`(?=.*(${ this.props.server }))(?=.*(credentialToken))(?=.*(credentialSecret))`, 'g');
}
componentWillMount() {
this.props.open();
}
componentWillReceiveProps(nextProps) {
if (this.props.services !== nextProps.services) {
LayoutAnimation.easeInEaseOut();
}
}
componentWillUnmount() {
this.props.close();
}
onPressFacebook = () => {
const { appId } = this.props.services.facebook;
const endpoint = 'https://m.facebook.com/v2.9/dialog/oauth';
const redirect_uri = `${ this.props.server }/_oauth/facebook?close`;
const scope = 'email';
const state = this.getOAuthState();
const params = `?client_id=${ appId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&display=touch`;
this.openOAuth(`${ endpoint }${ params }`);
}
onPressGithub = () => {
const { clientId } = this.props.services.github;
const endpoint = `https://github.com/login?client_id=${ clientId }&return_to=${ encodeURIComponent('/login/oauth/authorize') }`;
const redirect_uri = `${ this.props.server }/_oauth/github?close`;
const scope = 'user:email';
const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }`;
this.openOAuth(`${ endpoint }${ encodeURIComponent(params) }`);
}
onPressGitlab = () => {
const { clientId } = this.props.services.gitlab;
const endpoint = 'https://gitlab.com/oauth/authorize';
const redirect_uri = `${ this.props.server }/_oauth/gitlab?close`;
const scope = 'read_user';
const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`;
this.openOAuth(`${ endpoint }${ params }`);
}
onPressGoogle = () => {
const { clientId } = this.props.services.google;
const endpoint = 'https://accounts.google.com/o/oauth2/auth';
const redirect_uri = `${ this.props.server }/_oauth/google?close`;
const scope = 'email';
const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`;
this.openOAuth(`${ endpoint }${ params }`);
}
onPressLinkedin = () => {
const { clientId } = this.props.services.linkedin;
const endpoint = 'https://www.linkedin.com/uas/oauth2/authorization';
const redirect_uri = `${ this.props.server }/_oauth/linkedin?close`;
const scope = 'r_emailaddress';
const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&scope=${ scope }&state=${ state }&response_type=code`;
this.openOAuth(`${ endpoint }${ params }`);
}
onPressMeteor = () => {
const { clientId } = this.props.services['meteor-developer'];
const endpoint = 'https://www.meteor.com/oauth2/authorize';
const redirect_uri = `${ this.props.server }/_oauth/meteor-developer`;
const state = this.getOAuthState();
const params = `?client_id=${ clientId }&redirect_uri=${ redirect_uri }&state=${ state }&response_type=code`;
this.openOAuth(`${ endpoint }${ params }`);
}
onPressTwitter = () => {
const state = this.getOAuthState();
const url = `${ this.props.server }/_oauth/twitter/?requestTokenAndRedirect=true&state=${ state }`;
this.openOAuth(url);
}
getOAuthState = () => {
const credentialToken = random(43);
return Base64.encodeURI(JSON.stringify({ loginStyle: 'popup', credentialToken, isCordova: true }));
}
openOAuth = (oAuthUrl) => {
this.setState({ oAuthUrl, modalVisible: true });
} }
submit = () => { submit = () => {
@ -44,6 +169,10 @@ class LoginView extends React.Component {
Keyboard.dismiss(); Keyboard.dismiss();
} }
submitOAuth = (code, credentialToken) => {
this.props.loginSubmit({ code, credentialToken });
}
register = () => { register = () => {
this.props.navigation.navigate('Register'); this.props.navigation.navigate('Register');
} }
@ -60,6 +189,10 @@ class LoginView extends React.Component {
this.props.navigation.navigate('ForgotPassword'); this.props.navigation.navigate('ForgotPassword');
} }
closeOAuth = () => {
this.setState({ modalVisible: false });
}
renderTOTP = () => { renderTOTP = () => {
if (/totp/ig.test(this.props.login.error.error)) { if (/totp/ig.test(this.props.login.error.error)) {
return ( return (
@ -82,9 +215,11 @@ class LoginView extends React.Component {
render() { render() {
return ( return (
[
<KeyboardView <KeyboardView
contentContainerStyle={styles.container} contentContainerStyle={styles.container}
keyboardVerticalOffset={128} keyboardVerticalOffset={128}
key='login-view'
> >
<ScrollView <ScrollView
style={styles.loginView} style={styles.loginView}
@ -135,6 +270,65 @@ class LoginView extends React.Component {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={styles.loginOAuthButtons} key='services'>
{this.props.Accounts_OAuth_Facebook && this.props.services.facebook &&
<TouchableOpacity
style={[styles.oauthButton, styles.facebookButton]}
onPress={this.onPressFacebook}
>
<Icon name='facebook' size={20} color='#ffffff' />
</TouchableOpacity>
}
{this.props.Accounts_OAuth_Github && this.props.services.github &&
<TouchableOpacity
style={[styles.oauthButton, styles.githubButton]}
onPress={this.onPressGithub}
>
<Icon name='github' size={20} color='#ffffff' />
</TouchableOpacity>
}
{this.props.Accounts_OAuth_Gitlab && this.props.services.gitlab &&
<TouchableOpacity
style={[styles.oauthButton, styles.gitlabButton]}
onPress={this.onPressGitlab}
>
<Icon name='gitlab' size={20} color='#ffffff' />
</TouchableOpacity>
}
{this.props.Accounts_OAuth_Google && this.props.services.google &&
<TouchableOpacity
style={[styles.oauthButton, styles.googleButton]}
onPress={this.onPressGoogle}
>
<Icon name='google' size={20} color='#ffffff' />
</TouchableOpacity>
}
{this.props.Accounts_OAuth_Linkedin && this.props.services.linkedin &&
<TouchableOpacity
style={[styles.oauthButton, styles.linkedinButton]}
onPress={this.onPressLinkedin}
>
<Icon name='linkedin' size={20} color='#ffffff' />
</TouchableOpacity>
}
{this.props.Accounts_OAuth_Meteor && this.props.services['meteor-developer'] &&
<TouchableOpacity
style={[styles.oauthButton, styles.meteorButton]}
onPress={this.onPressMeteor}
>
<MaterialCommunityIcons name='meteor' size={25} color='#ffffff' />
</TouchableOpacity>
}
{this.props.Accounts_OAuth_Twitter && this.props.services.twitter &&
<TouchableOpacity
style={[styles.oauthButton, styles.twitterButton]}
onPress={this.onPressTwitter}
>
<Icon name='twitter' size={20} color='#ffffff' />
</TouchableOpacity>
}
</View>
<TouchableOpacity> <TouchableOpacity>
<Text style={styles.loginTermsText} accessibilityTraits='button'> <Text style={styles.loginTermsText} accessibilityTraits='button'>
By proceeding you are agreeing to our By proceeding you are agreeing to our
@ -148,22 +342,31 @@ class LoginView extends React.Component {
<Spinner visible={this.props.login.isFetching} textContent='Loading...' textStyle={{ color: '#FFF' }} /> <Spinner visible={this.props.login.isFetching} textContent='Loading...' textStyle={{ color: '#FFF' }} />
</SafeAreaView> </SafeAreaView>
</ScrollView> </ScrollView>
</KeyboardView> </KeyboardView>,
<Modal
key='modal-oauth'
visible={this.state.modalVisible}
animationType='slide'
style={styles.oAuthModal}
onBackButtonPress={this.closeOAuth}
useNativeDriver
>
<WebView
source={{ uri: this.state.oAuthUrl }}
userAgent={Platform.OS === 'ios' ? 'UserAgent' : 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'}
onNavigationStateChange={(webViewState) => {
const url = decodeURIComponent(webViewState.url);
if (this.redirectRegex.test(url)) {
const parts = url.split('#');
const credentials = JSON.parse(parts[1]);
this.props.loginSubmit({ oauth: { ...credentials } });
this.setState({ modalVisible: false });
}
}}
/>
<Icon name='close' size={30} style={styles.closeOAuth} onPress={this.closeOAuth} />
</Modal>
]
); );
} }
} }
function mapStateToProps(state) {
return {
server: state.server.server,
Accounts_EmailOrUsernamePlaceholder: state.settings.Accounts_EmailOrUsernamePlaceholder,
Accounts_PasswordPlaceholder: state.settings.Accounts_PasswordPlaceholder,
login: state.login
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(loginActions, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(LoginView);

View File

@ -241,7 +241,7 @@ export default class RoomsListView extends React.Component {
dataSource={this.state.dataSource} dataSource={this.state.dataSource}
style={styles.list} style={styles.list}
renderItem={this.renderItem} renderItem={this.renderItem}
renderHeader={Platform.OS === 'ios' ? this.renderSearchBar : null} ListHeaderComponent={Platform.OS === 'ios' ? this.renderSearchBar : null}
contentOffset={Platform.OS === 'ios' ? { x: 0, y: 38 } : {}} contentOffset={Platform.OS === 'ios' ? { x: 0, y: 38 } : {}}
enableEmptySections enableEmptySections
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'

View File

@ -1,4 +1,4 @@
import { StyleSheet, Dimensions } from 'react-native'; import { StyleSheet, Dimensions, Platform } from 'react-native';
export default StyleSheet.create({ export default StyleSheet.create({
container: { container: {
@ -157,6 +157,11 @@ export default StyleSheet.create({
flexWrap: 'wrap', flexWrap: 'wrap',
justifyContent: 'space-around' justifyContent: 'space-around'
}, },
loginOAuthButtons: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center'
},
validText: { validText: {
color: 'green' color: 'green'
}, },
@ -165,5 +170,43 @@ export default StyleSheet.create({
}, },
validatingText: { validatingText: {
color: '#aaa' color: '#aaa'
},
oauthButton: {
width: 50,
height: 50,
alignItems: 'center',
justifyContent: 'center',
margin: 4,
borderRadius: 4
},
facebookButton: {
backgroundColor: '#3b5998'
},
githubButton: {
backgroundColor: '#4c4c4c'
},
gitlabButton: {
backgroundColor: '#373d47'
},
googleButton: {
backgroundColor: '#dd4b39'
},
linkedinButton: {
backgroundColor: '#1b86bc'
},
meteorButton: {
backgroundColor: '#de4f4f'
},
twitterButton: {
backgroundColor: '#02acec'
},
closeOAuth: {
position: 'absolute',
left: 5,
top: Platform.OS === 'ios' ? 20 : 0,
backgroundColor: 'transparent'
},
oAuthModal: {
margin: 0
} }
}); });

View File

@ -28,6 +28,10 @@
launchOptions:launchOptions]; launchOptions:launchOptions];
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
NSString *newAgent = @"Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1";
NSDictionary *dictionary = [[NSDictionary alloc] initWithObjectsAndKeys:newAgent, @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new]; UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView; rootViewController.view = rootView;

5
package-lock.json generated
View File

@ -9073,6 +9073,11 @@
"jsdom": "11.6.2" "jsdom": "11.6.2"
} }
}, },
"js-base64": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.3.tgz",
"integrity": "sha512-H7ErYLM34CvDMto3GbD6xD0JLUGYXR3QTcH6B/tr4Hi/QpSThnCsIp+Sy5FRTw3B0d6py4HcNkW7nO/wdtGWEw=="
},
"jest-environment-node": { "jest-environment-node": {
"version": "22.3.0", "version": "22.3.0",
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-22.3.0.tgz", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-22.3.0.tgz",

View File

@ -30,6 +30,7 @@
"babel-preset-expo": "^4.0.0", "babel-preset-expo": "^4.0.0",
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
"ejson": "^2.1.2", "ejson": "^2.1.2",
"js-base64": "^2.4.3",
"lodash": "^4.17.4", "lodash": "^4.17.4",
"moment": "^2.20.1", "moment": "^2.20.1",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",