diff --git a/package.json b/package.json index 86c8535..bbf557f 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "react-native-htmlview": "^0.12.0", "react-navigation": "^1.0.0-beta.11", "react-redux": "^5.0.5", - "redux": "^3.7.2" + "redux": "^3.7.2", + "redux-thunk": "^2.2.0" }, "devDependencies": { "babel-jest": "20.0.3", @@ -26,7 +27,9 @@ }, "jest": { "preset": "react-native", - "setupFiles": ["/jest/setup.js"], + "setupFiles": [ + "/jest/setup.js" + ], "transformIgnorePatterns": [ "node_modules/(?!(jest-)?react-native|react-clone-referenced-element|react-navigation|native-base|@shoutem)" ] diff --git a/src/App.js b/src/App.js index a629875..d5c3b3a 100644 --- a/src/App.js +++ b/src/App.js @@ -1,11 +1,11 @@ import Dusken from "./Dusken"; import React, {Component} from 'react'; import { Provider } from 'react-redux'; -import { createStore } from 'redux' +import { createStore, applyMiddleware } from 'redux'; +import thunk from 'redux-thunk'; import duskenApp from "./reducers"; -let store = createStore(duskenApp); - +let store = createStore(duskenApp, applyMiddleware(thunk)); export default class App extends Component { render() { diff --git a/src/Dusken.js b/src/Dusken.js index 62a9c64..88a3a5c 100644 --- a/src/Dusken.js +++ b/src/Dusken.js @@ -2,6 +2,7 @@ import React from 'react'; import {connect} from "react-redux"; import {TabNavigator, StackNavigator} from "react-navigation"; +import LoginScreen from "./screens/LoginScreen"; import EventsScreen from "./screens/EventScreen"; import EventDetailsScreen from "./screens/EventDetailsScreen"; import AboutScreen from "./screens/AboutScreen"; @@ -22,7 +23,8 @@ const tabBarOptions = { const tabNav = TabNavigator(tabBarRoutes, tabBarOptions); const DuskenContainer = StackNavigator({ - Root: { screen: tabNav}, + Root: { screen: tabNav }, + Login: { screen: LoginScreen }, EventDetails: { screen: EventDetailsScreen }, }); diff --git a/src/actions.js b/src/actions.js index 6a02a92..4c42f0b 100644 --- a/src/actions.js +++ b/src/actions.js @@ -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 { - 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 { - LOGIN_START, - loginStart -}; \ No newline at end of file + LOGIN_REQUEST, loginRequest, + 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 :-(')) + } + ) + } +} diff --git a/src/modules/login/Login.js b/src/modules/login/Login.js index 8540739..7c15302 100644 --- a/src/modules/login/Login.js +++ b/src/modules/login/Login.js @@ -1,20 +1,38 @@ import React, { Component } from 'react'; 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 { constructor(props) { super(props); this.state = { - loggedIn: false + isAuthenticated: false, + isLoggingIn: false, + errorMessage: null, + email: '', + password: '' } } componentWillReceiveProps(nextProps) { - if(this.state.loggedIn !== nextProps.loggedIn) { + if(this.state.isAuthenticated !== nextProps.isAuthenticated) { 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 ( - The Norwegian Student Society + Det Norske Studentersamfund Chateau Neuf { this.refs.passwordInput.focus(); }} /> + + 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. 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. @@ -23,12 +27,16 @@ export default class Membership extends Component { ) - } + }; onPurchasePress() { // FIXME: Move this to process.env.DUSKEN_URL and use babel thing to do search replace Linking.openURL('https://medlem.neuf.no'); - } + }; + + onLoginPress = () => { + this.props.onLoginPress(); + }; } diff --git a/src/modules/membership/MembershipContainer.js b/src/modules/membership/MembershipContainer.js index 977d787..979ca82 100644 --- a/src/modules/membership/MembershipContainer.js +++ b/src/modules/membership/MembershipContainer.js @@ -1,5 +1,62 @@ +import React from 'react'; import {connect} from "react-redux"; import Membership from "./Membership"; +import MembershipProof from "./MembershipProof"; +import { requestUserData } from "../../actions"; -export default connect()(Membership); \ No newline at end of file +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 () + } + return () + } +} + +export default connect( + (state) => ({ + isAuthenticated: state.isAuthenticated, + isFetchingUserData: state.isFetchingUserData, + userToken: state.userToken, + user: state.user, + }), + { requestUserData } +)(MembershipContainer); diff --git a/src/modules/membership/MembershipProof.js b/src/modules/membership/MembershipProof.js new file mode 100644 index 0000000..c8378f7 --- /dev/null +++ b/src/modules/membership/MembershipProof.js @@ -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 ( + // ) + // } + + render() { + return ( + + + + {this.state.user.first_name} {this.state.user.last_name} + + + {this.statusText()} + + + Gyldig til: {this.validTo()} + + + + ) + }; +} + +const styles = StyleSheet.create({ + nameText: { + flex: 1, + textAlign: 'center', + fontSize: 20, + fontWeight: 'bold', + }, +}); \ No newline at end of file diff --git a/src/reducers.js b/src/reducers.js index 605bcb2..e2539bd 100644 --- a/src/reducers.js +++ b/src/reducers.js @@ -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 = { - loggedIn: false, + isAuthenticated: false, + isLoggingIn: false, + isFetchingUserData: false, + userToken: null, + user: null, }; export default function duskenApp(state = initialState, action) { switch (action.type) { - case LOGIN_START: + case LOGIN_REQUEST: 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: return state } -} \ No newline at end of file +} diff --git a/src/screens/LoginScreen.js b/src/screens/LoginScreen.js new file mode 100644 index 0000000..c1ab827 --- /dev/null +++ b/src/screens/LoginScreen.js @@ -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 ( + + ) + } +} \ No newline at end of file diff --git a/src/screens/MembershipScreen.js b/src/screens/MembershipScreen.js index cad5df5..8188632 100644 --- a/src/screens/MembershipScreen.js +++ b/src/screens/MembershipScreen.js @@ -10,9 +10,13 @@ export default class MembershipScreen extends Component { ), }; + onLoginPress = () => { + this.props.navigation.navigate('Login'); + }; + render() { return ( - + ) } } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 78c4f65..c44cadf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3258,9 +3258,9 @@ react-native-drawer-layout@1.3.2: dependencies: 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" - resolved "git+https://github.com/GeekyAnts/react-native-drawer.git#8d9078516d177c9cb02d2579a6860706d2370d25" + resolved "https://github.com/GeekyAnts/react-native-drawer#8d9078516d177c9cb02d2579a6860706d2370d25" dependencies: prop-types "^15.5.8" tween-functions "^1.0.1" @@ -3278,9 +3278,9 @@ react-native-htmlview@^0.12.0: entities "^1.1.1" 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" - 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: create-react-class "^15.6.0" prop-types "^15.5.10" @@ -3507,6 +3507,10 @@ rebound@^0.0.13: version "0.0.13" 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: version "3.7.2" resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"