Add login. Fetch user data. Learned basic React + Redux. Needs work.

This commit is contained in:
ponas committed 2017-08-06 21:33:48 +02:00
1 parent cc6c9e4973
commit d4d3f12aea
13 files changed
+433 -47

No files matched your search

+5 -2
View File
@@ -16,7 +16,8 @@
"react-native-htmlview": "^0.12.0", "react-native-htmlview": "^0.12.0",
"react-navigation": "^1.0.0-beta.11", "react-navigation": "^1.0.0-beta.11",
"react-redux": "^5.0.5", "react-redux": "^5.0.5",
"redux": "^3.7.2" "redux": "^3.7.2",
"redux-thunk": "^2.2.0"
}, },
"devDependencies": { "devDependencies": {
"babel-jest": "20.0.3", "babel-jest": "20.0.3",
@@ -26,7 +27,9 @@
}, },
"jest": { "jest": {
"preset": "react-native", "preset": "react-native",
"setupFiles": ["<rootDir>/jest/setup.js"], "setupFiles": [
"<rootDir>/jest/setup.js"
],
"transformIgnorePatterns": [ "transformIgnorePatterns": [
"node_modules/(?!(jest-)?react-native|react-clone-referenced-element|react-navigation|native-base|@shoutem)" "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|react-navigation|native-base|@shoutem)"
] ]
+3 -3
View File
@@ -1,11 +1,11 @@
import Dusken from "./Dusken"; import Dusken from "./Dusken";
import React, {Component} from 'react'; import React, {Component} from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { createStore } from 'redux' import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import duskenApp from "./reducers"; import duskenApp from "./reducers";
let store = createStore(duskenApp); let store = createStore(duskenApp, applyMiddleware(thunk));
export default class App extends Component { export default class App extends Component {
render() { render() {
+3 -1
View File
@@ -2,6 +2,7 @@ import React from 'react';
import {connect} from "react-redux"; import {connect} from "react-redux";
import {TabNavigator, StackNavigator} from "react-navigation"; import {TabNavigator, StackNavigator} from "react-navigation";
import LoginScreen from "./screens/LoginScreen";
import EventsScreen from "./screens/EventScreen"; import EventsScreen from "./screens/EventScreen";
import EventDetailsScreen from "./screens/EventDetailsScreen"; import EventDetailsScreen from "./screens/EventDetailsScreen";
import AboutScreen from "./screens/AboutScreen"; import AboutScreen from "./screens/AboutScreen";
@@ -22,7 +23,8 @@ const tabBarOptions = {
const tabNav = TabNavigator(tabBarRoutes, tabBarOptions); const tabNav = TabNavigator(tabBarRoutes, tabBarOptions);
const DuskenContainer = StackNavigator({ const DuskenContainer = StackNavigator({
Root: { screen: tabNav}, Root: { screen: tabNav },
Login: { screen: LoginScreen },
EventDetails: { screen: EventDetailsScreen }, EventDetails: { screen: EventDetailsScreen },
}); });
+124 -6
View File
@@ -1,11 +1,129 @@
const LOGIN_START = 'LOGIN_START'; const LOGIN_REQUEST = 'LOGIN_REQUEST';
const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
const LOGIN_FAILURE = 'LOGIN_FAILURE';
const LOGOUT = 'LOGOUT';
const USER_DATA_REQUEST = 'USER_DATA_REQUEST';
const USER_DATA_SUCCESS = 'USER_DATA_SUCCESS';
const USER_DATA_FAILURE = 'USER_DATA_FAILURE';
function loginStart() { function loginRequest() {
return { return {
type: LOGIN_START type: LOGIN_REQUEST
} }
} }
function loginSuccess(username, token) {
return {
type: LOGIN_SUCCESS,
username,
token
}
}
function loginFailure(errorMessage) {
return {
type: LOGIN_FAILURE,
errorMessage
}
}
function logout() {
return {
type: LOGOUT
}
}
function userDataRequest() {
return {
type: USER_DATA_REQUEST
}
}
function userDataSuccess(data) {
return {
type: USER_DATA_SUCCESS,
data
}
}
function userDataFailure(errorMessage) {
return {
type: USER_DATA_FAILURE,
errorMessage
}
}
export { export {
LOGIN_START, LOGIN_REQUEST, loginRequest,
loginStart LOGIN_SUCCESS, loginSuccess,
}; LOGIN_FAILURE, loginFailure,
LOGOUT, logout,
USER_DATA_REQUEST, userDataRequest,
USER_DATA_SUCCESS, userDataSuccess,
USER_DATA_FAILURE, userDataFailure,
};
export function requestLogin(username, password) {
return (dispatch) => {
// We are now logging in
dispatch(loginRequest())
return fetch('http://10.0.0.10:3000/auth/obtain-token/', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'username': username,
'password': password
})
})
.then(response => response.json().then(json => ({ json, response })))
.then(({json, response}) => {
if (response.ok === false) {
return Promise.reject(json)
}
return json
})
.then(
data => {
dispatch(loginSuccess(username, data.token))
},
data => {
dispatch(loginFailure(data.error || 'Log in failed'))
}
)
}
}
export function requestUserData(token) {
return (dispatch) => {
// We are now fetching user data
dispatch(userDataRequest())
return fetch('http://10.0.0.10:3000/api/me/', {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Token ' + token
}
})
.then(response => response.json().then(json => ({ json, response })))
.then(({json, response}) => {
if (response.ok === false) {
return Promise.reject(json)
}
return json
})
.then(
data => {
dispatch(userDataSuccess(data))
},
data => {
dispatch(userDataFailure('Det funka ikke :-('))
}
)
}
}
+48 -18
View File
@@ -1,20 +1,38 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import {Button, Image, StyleSheet, Text, TextInput, View} from 'react-native'; import {Button, Image, StyleSheet, Text, TextInput, View} from 'react-native';
import { Spinner } from 'native-base';
import { loginStart } from "../../actions"; // import { login } from "../../actions";
export default class Login extends Component { export default class Login extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
loggedIn: false isAuthenticated: false,
isLoggingIn: false,
errorMessage: null,
email: '',
password: ''
} }
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
if(this.state.loggedIn !== nextProps.loggedIn) { if(this.state.isAuthenticated !== nextProps.isAuthenticated) {
this.setState({ this.setState({
loggedIn: nextProps.loggedIn isAuthenticated: nextProps.isAuthenticated
})
if (nextProps.isAuthenticated) {
this.onLogin();
}
}
if(this.state.isLoggingIn !== nextProps.isLoggingIn) {
this.setState({
isLoggingIn: nextProps.isLoggingIn
})
}
if(this.state.errorMessage !== nextProps.errorMessage) {
this.setState({
errorMessage: nextProps.errorMessage
}) })
} }
} }
@@ -25,48 +43,60 @@ export default class Login extends Component {
}; };
return ( return (
<View style={styles.container}> <View style={styles.container}>
<Text style={styles.text}>The Norwegian Student Society</Text> <Text style={styles.text}>Det Norske Studentersamfund</Text>
<Text style={styles.text}>Chateau Neuf</Text> <Text style={styles.text}>Chateau Neuf</Text>
<Image source={pic} style={{width: 200, height: 150, marginBottom: 40}} resizeMode='contain' /> <Image source={pic} style={{width: 200, height: 150, marginBottom: 40}} resizeMode='contain' />
<TextInput <TextInput
placeholder="Email" placeholder="E-post"
style={styles.input} style={styles.input}
keyboardType="email-address" keyboardType="email-address"
autoCapitalize="none" autoCapitalize="none"
returnKeyType="next" returnKeyType="next"
onChangeText={this.handleEmail}
onSubmitEditing={(event) => { onSubmitEditing={(event) => {
this.refs.passwordInput.focus(); this.refs.passwordInput.focus();
}} }}
/> />
<TextInput <TextInput
placeholder="Password" placeholder="Passord"
style={styles.input} style={styles.input}
secureTextEntry={true} secureTextEntry={true}
autoCapitalize="none" autoCapitalize="none"
ref='passwordInput' ref='passwordInput'
onChangeText={this.handlePassword}
onSubmitEditing={this.onLoginPress} onSubmitEditing={this.onLoginPress}
/> />
<View style={styles.button}> <View style={styles.button}>
<Button <Button
title="Log in" title="Logg inn"
onPress={this.onLoginPress} onPress={this.onLoginPress}
/> />
</View> </View>
<Text style={{paddingTop: 10}}>{this.loginState()}</Text> {this.spinIfLoggingIn}
</View> </View>
); );
} }
onLoginPress = () => { spinIfLoggingIn = () => {
console.log("YO from onLoginPress"); if (this.state.isLoggingIn) {
this.props.dispatch(loginStart()) return <Spinner color="#f58220"></Spinner>
}
}
handleEmail = (text) => {
this.setState({ email: text })
}; };
loginState() { handlePassword = (text) => {
if(this.state.loggedIn) { this.setState({ password: text })
return 'LOGGED IN' };
}
return 'NOT YET' onLoginPress = () => {
this.props.requestLogin(this.state.email, this.state.password)
};
onLogin = () => {
this.props.onLogin();
} }
} }
@@ -78,7 +108,7 @@ const styles = StyleSheet.create({
backgroundColor: '#F5FCFF', backgroundColor: '#F5FCFF',
}, },
input: { input: {
width: 240, width: 320,
fontSize: 20, fontSize: 20,
height: 52, height: 52,
}, },
+9 -4
View File
@@ -1,8 +1,13 @@
import {connect} from "react-redux"; import {connect} from "react-redux";
import { bindActionCreators } from "redux";
import Login from "./Login"; import Login from "./Login";
import { requestLogin } from "../../actions";
export default connect( export default connect(
state => ({ (state) => ({
loggedIn: state.loggedIn isAuthenticated: state.isAuthenticated,
}) isLoggingIn: state.isLoggingIn,
)(Login); errorMessage: state.errorMessage,
}),
{ requestLogin }
)(Login);
+10 -2
View File
@@ -7,6 +7,10 @@ export default class Membership extends Component {
return ( return (
<ScrollView> <ScrollView>
<View style={styles.card}> <View style={styles.card}>
<View style={styles.button}>
<Button onPress={this.onLoginPress} full><NBText>Logg inn</NBText></Button>
</View>
<Text style={styles.paragraph}>Siden 1813 har Det Norske Studentersamfund samlet studenter til kulturell, politisk, faglig og ikke minst sosial aktivitet. Som medlem i DNS er du med på å understreke viktigheten til en felles møteplass for alle Oslos studenter, uavhengig av utdanningsinstitusjon, studie og hvem de er.</Text> <Text style={styles.paragraph}>Siden 1813 har Det Norske Studentersamfund samlet studenter til kulturell, politisk, faglig og ikke minst sosial aktivitet. Som medlem i DNS er du med på å understreke viktigheten til en felles møteplass for alle Oslos studenter, uavhengig av utdanningsinstitusjon, studie og hvem de er.</Text>
<Text style={styles.paragraph}>Vi setter pris på felleskapet som er med på å bygge Chateau Neuf. DNS er en organisasjon av og for medlemmene. Derfor sørger vi for at det blir litt lettere å være student ved å ta en del av kaka for medlemmene våre. Med medlemskap får billigere inngang og servering på alt som skjer på Chateau Neuf.</Text> <Text style={styles.paragraph}>Vi setter pris på felleskapet som er med på å bygge Chateau Neuf. DNS er en organisasjon av og for medlemmene. Derfor sørger vi for at det blir litt lettere å være student ved å ta en del av kaka for medlemmene våre. Med medlemskap får billigere inngang og servering på alt som skjer på Chateau Neuf.</Text>
@@ -23,12 +27,16 @@ export default class Membership extends Component {
</View> </View>
</ScrollView> </ScrollView>
) )
} };
onPurchasePress() { onPurchasePress() {
// FIXME: Move this to process.env.DUSKEN_URL and use babel thing to do search replace // FIXME: Move this to process.env.DUSKEN_URL and use babel thing to do search replace
Linking.openURL('https://medlem.neuf.no'); Linking.openURL('https://medlem.neuf.no');
} };
onLoginPress = () => {
this.props.onLoginPress();
};
} }
+58 -1
View File
@@ -1,5 +1,62 @@
import React from 'react';
import {connect} from "react-redux"; import {connect} from "react-redux";
import Membership from "./Membership"; import Membership from "./Membership";
import MembershipProof from "./MembershipProof";
import { requestUserData } from "../../actions";
export default connect()(Membership); class MembershipContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
isAuthenticated: props.isAuthenticated,
userToken: props.userToken,
isFetchingUserData: props.isFetchingUserData,
user: props.user,
}
}
componentDidMount() {
this.fetchUser();
}
componentWillReceiveProps(nextProps) {
if(this.state.isAuthenticated !== nextProps.isAuthenticated) {
this.setState({isAuthenticated: nextProps.isAuthenticated})
}
if(this.state.userToken !== nextProps.userToken) {
this.setState({userToken: nextProps.userToken})
this.fetchUser();
}
if(this.state.user !== nextProps.user) {
this.setState({user: nextProps.user})
}
}
fetchUser = () => {
if (!this.state.userToken) { return; }
this.props.requestUserData(this.state.userToken)
}
onLoginPress = () => {
this.props.onLoginPress();
}
render() {
if (this.state.user) {
return (<MembershipProof user={this.state.user} fetchUser={this.fetchUser}/>)
}
return (<Membership onLoginPress={this.onLoginPress}/>)
}
}
export default connect(
(state) => ({
isAuthenticated: state.isAuthenticated,
isFetchingUserData: state.isFetchingUserData,
userToken: state.userToken,
user: state.user,
}),
{ requestUserData }
)(MembershipContainer);
+90
View File
@@ -0,0 +1,90 @@
import React, {Component} from 'react';
import {StyleSheet, Platform, RefreshControl} from "react-native";
import {Card, CardItem, Text, Content} from 'native-base';
export default class MembershipProof extends Component {
constructor(props) {
super(props);
this.state = {
user: props.user,
refreshing: false,
}
};
componentWillReceiveProps(nextProps) {
if(this.state.user !== nextProps.user) {
this.setState({user: nextProps.user})
}
}
validTo() {
if (this.state.user.last_membership.membership_type == 'lifelong') {
return 'Livsvarig'
}
return this.state.user.last_membership.end_date;
}
statusText() {
if (!this.state.user.is_member) {
if (!this.state.user.last_membership) {
return 'Ikke medlem'
}
return 'Utløpt'
}
if (this.state.user.is_volunteer) {
return 'Aktiv'
}
return 'Medlem'
}
fetchUser = () => {
this.props.fetchUser()
};
// fetchUser = () => {
// this.props.fetchUser().then(() => {
// this.setState({refreshing: false});
// });
// };
// _onRefresh() {
// console.log('onRefresh')
// this.setState({refreshing: true});
// this.fetchUser()
// }
// refreshControl() {
// return (
// <RefreshControl
// refreshing={this.state.refreshing}
// onRefresh={this._onRefresh.bind(this)}
// />)
// }
render() {
return (
<Content>
<Card>
<CardItem>
<Text style={styles.nameText}>{this.state.user.first_name} {this.state.user.last_name}</Text>
</CardItem>
<CardItem>
<Text>{this.statusText()}</Text>
</CardItem>
<CardItem>
<Text>Gyldig til: {this.validTo()}</Text>
</CardItem>
</Card>
</Content>
)
};
}
const styles = StyleSheet.create({
nameText: {
flex: 1,
textAlign: 'center',
fontSize: 20,
fontWeight: 'bold',
},
});
+51 -5
View File
@@ -1,16 +1,62 @@
import {LOGIN_START} from './actions'; import {
LOGIN_REQUEST,
LOGIN_SUCCESS,
LOGIN_FAILURE,
LOGOUT,
USER_DATA_REQUEST,
USER_DATA_SUCCESS,
USER_DATA_FAILURE,
} from './actions';
const initialState = { const initialState = {
loggedIn: false, isAuthenticated: false,
isLoggingIn: false,
isFetchingUserData: false,
userToken: null,
user: null,
}; };
export default function duskenApp(state = initialState, action) { export default function duskenApp(state = initialState, action) {
switch (action.type) { switch (action.type) {
case LOGIN_START: case LOGIN_REQUEST:
return Object.assign({}, state, { return Object.assign({}, state, {
loggedIn: true // INSTANT LOGIN! isLoggingIn: true
}); });
case LOGIN_FAILURE:
return Object.assign({}, state, {
isLoggingIn: false,
errorMessage: action.errorMessage
});
case LOGIN_SUCCESS:
return Object.assign({}, state, {
isLoggingIn: false,
isAuthenticated: true,
userToken: action.token
});
case LOGOUT:
return initialState
case USER_DATA_REQUEST:
return Object.assign({}, state, {
isFetchingUserData: true
});
case USER_DATA_FAILURE:
return Object.assign({}, state, {
isFetchingUserData: false,
errorMessage: action.errorMessage
});
case USER_DATA_SUCCESS:
return Object.assign({}, state, {
isFetchingUserData: false,
user: action.data
});
default: default:
return state return state
} }
} }
+19
View File
@@ -0,0 +1,19 @@
import React, { Component } from 'react';
import LoginContainer from "../modules/login/LoginContainer";
import Icon from 'react-native-vector-icons/SimpleLineIcons';
export default class LoginScreen extends Component {
static navigationOptions = {
title: 'Innlogging',
};
onLogin = () => {
this.props.navigation.navigate('Membership');
};
render() {
return (
<LoginContainer onLogin={this.onLogin} />
)
}
}
+5 -1
View File
@@ -10,9 +10,13 @@ export default class MembershipScreen extends Component {
), ),
}; };
onLoginPress = () => {
this.props.navigation.navigate('Login');
};
render() { render() {
return ( return (
<MembershipContainer /> <MembershipContainer onLoginPress={this.onLoginPress} />
) )
} }
} }
+8 -4
View File
@@ -3258,9 +3258,9 @@ react-native-drawer-layout@1.3.2:
dependencies: dependencies:
react-native-dismiss-keyboard "1.0.0" react-native-dismiss-keyboard "1.0.0"
"react-native-drawer@git+https://github.com/GeekyAnts/react-native-drawer.git": "react-native-drawer@https://github.com/GeekyAnts/react-native-drawer":
version "2.3.0" version "2.3.0"
resolved "git+https://github.com/GeekyAnts/react-native-drawer.git#8d9078516d177c9cb02d2579a6860706d2370d25" resolved "https://github.com/GeekyAnts/react-native-drawer#8d9078516d177c9cb02d2579a6860706d2370d25"
dependencies: dependencies:
prop-types "^15.5.8" prop-types "^15.5.8"
tween-functions "^1.0.1" tween-functions "^1.0.1"
@@ -3278,9 +3278,9 @@ react-native-htmlview@^0.12.0:
entities "^1.1.1" entities "^1.1.1"
htmlparser2-without-node-native "^3.9.0" htmlparser2-without-node-native "^3.9.0"
"react-native-keyboard-aware-scroll-view@git+https://github.com/GeekyAnts/react-native-keyboard-aware-scroll-view.git": "react-native-keyboard-aware-scroll-view@https://github.com/GeekyAnts/react-native-keyboard-aware-scroll-view":
version "0.2.9" version "0.2.9"
resolved "git+https://github.com/GeekyAnts/react-native-keyboard-aware-scroll-view.git#f4805f254d6cc8e3329f5fb224944401f66655db" resolved "https://github.com/GeekyAnts/react-native-keyboard-aware-scroll-view#f4805f254d6cc8e3329f5fb224944401f66655db"
dependencies: dependencies:
create-react-class "^15.6.0" create-react-class "^15.6.0"
prop-types "^15.5.10" prop-types "^15.5.10"
@@ -3507,6 +3507,10 @@ rebound@^0.0.13:
version "0.0.13" version "0.0.13"
resolved "https://registry.yarnpkg.com/rebound/-/rebound-0.0.13.tgz#4a225254caf7da756797b19c5817bf7a7941fac1" resolved "https://registry.yarnpkg.com/rebound/-/rebound-0.0.13.tgz#4a225254caf7da756797b19c5817bf7a7941fac1"
redux-thunk@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5"
redux@^3.7.2: redux@^3.7.2:
version "3.7.2" version "3.7.2"
resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b" resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"