From c123ec385cd13f1db0dbb26edc1dd003734a398a Mon Sep 17 00:00:00 2001 From: Mike Fitzpatrick Date: Mon, 5 Aug 2019 16:59:38 -0400 Subject: [PATCH] - Registration screens stubbing and partial buildouts --- app/actions/items.js | 39 ++++---- app/actions/profile.js | 89 ++++++++++++++++++ app/api/auction.js | 0 app/api/helpers.js | 86 ++++++++++++++++++ app/api/index.js | 57 +++++++++++- app/api/items.js | 7 ++ app/api/profile.js | 0 .../FacebookLogin/FacebookLogin.container.js | 19 ++++ app/components/FacebookLogin/FacebookLogin.js | 41 +++++++++ app/constants/actionTypes.js | 2 + app/constants/constants.js | 6 ++ app/router.js | 23 ++++- app/screens/Register.container.js | 17 ++++ app/screens/Register.js | 90 +++++++++++++++++++ app/screens/SignInOrRegister.js | 36 ++++++++ app/screens/SignInOrRegister.styles.js | 15 ++++ package.json | 1 + 17 files changed, 505 insertions(+), 23 deletions(-) create mode 100644 app/actions/profile.js create mode 100644 app/api/auction.js create mode 100644 app/api/helpers.js create mode 100644 app/api/items.js create mode 100644 app/api/profile.js create mode 100644 app/components/FacebookLogin/FacebookLogin.container.js create mode 100644 app/components/FacebookLogin/FacebookLogin.js create mode 100644 app/screens/Register.container.js create mode 100644 app/screens/Register.js create mode 100644 app/screens/SignInOrRegister.js create mode 100644 app/screens/SignInOrRegister.styles.js 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 new file mode 100644 index 0000000..cd29173 --- /dev/null +++ b/app/actions/profile.js @@ -0,0 +1,89 @@ +import { blockUI, unblockUI } from './index.js'; + +import { + getEmailAvailability, + getNomAvailaibility, + registerNewUser, +} from '../api/profile.js'; + +import { + DO_LOGOUT, + PROFILE_EMAIL_AVAILABLE, + PROFILE_NOM_AVAILABLE, + UPDATE_PROFILE, +} from '../constants/actionTypes.js'; + +const isValidEmail = (payload) => ({ + type: PROFILE_EMAIL_AVAILABLE, + payload, +}); + +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 a74b001..7110c9b 100644 --- a/app/constants/actionTypes.js +++ b/app/constants/actionTypes.js @@ -17,6 +17,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 e7e06b2..e23031a 100644 --- a/app/router.js +++ b/app/router.js @@ -11,8 +11,8 @@ import ImageDetail from './screens/ImageDetail.js'; import Item from './screens/Item.js'; import Marketplace from './screens/Marketplace.js'; import Profile from './screens/Profile.js'; - -let screen = Dimensions.get('window'); +import Register from './screens/Register.js'; +import SignInOrRegister from './screens/SignInOrRegister.js'; export const Tabs = createBottomTabNavigator({ 'Event': { @@ -45,6 +45,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 + + + + + + + +