From 9ab8061cc3f49544c7b1fdc611d6482327a9938d Mon Sep 17 00:00:00 2001 From: Nikolai R Kristiansen Date: Tue, 3 Oct 2017 02:07:04 +0200 Subject: [PATCH] First working stripe charge Refactored Proof, ProofContainer and MembershipContainer Minor fixes --- src/Dusken.js | 2 - src/actions.js | 132 ++++------------ src/api.js | 146 ++++++++++++++++++ src/modules/login/LoginContainer.js | 2 +- src/modules/membership/Charge.js | 27 ---- src/modules/membership/MembershipContainer.js | 30 ++-- src/modules/membership/Proof.js | 56 ++----- src/modules/membership/ProofContainer.js | 71 ++++++++- src/modules/register/UserRegister.js | 2 +- src/modules/register/UserRegisterContainer.js | 2 +- src/reducers.js | 24 ++- src/screens/ChargeScreen.js | 30 ---- src/screens/MembershipScreen.js | 7 +- src/screens/UserRegisterScreen.js | 2 +- 14 files changed, 297 insertions(+), 236 deletions(-) create mode 100644 src/api.js delete mode 100644 src/modules/membership/Charge.js delete mode 100644 src/screens/ChargeScreen.js diff --git a/src/Dusken.js b/src/Dusken.js index aa5c26a..df46362 100644 --- a/src/Dusken.js +++ b/src/Dusken.js @@ -9,7 +9,6 @@ import EventListScreen from "./screens/EventListScreen"; import LoginScreen from "./screens/LoginScreen"; import MembershipScreen from "./screens/MembershipScreen"; import UserRegisterScreen from "./screens/UserRegisterScreen"; -import ChargeScreen from "./screens/ChargeScreen"; /* Navigation */ @@ -49,7 +48,6 @@ const DuskenContainer = StackNavigator({ Root: { screen: tabNav }, Login: { screen: LoginScreen }, UserRegister: { screen: UserRegisterScreen }, - Charge: { screen: ChargeScreen }, EventDetail: { screen: EventDetailScreen }, }); diff --git a/src/actions.js b/src/actions.js index b2cf622..55f4c85 100644 --- a/src/actions.js +++ b/src/actions.js @@ -1,5 +1,3 @@ -import Config from 'react-native-config'; - const LOGIN_REQUEST = 'LOGIN_REQUEST'; const LOGIN_SUCCESS = 'LOGIN_SUCCESS'; const LOGIN_FAILURE = 'LOGIN_FAILURE'; @@ -10,6 +8,9 @@ const USER_DATA_FAILURE = 'USER_DATA_FAILURE'; const REGISTER_USER_REQUEST = 'REGISTER_USER_REQUEST'; const REGISTER_USER_SUCCESS = 'REGISTER_USER_SUCCESS'; const REGISTER_USER_FAILURE = 'REGISTER_USER_FAILURE'; +const MEMBERSHIP_CHARGE_REQUEST = 'MEMBERSHIP_CHARGE_REQUEST'; +const MEMBERSHIP_CHARGE_SUCCESS = 'MEMBERSHIP_CHARGE_SUCCESS'; +const MEMBERSHIP_CHARGE_FAILURE = 'MEMBERSHIP_CHARGE_FAILURE'; function loginRequest() { return { @@ -77,6 +78,25 @@ function registerUserFailure(registerError) { registerError } } +function membershipChargeRequest() { + return { + type: MEMBERSHIP_CHARGE_REQUEST + } +} + +function membershipChargeSuccess(data) { + return { + type: MEMBERSHIP_CHARGE_SUCCESS, + data + } +} + +function membershipChargeFailure(chargeError) { + return { + type: MEMBERSHIP_CHARGE_FAILURE, + chargeError + } +} export { LOGIN_REQUEST, loginRequest, @@ -89,107 +109,7 @@ export { REGISTER_USER_REQUEST, registerUserRequest, REGISTER_USER_SUCCESS, registerUserSuccess, REGISTER_USER_FAILURE, registerUserFailure, -}; - -export function requestLogin(username, password) { - return (dispatch) => { - // We are now logging in - dispatch(loginRequest()); - return fetch(`${Config.DUSKEN_API_URL}/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 => { - console.log(data); - dispatch(loginFailure(data || {'non_field_errors': ['Kunne ikke logge inn, prøv igjen']})) - } - ) - } -} - -export function requestUserData(token) { - return (dispatch) => { - // We are now fetching user data - dispatch(userDataRequest()); - - const apiURL = Config.DUSKEN_API_URL; - return fetch(`${apiURL}/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 :-(')) - } - ) - } -} - -export function requestRegisterUser(firstName, lastName, email, phoneNumber, password) { - return (dispatch) => { - dispatch(registerUserRequest()); - - return fetch(`${Config.DUSKEN_API_URL}/api/user/register/`, { - method: 'POST', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - 'first_name': firstName, - 'last_name': lastName, - 'email': email, - 'phone_number': phoneNumber, - '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(registerUserSuccess(data)) - }, - data => { - dispatch(registerUserFailure(data || {'non_field_errors': ['Kunne ikke registrere bruker, prøv igjen']})) - } - ) - } -} - + MEMBERSHIP_CHARGE_REQUEST, membershipChargeRequest, + MEMBERSHIP_CHARGE_SUCCESS, membershipChargeSuccess, + MEMBERSHIP_CHARGE_FAILURE, membershipChargeFailure, +}; \ No newline at end of file diff --git a/src/api.js b/src/api.js new file mode 100644 index 0000000..133c5ca --- /dev/null +++ b/src/api.js @@ -0,0 +1,146 @@ +import Config from 'react-native-config'; + +import { + loginFailure, loginRequest, loginSuccess, + registerUserFailure, registerUserRequest, registerUserSuccess, + userDataFailure, userDataRequest, userDataSuccess, + membershipChargeFailure, membershipChargeRequest, membershipChargeSuccess, +} from "./actions"; + +export function requestLogin(username, password) { + return (dispatch) => { + // We are now logging in + dispatch(loginRequest()); + return fetch(`${Config.DUSKEN_API_URL}/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 => { + console.log(data); + dispatch(loginFailure(data || {'non_field_errors': ['Kunne ikke logge inn, prøv igjen']})) + } + ) + } +} + +export function requestUserData(auth_token) { + return (dispatch) => { + // We are now fetching user data + dispatch(userDataRequest()); + + return fetch(`${Config.DUSKEN_API_URL}/api/me/`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': `Token ${auth_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 :-(')) + } + ) + } +} + +export function requestRegisterUser(firstName, lastName, email, phoneNumber, password) { + return (dispatch) => { + dispatch(registerUserRequest()); + + return fetch(`${Config.DUSKEN_API_URL}/api/user/register/`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + 'first_name': firstName, + 'last_name': lastName, + 'email': email, + 'phone_number': phoneNumber, + '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(registerUserSuccess(data)) + }, + data => { + dispatch(registerUserFailure(data || {'non_field_errors': ['Kunne ikke registrere bruker, prøv igjen']})) + } + ) + } +} + +export function requestMembershipCharge(auth_token, stripe_token, membership_type) { + return (dispatch) => { + dispatch(membershipChargeRequest()); + + return fetch(`${Config.DUSKEN_API_URL}/api/membership/charge/`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': `Token ${auth_token}` + }, + body: JSON.stringify({ + 'stripe_token': stripe_token, + 'membership_type': membership_type, + 'payment_method': 'app', + }) + }) + .then(response => response.json().then(json => ({ json, response }))) + .then(({json, response}) => { + if (response.ok === false) { + return Promise.reject(json) + } + return json + }) + .then( + data => { + dispatch(membershipChargeSuccess(data)) + }, + data => { + dispatch(membershipChargeFailure(data || {'non_field_errors': ['Kunne ikke belaste kort, prøv igjen']})) + } + ) + } +} + + diff --git a/src/modules/login/LoginContainer.js b/src/modules/login/LoginContainer.js index 55f61cb..0eb40fc 100644 --- a/src/modules/login/LoginContainer.js +++ b/src/modules/login/LoginContainer.js @@ -1,7 +1,7 @@ import React, {Component} from 'react'; import {connect} from "react-redux"; import Login from "./Login"; -import {requestLogin} from "../../actions"; +import {requestLogin} from "../../api"; class LoginContainer extends Component { constructor(state) { diff --git a/src/modules/membership/Charge.js b/src/modules/membership/Charge.js deleted file mode 100644 index eda16e2..0000000 --- a/src/modules/membership/Charge.js +++ /dev/null @@ -1,27 +0,0 @@ -import stripe from 'tipsi-stripe' -import React, {Component} from 'react'; -import Config from 'react-native-config'; -import {ActivityIndicator, Text, View} from "react-native"; -// import {ScrollView, Text, View, Linking, StyleSheet, Platform} from "react-native"; -// import {Button, Text as NBText} from "native-base"; - -stripe.init({ - publishableKey: Config.STRIPE_PUBLISHABLE_KEY, -}); - -export default class Charge extends Component { - // TODO: You are here - render() { - return - In charge view - - - } - - openStripe() { - stripe.createTokenWithCard().then((data) => { - // returns a token.tokenId which should be saved - - }) - } -} diff --git a/src/modules/membership/MembershipContainer.js b/src/modules/membership/MembershipContainer.js index 729d403..9017461 100644 --- a/src/modules/membership/MembershipContainer.js +++ b/src/modules/membership/MembershipContainer.js @@ -2,8 +2,9 @@ import React, {Component} from 'react'; import {connect} from "react-redux"; import Membership from "./Membership"; -import Proof from "./Proof"; -import { requestUserData, logout } from "../../actions"; +import { logout } from "../../actions"; +import {requestUserData} from "../../api"; +import ProofContainer from "./ProofContainer"; class MembershipContainer extends Component { constructor(props) { @@ -11,9 +12,9 @@ class MembershipContainer extends Component { this.state = { isAuthenticated: props.isAuthenticated, - userToken: props.userToken, isFetchingUserData: props.isFetchingUserData, - user: props.user, + userToken: props.userToken, + lastOrder: props.lastOrder, } } @@ -29,8 +30,12 @@ class MembershipContainer extends Component { }); } - if(this.state.user !== nextProps.user) { - this.setState({user: nextProps.user}) + if(this.state.lastOrder !== nextProps.lastOrder) { + this.setState({lastOrder: nextProps.lastOrder}, () => { + if(nextProps.userToken) { + this.fetchUser(); + } + }); } } @@ -46,22 +51,18 @@ class MembershipContainer extends Component { this.props.onRegisterPress(); }; - onChargePress = () => { - this.props.onChargePress(); - }; - onLogoutPress = () => { this.props.logout(); this.props.onLogoutPress(); }; render() { - if (this.state.user) { - return ; } return ; @@ -74,6 +75,7 @@ function stateToProps(state) { isFetchingUserData: state.isFetchingUserData, userToken: state.userToken, user: state.user, + lastOrder: state.lastOrder }; } diff --git a/src/modules/membership/Proof.js b/src/modules/membership/Proof.js index 3894c66..f536e55 100644 --- a/src/modules/membership/Proof.js +++ b/src/modules/membership/Proof.js @@ -1,27 +1,18 @@ import React, {Component} from 'react'; -import {Image, Linking, StyleSheet, Platform, RefreshControl, TouchableOpacity, View} from "react-native"; +import {Image, StyleSheet, TouchableOpacity, View} from "react-native"; import {Button, Body, Card, CardItem, Text, Content} from 'native-base'; import Confetti from 'react-native-confetti'; export default class Proof extends Component { - // FIXME: Move logic to container CONFETTI_TIMEOUT = 15000; constructor(props) { super(props); this.state = { - user: props.user, - refreshing: false, stopConfetti: false, } }; - componentWillReceiveProps(nextProps) { - if(this.state.user !== nextProps.user) { - this.setState({user: nextProps.user}) - } - } - componentDidUpdate () { if (this._confettiView && this.state.stopConfetti) { this._confettiView.stopConfetti(); @@ -35,26 +26,6 @@ export default class Proof extends Component { } } - // fetchUser = () => { - // this.props.fetchUser().then(() => { - // this.setState({refreshing: false}); - // }); - // }; - - // _onRefresh() { - // console.log('onRefresh') - // this.setState({refreshing: true}); - // this.fetchUser() - // } - - // refreshControl() { - // return ( - // ) - // } - onMembershipPress = () => { this.setState({showConfetti: true}, () => { if(this._confettiView) { @@ -67,12 +38,12 @@ export default class Proof extends Component { }; membershipValidTo() { - if( !this.state.user.last_membership) { + if( !this.props.user.last_membership) { return; } - let validTo = this.state.user.last_membership.end_date; - if (this.state.user.last_membership.membership_type === 'lifelong') { + let validTo = this.props.user.last_membership.end_date; + if (this.props.user.last_membership.membership_type === 'lifelong') { validTo = 'Livsvarig' } @@ -87,8 +58,8 @@ export default class Proof extends Component { } membershipStatus() { - if (!this.state.user.is_member) { - if ( !this.state.user.last_membership ) { + if (!this.props.user.is_member) { + if ( !this.props.user.last_membership ) { return ( @@ -101,7 +72,7 @@ export default class Proof extends Component { } let statusText = 'Medlem'; - if (this.state.user.is_volunteer) { + if (this.props.user.is_volunteer) { statusText = 'Aktiv'; } @@ -114,7 +85,7 @@ export default class Proof extends Component { } purchaseButton() { - if (this.state.user.is_member) { + if (this.props.user.is_member) { return; } return @@ -131,14 +102,14 @@ export default class Proof extends Component { } confetti() { - if (this.state.user.is_member) { + if (this.props.user.is_member) { // FIXME: allmost infinite confetti. Are the animations paused when not visible, if not power hog? return this._confettiView = node} confettiCount={Number.MAX_SAFE_INTEGER} /> } } logo() { - if (!this.state.user.is_member) { + if (!this.props.user.is_member) { return; } // FIXME: URL to config @@ -149,9 +120,6 @@ export default class Proof extends Component { /> } - fetchUser = () => { - this.props.fetchUser() - }; render() { return ( @@ -174,9 +142,9 @@ export default class Proof extends Component { }; membershipName() { - let name = `${this.state.user.first_name} ${this.state.user.last_name}`; + let name = `${this.props.user.first_name} ${this.props.user.last_name}`; if(name === ' ') { - name = this.state.user.email; + name = this.props.user.email; } return {name} diff --git a/src/modules/membership/ProofContainer.js b/src/modules/membership/ProofContainer.js index 7ca641d..53afa9d 100644 --- a/src/modules/membership/ProofContainer.js +++ b/src/modules/membership/ProofContainer.js @@ -1,15 +1,82 @@ import React, {Component} from 'react'; import {connect} from "react-redux"; +import stripe from 'tipsi-stripe' +import Config from 'react-native-config'; import Proof from "./Proof"; +import {requestMembershipCharge} from "../../api"; + + +stripe.init({ + publishableKey: Config.STRIPE_PUBLISHABLE_KEY, +}); class ProofContainer extends Component { constructor(props) { super(props); + + this.state = { + refreshing: false, + } } + + componentWillReceiveProps(nextProps) { + if(this.state.user !== nextProps.user) { + this.setState({user: nextProps.user}) + } + } + render() { - return + return } + + openStripe = () => { + const options = { + prefilledInformation: { + email: this.props.user.email, + }, + smsAutofillDisabled: true, + }; + + stripe.paymentRequestWithCardForm(options).then( + (token) => { + // returns a token.tokenId which should be saved + this.props.requestMembershipCharge( + this.props.userToken, + {id: token.tokenId, email: this.props.user.email}, + 'standard'); + }, + (error) => { + // TODO: display error + console.log(error); + } + ); + }; + + // fetchUser = () => { + // this.props.fetchUser().then(() => { + // this.setState({refreshing: false}); + // }); + // }; + + // _onRefresh() { + // console.log('onRefresh') + // this.setState({refreshing: true}); + // this.fetchUser() + // } + + // refreshControl() { + // return ( + // ) + // } } -export default connect()(ProofContainer); +export default connect( + (state) => ({ + userToken: state.userToken + }), + {requestMembershipCharge} +)(ProofContainer); diff --git a/src/modules/register/UserRegister.js b/src/modules/register/UserRegister.js index e018f5f..0d89db8 100644 --- a/src/modules/register/UserRegister.js +++ b/src/modules/register/UserRegister.js @@ -163,7 +163,7 @@ export default class UserRegister extends Component { onPress={this.onRegisterPress} style={styles.registerButton} > - Registrer + Registrer meg ); } } diff --git a/src/modules/register/UserRegisterContainer.js b/src/modules/register/UserRegisterContainer.js index ff85fcf..aa6c47a 100644 --- a/src/modules/register/UserRegisterContainer.js +++ b/src/modules/register/UserRegisterContainer.js @@ -1,7 +1,7 @@ import React, {Component} from 'react'; import {connect} from "react-redux"; import UserRegister from "./UserRegister"; -import {requestRegisterUser} from "../../actions"; +import {requestRegisterUser} from "../../api"; class UserRegisterContainer extends Component { diff --git a/src/reducers.js b/src/reducers.js index 943d4bc..abf1dac 100644 --- a/src/reducers.js +++ b/src/reducers.js @@ -8,7 +8,7 @@ import { USER_DATA_FAILURE, REGISTER_USER_REQUEST, REGISTER_USER_FAILURE, - REGISTER_USER_SUCCESS, + REGISTER_USER_SUCCESS, MEMBERSHIP_CHARGE_REQUEST, MEMBERSHIP_CHARGE_FAILURE, MEMBERSHIP_CHARGE_SUCCESS, } from './actions'; const initialState = { @@ -16,11 +16,14 @@ const initialState = { isLoggingIn: false, isFetchingUserData: false, isRegisteringUser: false, + isChargingMembership: false, userToken: null, user: null, loginError: null, userError: null, registerError: null, + chargeError: null, + lastOrder: null, }; export default function duskenApp(state = initialState, action) { @@ -92,6 +95,25 @@ export default function duskenApp(state = initialState, action) { userToken: token, }); + /* User data */ + case MEMBERSHIP_CHARGE_REQUEST: + return Object.assign({}, state, { + isChargingMembership: true, + chargeError: null + }); + + case MEMBERSHIP_CHARGE_FAILURE: + return Object.assign({}, state, { + isChargingMembership: false, + chargeError: action.chargeError + }); + + case MEMBERSHIP_CHARGE_SUCCESS: + return Object.assign({}, state, { + isChargingMembership: false, + lastOrder: action.data, + }); + default: return state } diff --git a/src/screens/ChargeScreen.js b/src/screens/ChargeScreen.js deleted file mode 100644 index ab9d586..0000000 --- a/src/screens/ChargeScreen.js +++ /dev/null @@ -1,30 +0,0 @@ -import React, { Component } from 'react'; -import Charge from "../modules/membership/Charge"; -import {StyleProvider} from "native-base"; -import getTheme from '../../native-base-theme/components'; - -export default class ChargeScreen extends Component { - static navigationOptions = { - title: 'Kjøp medlemskap', - headerStyle: { - backgroundColor: '#f58220', - }, - headerTitleStyle: { - color: 'white', - }, - headerTintColor: 'white', - }; - - onCharged = () => { - // This works since ChargeScreen is allways navigated to from MembershipScreen - this.props.navigation.goBack(); - }; - - render() { - return ( - - - - ) - } -} \ No newline at end of file diff --git a/src/screens/MembershipScreen.js b/src/screens/MembershipScreen.js index e46b6ce..2de3deb 100644 --- a/src/screens/MembershipScreen.js +++ b/src/screens/MembershipScreen.js @@ -31,18 +31,13 @@ export default class MembershipScreen extends Component { this.props.navigation.navigate('UserRegister'); }; - chargeNavigate = () => { - this.props.navigation.navigate('Charge'); - }; - render() { return ( + onRegisterPress={this.registerNavigate} /> ) } diff --git a/src/screens/UserRegisterScreen.js b/src/screens/UserRegisterScreen.js index 315a57d..0b16965 100644 --- a/src/screens/UserRegisterScreen.js +++ b/src/screens/UserRegisterScreen.js @@ -5,7 +5,7 @@ import getTheme from '../../native-base-theme/components'; export default class UserRegisterScreen extends Component { static navigationOptions = { - title: 'Registrer', + title: 'Bli medlem', headerStyle: { backgroundColor: '#f58220', },