diff --git a/app/actions/items.js b/app/actions/items.js
index 01c9ccd..73a1c05 100644
--- a/app/actions/items.js
+++ b/app/actions/items.js
@@ -1,38 +1,39 @@
import { List } from 'immutable';
-import { getEndpointUrl } from '../api/index.js';
-
+import { fetchItems } from '../api/items.js';
import {
GET_ITEMS,
ITEMS_LOADED,
} from '../constants/actionTypes.js';
+import { getActiveEventId } from '../selectors/activeEvent.js';
+import { getLoginToken } from '../selectors/profile.js';
import { blockUI, unblockUI } from './index.js';
-import { API_ENDPOINTS } from '../constants/constants.js';
import Item from '../domain/Item.js';
+const itemsLoaded = (payload) => ({ type: ITEMS_LOADED, payload });
-const itemsLoadSuccess = (items, dispatch) => {
+const itemsLoadError = (payload) => ({ type: ITEMS_LOAD_FAILED, payload });
+
+const itemsFetchSuccess = (items) => (dispatch) => {
const payload = List(items).map((i) => Item.fromJS(i));
- dispatch({ type: ITEMS_LOADED, payload });
+
+ dispatch(itemsLoaded(payload));
+ dispatch(unblockUI);
+};
+
+const itemsFetchFailure = (error) => (dispatch) => {
+ console.error('[actions::getItems]', error));
+ dispatch(itemsLoadFailure(error));
dispatch(unblockUI);
};
export const fetchItems = () => (dispatch, getState) => {
- const state = getState();
- const activeEvent = state.get('activeEvent');
+ const eventId = getActiveEventId(getState());
+ const authToken = getLoginToken(getState());
- let apiUrl = getEndpointUrl(API_ENDPOINTS.GET_ITEMS);
- apiUrl = apiUrl.replace(/:event_id$/, '');
- if (activeEvent) {
- apiUrl = `${apiUrl}${activeEvent}`;
- }
-
- dispatch(blockUI());
-
- fetch(apiUrl)
- .then(response => response.json())
- .then(payload => itemsLoadSuccess(payload, dispatch))
- .catch(err => console.error('[actions::getItems]', err));
+ fetchItems(activeEvent, authToken)
+ .then(payload => dispatch(itemsFetchSuccess(payload)))
+ .catch(err => dispatch(itemsFetchFailure(err));
};
diff --git a/app/actions/profile.js b/app/actions/profile.js
index a926f1b..cd29173 100644
--- a/app/actions/profile.js
+++ b/app/actions/profile.js
@@ -1,33 +1,89 @@
-import { List } from 'immutable';
-
-import { getEndpointUrl } from '../api/index.js';
+import { blockUI, unblockUI } from './index.js';
import {
- EVENTS_LOADED,
- GET_EVENTS,
+ getEmailAvailability,
+ getNomAvailaibility,
+ registerNewUser,
+} from '../api/profile.js';
+
+import {
+ DO_LOGOUT,
+ PROFILE_EMAIL_AVAILABLE,
+ PROFILE_NOM_AVAILABLE,
+ UPDATE_PROFILE,
} from '../constants/actionTypes.js';
-import { blockUI, unblockUI } from './index.js';
-import { API_ENDPOINTS } from '../constants/constants.js';
-
-import Event from '../domain/Event.js';
-
-
-const eventsLoadSuccess = (events, dispatch) => {
- const payload = List(events).map((i) => Event.fromJS(i));
- dispatch({ type: EVENTS_LOADED, payload });
- dispatch(unblockUI);
-};
-
-export const setActiveEvent = (eventId) => ({
- type: SET_ACTIVE_EVENT,
- payload: eventId,
+const isValidEmail = (payload) => ({
+ type: PROFILE_EMAIL_AVAILABLE,
+ payload,
});
-export const fetchEvents = () => (dispatch) => {
- dispatch(blockUI());
- fetch(getEndpointUr(API_ENDPOINTS.GET_EVENTS))
- .then(response => response.json())
- .then(payload => eventsLoadSuccess(payload, dispatch))
- .catch(err => console.error('[actions::getEvents]', err));
+const isValidNom = (payload) => ({
+ type: PROFILE_NOM_AVAILABLE,
+ payload,
+});
+
+const logoutUser = () => ({
+ type: DO_LOGOUT,
+});
+
+const registrationFailure = (payload) => ({
+ type: REGISTRATION_FAILURE,
+ payload,
+});
+
+const registrationSuccess = (payload) => ({
+ type: REGISTRATION_SUCCESS,
+ payload,
+});
+
+const updateProfile = (profile) => ({
+ type: UPDATE_PROFILE,
+ payload: profile,
+});
+
+export const checkEmailAvailability = (email) => (dispatch) => {
+
+};
+
+export const checkNomAvailability = (nomDeBid) => (dispatch) => {
+
+};
+
+
+export const logout = () => (dispatch) => dispatch(logoutUser());
+
+// USER REGISTRATION
+const handleRegistrationSuccess = (user) => (dispatch) => {
+ dispatch(unblockUI());
+ dispatch(registrationSuccess());
+ dispatch(loginSuccess(user));
+};
+
+const handleRegistrationFailure = (error) => (dispatch) => {
+ dispatch(unblockUI());
+ dispatch(registrationFailure(error));
+};
+
+export const registerUser = (user) => (dispatch) => {
+ dispatch(blockUI());
+ registerNewUser(user)
+ .then(user => dispatch(handleRegistrationSuccess(user)))
+ .catch(err => dispatch(handleRegistrationFailure(err)));
+};
+
+// FACEBOOK
+export const facebookLoginSuccess = (result) => (dispatch) => {
+ console.log('facebookLoginSuccess', result);
+ dispatch(facebookLoginSuccess(result));
+};
+
+
+// LOGIN SERVICES COMMON ACTIONS
+export const serviceRegistrationError = (error) => (dispatch) => {
+
+};
+
+export const serviceRegistrationCanceled = () => (dispatch) => {
+
};
diff --git a/app/api/auction.js b/app/api/auction.js
new file mode 100644
index 0000000..e69de29
diff --git a/app/api/helpers.js b/app/api/helpers.js
new file mode 100644
index 0000000..fbc2f47
--- /dev/null
+++ b/app/api/helpers.js
@@ -0,0 +1,86 @@
+import { API_URL } from '../constants/constants.js';
+
+export const constructUrl = (path, params, host = API_URL) => {
+ if (!(/^\//g).test(path)) {
+ throw new Error(`Invalid path! Expecting a valid relative path (path needs to start with /)(${path})`);
+ }
+
+ let url = path;
+
+ if (host) {
+ url = `${host}${url}`;
+ }
+
+ if (params) {
+ url = `${url}?${params}`;
+ }
+
+ return url;
+};
+
+/**
+ * Formats data for a POST request.
+ *
+ * @param {Object} body - data to be passed to the POST endpoint
+ * @param {Boolean} isTraditional - similar to jQuery's option `traditional` for $.ajax(),
+ * this serializes data in a way that helps support legacy endpoints
+ */
+export const formatPostData = (body) => {
+ const postData = new FormData();
+ Object.keys(body).forEach(key => postData.append(key, body[key]));
+ return postData;
+};
+
+const parseQueryParamsString = (queryParams) => {
+ if (typeof queryParams !== 'string') {
+ return null;
+ }
+
+ return queryParams;
+};
+
+const parseQueryParamsObject = (queryParams) => {
+ if (typeof queryParams !== 'object' && Array.isArray(queryParams)) {
+ return null;
+ }
+
+ return Object
+ .keys(queryParams)
+ .map((key) => {
+ const value = queryParams[key];
+ return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
+ })
+ .join('&');
+};
+
+const parseQueryParamsArray = (queryParams) => {
+ if (!Array.isArray(queryParams)) {
+ return null;
+ }
+
+ return queryParams
+ .map((param) => {
+ const [key, value] = param.split('=');
+ return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
+ })
+ .join('&');
+};
+
+export const parseQueryParams = queryParams => (
+ parseQueryParamsString(queryParams) ||
+ parseQueryParamsObject(queryParams) ||
+ parseQueryParamsArray(queryParams) ||
+ ''
+);
+
+export const request = (url, options) => {
+ try {
+ return fetch(url, options).catch((err) => console.error(err));
+ } catch (error) {
+ return Promise.reject(error);
+ }
+};
+
+export const unwrapJson = (response) => response.json();
+
+export const validateResponse = (response) => response;
diff --git a/app/api/index.js b/app/api/index.js
index 16c05f7..f2cac76 100644
--- a/app/api/index.js
+++ b/app/api/index.js
@@ -1,4 +1,15 @@
-const apiUrl = 'http://localhost:3001';
+import {
+ constructUrl,
+ formatPostData,
+ parseQueryParams,
+ request,
+ unwrapJson,
+ validateResponse,
+} from './helpers.js';
+
+import { API_URL } from '../constants/constants.js';
+
+const DefaultRequestOptions = {};
const endpoints = {
// Events and Items
@@ -35,5 +46,47 @@ export const getEndpointUrl = (endpoint) => {
throw new Error('Invalid API endpoint specified');
}
- return `${apiUrl}${endpoints[endpoint]}`; //`${cacheBuster()}`;
+ return `${API_URL}${endpoints[endpoint]}`; //`${cacheBuster()}`;
+};
+
+export const requestGet = (path, queryParams = [], requestOptions = {}) => {
+ const params = parseQueryParams(queryParams);
+
+ if (params === null) {
+ throw new Error('Invalid queryParams');
+ }
+
+ return request(constructUrl(path, params), {
+ ...DefaultRequestOptions,
+ ...requestOptions
+ })
+ .then(validateResponse)
+ .then(unwrapJson);
+};
+
+export const requestPost = (options) => {
+ const {
+ path,
+ body = {},
+ queryParams = [],
+ requestOptions = {},
+ isFormattedPostData = false
+ } = options;
+
+ const params = parseQueryParams(queryParams || []);
+
+ if (params === null) {
+ throw new Error('invalid queryParams');
+ }
+
+ // Additional formatting happens in `formatPostData()`
+ // like handling what jQuery calls `traditional` form data
+ return request(constructUrl(path, params), {
+ ...DefaultRequestOptions,
+ ...requestOptions,
+ method: 'POST',
+ body: isFormattedPostData ? body : formatPostData(body)
+ })
+ .then(validateResponse)
+ .then(unwrapJson);
};
diff --git a/app/api/items.js b/app/api/items.js
new file mode 100644
index 0000000..79a1580
--- /dev/null
+++ b/app/api/items.js
@@ -0,0 +1,7 @@
+import { getEndpointUrl, requestGet } from './index.js';
+
+export const fetchItems = (eventId, auth) => {
+ const path = String(API_ENDPOINTS.GET_ITEMS).replace(/:event_id$/, eventId);
+ const opts = { Authorization: auth ? `Bearer ${auth}` : null };
+ return requestGet(path, null, opts);
+};
diff --git a/app/api/profile.js b/app/api/profile.js
new file mode 100644
index 0000000..e69de29
diff --git a/app/components/FacebookLogin/FacebookLogin.container.js b/app/components/FacebookLogin/FacebookLogin.container.js
new file mode 100644
index 0000000..4004851
--- /dev/null
+++ b/app/components/FacebookLogin/FacebookLogin.container.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux';
+
+import {
+ facebookLoginSuccess,
+ logout,
+ registrationServiceError,
+ userCanceledRegistration,
+} from '../actions/profile.js';
+
+import FacebookLogin from './FacebookLogin.js';
+
+const mapDispatchToProps = (dispatch) => ({
+ doCancelAction: () => dispatch(userCanceledRegistration()),
+ doErrorAction: (error) => dispatch(registrationServiceError(error)),
+ doLogoutAction: () => dispatch(logout()),
+ doSuccessAction: (result) => dispatch(facebookLoginSuccess(result)),
+});
+
+export default connect(null, mapDispatchToProps)(FacebookLogin);
diff --git a/app/components/FacebookLogin/FacebookLogin.js b/app/components/FacebookLogin/FacebookLogin.js
new file mode 100644
index 0000000..4d749c3
--- /dev/null
+++ b/app/components/FacebookLogin/FacebookLogin.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { View } from 'react-native';
+import { LoginButton } from 'react-native-fbsdk';
+
+import { PERMISSIONS } from '../../constants/constants.js';
+
+export default function FacebookLogin({
+ doCancelAction,
+ doErrorAction,
+ doLogoutAction,
+ doSuccessAction,
+}) {
+
+ return (
+
+ {
+ if (error) {
+ doErrorAction(error);
+ } else if (result.isCancelled) {
+ doCancelAction();
+ } else {
+ doSuccessAction(result);
+ }
+ }
+ }
+ onLogoutFinished={doLogoutAction}
+ />
+
+ );
+}
+
+FacebookLogin.propTypes = {
+ doCancelAction: PropTypes.func.isRequired,
+ doErrorAction: PropTypes.func.isRequired,
+ doLogoutAction: PropTypes.func.isRequired,
+ doSuccessAction: PropTypes.func.isRequired,
+};
diff --git a/app/constants/actionTypes.js b/app/constants/actionTypes.js
index c9979eb..994eb39 100644
--- a/app/constants/actionTypes.js
+++ b/app/constants/actionTypes.js
@@ -19,6 +19,8 @@ export const DO_LOGIN = 'DO_LOGIN';
export const DO_LOGOUT = 'DO_LOOUT';
export const DO_SIGNUP = 'DO_SIGNUP';
+export const PROFILE_EMAIL_AVAILABLE = 'PROFILE_EMAIL_AVAILABLE';
+export const PROFILE_NOM_AVAILABLE = 'PROFILE_NOM_AVAILABLE';
export const DO_SIGNUP_APPLE = 'DO_SIGNUP_APPLE';
export const DO_SIGNUP_FACEBOOK = 'DO_SIGNUP_FACEBOOK';
diff --git a/app/constants/constants.js b/app/constants/constants.js
index 271a04d..77ba6cf 100644
--- a/app/constants/constants.js
+++ b/app/constants/constants.js
@@ -30,6 +30,8 @@ export const AUCTION_VIEW_MODES = {
WINNING: 'WINNING',
};
+export const API_URL = 'http://localhost:3001';
+
export const API_ENDPOINTS = {
GET_EVENTS: 'GET_EVENTS',
GET_ITEMS: 'GET_ITEMS',
@@ -45,3 +47,7 @@ export const API_ENDPOINTS = {
GOOGLE_SIGNUP: 'GOOGLE_SIGNUP',
GOOGLE_LINK: 'GOOGLE_LINK',
};
+
+export const PERMISSIONS = {
+ FACEBOOK: [ 'email', 'public_profile' ],
+};
diff --git a/app/router.js b/app/router.js
index d02717e..9869ed0 100644
--- a/app/router.js
+++ b/app/router.js
@@ -13,8 +13,8 @@ import ImageDetail from './screens/ImageDetail.js';
import Item from './screens/Item.js';
import Marketplace from './screens/Marketplace.js';
import Profile from './screens/Profile.container.js';
-
-//let screen = Dimensions.get('window');
+import Register from './screens/Register.js';
+import SignInOrRegister from './screens/SignInOrRegister.js';
export const Tabs = createBottomTabNavigator({
'Event': {
@@ -47,6 +47,25 @@ export const Tabs = createBottomTabNavigator({
},
});
+export const SignInOrRegisterStack = createStackNavigator({
+ SignInOrRegister: {
+ screen: SignInOrRegister,
+ navigationOptions: ({ navigation }) => {
+ header: null,
+ tabBarVisible: false,
+ gesturesEnabled: false
+ },
+ },
+ Register: {
+ screen: Register,
+ navigationOptions: ({ navigation }) => {
+ header: null,
+ tabBarVisible: false,
+ gesturesEnabled: false
+ },
+ },
+});
+
export const AuctionStack = createStackNavigator({
Auction: {
screen: Auction,
diff --git a/app/screens/Register.container.js b/app/screens/Register.container.js
new file mode 100644
index 0000000..7c23470
--- /dev/null
+++ b/app/screens/Register.container.js
@@ -0,0 +1,17 @@
+import { connect } from 'react-redux';
+
+import {
+ checkEmailAvailability,
+ checkNomAvailability,
+ registerUser,
+} from '../actions/profile.js';
+
+import Register from './Register.js';
+
+const mapDispatchToProps = (dispatch) => ({
+ checkEmail: (email) => dispatch(checkEmailAvailability(email)),
+ checkNomDeBid: (nomDeBid) => dispatch(checkNomAvailability(nomDeBid)),
+ doRegistration: (user) => dispatch(registerUser(user)),
+});
+
+export default connect(null, mapDispatchToProps)(Register);
diff --git a/app/screens/Register.js b/app/screens/Register.js
new file mode 100644
index 0000000..d5cb63a
--- /dev/null
+++ b/app/screens/Register.js
@@ -0,0 +1,90 @@
+import React, { Component } from 'react';
+import { Text, View } from 'react-native';
+
+import styles from './Register.styles.js';
+
+export default class Register extends Component {
+
+ static get propTypes() {
+ return {
+ checkEmail: PropTypes.func.isRequired,
+ checkNomDeBid: PropTypes.func.isRequired,
+ doRegistration: PropTypes.func.isRequired,
+// invalidEmail: PropTypes.bool.isRequired,
+// invalidNomDeBid: PropTypes.bool.isRequired,
+ };
+ }
+
+ constructor() {
+ super(props);
+
+ this.state = {
+ addresses: [],
+ avatar: null,
+ email: null,
+ firstName: null,
+ lastName: null,
+ nomDeBid: null,
+ invalidEmail: false,
+ invalidNomDeBid: false,
+ password: null,
+ phones: [],
+ };
+
+ this._doRegistration = this._doRegistration.bind(this);
+ }
+
+ _doRegistration() {
+ if (!this.isFormComplete()) {
+ console.error('Incomplete form... how did the button become enabled?');
+ alert('Please complete all of the required fields. They have bold labels.');
+ return;
+ }
+
+ this.props.doRegistration(this.getUserRegistration());
+ }
+
+ _validateEmail() {
+ this.props.checkEmail(this.state.email, (valid) => this.setState('invalidEmail', valid));
+ }
+
+ _validateNomDeBid() {
+ this.props.checkNomDeBid(this.state.nomDeBid, (valid) => this.setState('invalidNomDeBid', valid));
+ }
+
+ getUserRegistration() {
+ return {
+ addresses: this.state.addresses,
+ avatar: this.state.avatar,
+ email: this.state.email,
+ firstName: this.state.firstName,
+ lastName: this.state.lastName,
+ nomDeBid: this.state.nomDeBid,
+ password: this.state.password,
+ phones: this.state.phones,
+ };
+ }
+
+ isFormComplete() {
+ return !this.state.invalidEmail && !this.state.invalidNomDeBid &&
+ !!this.state.firstName && !!this.state.lastName && !!this.state.email &&
+ !!this.state.nomDeBid && !!this.state.phones.length && !!this.state.password;
+ }
+
+ render() {
+ return (
+
+ Sign In or Register
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/app/screens/SignInOrRegister.js b/app/screens/SignInOrRegister.js
new file mode 100644
index 0000000..8831bc2
--- /dev/null
+++ b/app/screens/SignInOrRegister.js
@@ -0,0 +1,36 @@
+import React, { Component } from 'react';
+import { Text, View } from 'react-native';
+
+import FacebookLogin from '../components/FacebookLogin/FacebookLogin.container.js';
+
+import styles from './SignInOrRegister.styles.js';
+
+export default class SignInOrRegister extends Component {
+
+ constructor() {
+ super(props);
+
+ this._doRegistration = this._doRegistration.bind(this);
+ }
+
+ _doRegistration() {
+ this.props.navigation.navigate('Register');
+ }
+
+ render() {
+ return (
+
+ Sign In or Register
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
diff --git a/app/screens/SignInOrRegister.styles.js b/app/screens/SignInOrRegister.styles.js
new file mode 100644
index 0000000..d0fc2fc
--- /dev/null
+++ b/app/screens/SignInOrRegister.styles.js
@@ -0,0 +1,15 @@
+import { StyleSheet } from 'react-native';
+
+export default const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: '#F5FCFF',
+ },
+ title: {
+ fontSize: 20,
+ textAlign: 'center',
+ margin: 10,
+ }
+});
diff --git a/package.json b/package.json
index ccecf71..3356b19 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"react-native": "0.60.0",
"react-native-debugger": "^1.1.0",
"react-native-elements": "^1.1.0",
+ "react-native-fbsdk": "^1.0.1",
"react-native-gallery-swiper": "^1.22.1",
"react-native-gesture-handler": "^1.3.0",
"react-native-screens": "^1.0.0-alpha.23",