From f65886664d85f27e669c97a5705c1ff396a219c0 Mon Sep 17 00:00:00 2001 From: Omar Date: Sat, 29 Dec 2018 00:21:43 -0800 Subject: [PATCH] Tagfer ID Creation --- src/config/apiEndpoints.js | 10 +- src/config/router.js | 6 +- src/locales/translations/en.json | 10 +- src/screens/auth/ForgotPasswordScreen.js | 10 +- src/screens/auth/LoginScreen.js | 26 +- .../{SignScreenOne.js => SignupScreenOne.js} | 92 +++---- src/screens/auth/SignupScreenTwo.js | 254 ++++++++++++++++++ src/utils/errorHandler.js | 85 ++++-- 8 files changed, 406 insertions(+), 87 deletions(-) rename src/screens/auth/{SignScreenOne.js => SignupScreenOne.js} (75%) create mode 100644 src/screens/auth/SignupScreenTwo.js diff --git a/src/config/apiEndpoints.js b/src/config/apiEndpoints.js index 2d466f5..de7ec0e 100644 --- a/src/config/apiEndpoints.js +++ b/src/config/apiEndpoints.js @@ -8,10 +8,14 @@ const endpoints = { method: 'POST', url: `${baseurl}/auth/passwordReset` }, - emailExists: { + emailExists: (email) => ({ method: 'GET', - url: `${baseurl}/auth/email/:email/exists` - } + url: `${baseurl}/auth/email/${email}/exists` + }), + tagferIdExists: (tagferId) => ({ + method: 'GET', + url: `${baseurl}/auth/tagferId/${tagferId}/exists` + }) }; export default endpoints; diff --git a/src/config/router.js b/src/config/router.js index 2f14166..543388f 100644 --- a/src/config/router.js +++ b/src/config/router.js @@ -1,14 +1,16 @@ import { createStackNavigator, createAppContainer } from 'react-navigation'; import WelcomeScreen from '../screens/onboaring/WelcomeScreen'; import LoginScreen from '../screens/auth/LoginScreen'; -import SignupScreenOne from '../screens/auth/SignScreenOne'; +import SignupScreenOne from '../screens/auth/SignupScreenOne'; +import SignupScreenTwo from '../screens/auth/SignupScreenTwo'; import ForgotPasswordScreen from '../screens/auth/ForgotPasswordScreen'; const StackNavigator = createStackNavigator({ welcome: WelcomeScreen, login: LoginScreen, forgotPassword: ForgotPasswordScreen, - signupOne: SignupScreenOne + signupOne: SignupScreenOne, + signupTwo: SignupScreenTwo }); const AppNavigator = createAppContainer(StackNavigator); diff --git a/src/locales/translations/en.json b/src/locales/translations/en.json index 061bb36..2d0b810 100644 --- a/src/locales/translations/en.json +++ b/src/locales/translations/en.json @@ -538,16 +538,18 @@ "userCreateFlow": { "createTagferId": { "connect_button": "Connect With Twitter", - "explanation": "Your Tagfer ID is your unique universal identifier. You cannot change it once it's been activated. Only one Tagfer ID per mobile number.", + "explanation": "Your Tagfer ID is your unique universal identifier. You cannot change it once it's been activated. Only one Tagfer ID per mobile number. A Tagfer ID can include any chracter except '@'.", + "tagferIDExamples": "Examples: John, Barbara, CEO, CTO, Designer, Gamer, FunGuy or FunGirl", "nameExamples1": "e.g. names: \"John\", \"Doe\", \"Doejohn\"", "nameExamples2": "e.g. profession: \"Designer\", \"CEO\"", "orUseTwitter": "or claim your ID now", "referralIdField_description": "Give credit to who referred you", "referralIdField_warning": "If you don't choose a referral now you and the person who referred you will not receive bonus tokens.", "referralIdField_warning2": "Are you absolutely sure you don't have a referral to list?", - "tagferIdField_description": "This will be your permanent username", - "title": "Claim your Tagfer ID", - "wantTwitter": "Want to user your current Twitter Handle as your Tagfer ID?" + "tagferIdField_description": "Your Tagfer ID here", + "title": "Tagfer ID Creation", + "wantTwitter": "Want to use your current Twitter handle as your Tagfer ID?", + "twitterButton": "Connect with Twitter to confirm" }, "phoneAdd": { "countrySelect": "Click the flag to select your country", diff --git a/src/screens/auth/ForgotPasswordScreen.js b/src/screens/auth/ForgotPasswordScreen.js index 8f7201f..93218de 100644 --- a/src/screens/auth/ForgotPasswordScreen.js +++ b/src/screens/auth/ForgotPasswordScreen.js @@ -6,10 +6,11 @@ import { showMessage } from 'react-native-flash-message'; import { FormLabel, Spacer } from '../../components/common'; import { fetchAuth } from '../../utils/fetch'; import { strings } from '../../locales/i18n'; -import { showAuthErrorMessage } from '../../utils/errorHandler'; +import { showFlashErrorMessage } from '../../utils/errorHandler'; import { isEmail } from '../../utils/aux'; import endpoints from '../../config/apiEndpoints'; +import errors from '../../config/errors'; import colors from '../../config/colors.json'; import logo from '../../../assets/logo-round.png'; @@ -37,13 +38,13 @@ export default class ForgotPasswordScreen extends React.Component { fetchAuth(endpoints.passwordReset, body).then(res => { this.setState({ loading: false }); if (res.error) { - showAuthErrorMessage(res.error); + showFlashErrorMessage(res.error); } else { this.showSuccessMessage(); } - }).catch(error => { + }).catch(() => { this.setState({ loading: false }); - showAuthErrorMessage(error); + showFlashErrorMessage(errors.APP); }); } @@ -86,6 +87,7 @@ export default class ForgotPasswordScreen extends React.Component { textStyle={styles.labelStyle} /> this.setState({ email })} placeholder={strings('common.contactFields.email_or_tagferid_placeholder')} autoCorrect={false} diff --git a/src/screens/auth/LoginScreen.js b/src/screens/auth/LoginScreen.js index 3b0d484..6699490 100644 --- a/src/screens/auth/LoginScreen.js +++ b/src/screens/auth/LoginScreen.js @@ -5,9 +5,10 @@ import { Button, FormInput } from 'react-native-elements'; import { FormLabel, Spacer, TextButton } from '../../components/common'; import { strings } from '../../locales/i18n'; import { fetchAuth } from '../../utils/fetch'; -import { showAuthErrorMessage } from '../../utils/errorHandler'; +import { showFlashErrorMessage } from '../../utils/errorHandler'; import endpoints from '../../config/apiEndpoints'; +import errors from '../../config/errors'; import colors from '../../config/colors.json'; import logo from '../../../assets/logo-round.png'; @@ -35,16 +36,14 @@ export default class LoginScreen extends React.Component { const body = this.getLoginRequestBody(); fetchAuth(endpoints.login, body).then(res => { + this.setState({ loading: false }); if (res.error) { - this.setState({ loading: false, password: '' }); - showAuthErrorMessage(res.error); - } else { - this.setState({ loading: false }); - console.log(res.sessionId); + this.clearInvalidInputField(res.error); + showFlashErrorMessage(res.error); } - }).catch(error => { + }).catch(() => { this.setState({ loading: false, password: '' }); - showAuthErrorMessage(error); + showFlashErrorMessage(errors.APP_NETWORK_ERROR); }); } @@ -53,7 +52,7 @@ export default class LoginScreen extends React.Component { */ getLoginRequestBody() { const body = { password: this.state.password }; - if (this.state.id.includes('@')) { + if (this.state.id.indexOf('@') !== -1) { body.email = this.state.id; } else { body.tagferId = this.state.id; @@ -61,6 +60,14 @@ export default class LoginScreen extends React.Component { return body; } + clearInvalidInputField(error) { + if (error === errors.AUTH_USER_NOT_FOUND) { + this.setState({ id: '' }); + } else if (error === errors.AUTH_WRONG_PASSWORD) { + this.setState({ password: '' }); + } + } + /** * Validation for the disbaling the button, if the there is id or password is less than 6. */ @@ -90,6 +97,7 @@ export default class LoginScreen extends React.Component { textStyle={styles.labelStyle} /> this.setState({ id })} placeholder={strings('common.contactFields.email_or_tagferid_placeholder')} autoCorrect={false} diff --git a/src/screens/auth/SignScreenOne.js b/src/screens/auth/SignupScreenOne.js similarity index 75% rename from src/screens/auth/SignScreenOne.js rename to src/screens/auth/SignupScreenOne.js index c961485..49a79ab 100644 --- a/src/screens/auth/SignScreenOne.js +++ b/src/screens/auth/SignupScreenOne.js @@ -1,11 +1,11 @@ -import React, { Component } from 'react'; +import React from 'react'; import { Dimensions, Image, KeyboardAvoidingView, Platform, Text, View, ScrollView, Keyboard } from 'react-native'; -import { Button, FormInput } from 'react-native-elements'; +import { Button, FormInput, FormValidationMessage } from 'react-native-elements'; import { FormLabel, Spacer } from '../../components/common'; import { strings } from '../../locales/i18n'; import { fetchAuth } from '../../utils/fetch'; -import { showAuthErrorMessage } from '../../utils/errorHandler'; +import { getErrorObject, showFlashErrorMessage } from '../../utils/errorHandler'; import { isEmail, isEmpty } from '../../utils/aux'; import logo from '../../../assets/logo-round.png'; @@ -13,7 +13,7 @@ import colors from '../../config/colors.json'; import endpoints from '../../config/apiEndpoints'; import errors from '../../config/errors'; -export default class SignupScreenOne extends Component { +export default class SignupScreenOne extends React.Component { static navigationOptions = { header: null } @@ -25,7 +25,8 @@ export default class SignupScreenOne extends Component { lastName: '', email: '', password: '', - loading: false + loading: false, + error: '' }; } /** @@ -36,49 +37,32 @@ export default class SignupScreenOne extends Component { Keyboard.dismiss(); this.setState({ loading: true }); - const endpoint = this.addParamsToEndpoint(endpoints.emailExists, this.state.email); + const endpointWithParams = endpoints.emailExists(this.state.email); - fetchAuth(endpoint).then(res => { - this.toggleLoadingSpinner(); + fetchAuth(endpointWithParams).then(res => { + this.setState({ loading: false }); if (res.error) { - showAuthErrorMessage(res.error); + showFlashErrorMessage(res.error); } else if (res.result === true) { - this.setState({ email: '' }); - showAuthErrorMessage(errors.AUTH_EMAIL_ALREADY_EXISTS); + this.input.shake(); + const errorObject = getErrorObject(errors.AUTH_EMAIL_ALREADY_EXISTS); + this.setState({ error: errorObject.desc }); } else { - console.log(res.result); + this.props.navigation.navigate('signupTwo'); } - }).catch(error => { - this.toggleLoadingSpinner(); - showAuthErrorMessage(error); + }).catch(() => { + this.setState({ loading: false }); + showFlashErrorMessage(errors.APP_NETWORK_ERROR); }); } /** - * Replaces the default express `:email` parameter with the actual value. - * @param {Object} endpoint takes an endpoint object @see config/endpoints - * @param {String} email - */ - addParamsToEndpoint(endpoint, email) { - const endpointWithParams = { ...endpoint }; - endpointWithParams.url = endpoint.url.replace(':email', email); - return endpointWithParams; - } - - /** - * Boolen function that says if the button is disabled based on if the fields are empty. + * Boolean function that says if the button is disabled based on if the fields are empty. */ isButtonDisabled() { - const { firstName, lastName, email, password } = this.state; - return isEmpty(firstName) || isEmpty(lastName) || !isEmail(email) || password.length < 6; - } - - /** - * Utility function that toggles the state of the loading variable, true to false and false to true. - */ - toggleLoadingSpinner() { - this.setState({ loading: !this.state.loading }); + const { firstName, lastName, email, password, error } = this.state; + return isEmpty(firstName) || isEmpty(lastName) || !isEmail(email) || password.length < 6 || !isEmpty(error); } renderContent() { @@ -100,7 +84,7 @@ export default class SignupScreenOne extends Component { this.setState({ email })} + onChangeText={email => this.setState({ email, error: '' })} placeholder={strings('common.contactFields.email_or_tagferid_placeholder')} autoCorrect={false} autoCapitalize='none' inputStyle={styles.inputTextStyle} + ref={ref => (this.input = ref)} /> - + + {this.state.error} + - - + {/* TERMS AND CONDITIONS */} + {strings('userCreateFlow.termsLeft')} @@ -177,7 +165,7 @@ export default class SignupScreenOne extends Component { disabled={this.isButtonDisabled()} loading={this.state.loading} fontSize={20} - iconRight={{ name: 'chevron-right', color: colors.white, size: 30, style: { marginLeft: 0 } }} + iconRight={styles.chevronIconStyle} /> @@ -224,10 +212,21 @@ const styles = { borderRadius: 5, backgroundColor: colors.buttonBlue }, + chevronIconStyle: { + name: 'chevron-right', + color: colors.white, + size: 30, + style: { marginLeft: 0 } + }, inputTextStyle: { marginLeft: 15, color: colors.black }, + formValidationStyle: { + width: '100%', + alignItems: 'flex-end', + marginBottom: 0 + }, freeTextStyle: { marginLeft: 20, fontSize: 18, @@ -240,6 +239,7 @@ const styles = { fontWeight: 'bold' }, labelStyle: { - color: colors.labelBlue + color: colors.labelBlue, + textTransform: 'uppercase' } }; diff --git a/src/screens/auth/SignupScreenTwo.js b/src/screens/auth/SignupScreenTwo.js new file mode 100644 index 0000000..2414612 --- /dev/null +++ b/src/screens/auth/SignupScreenTwo.js @@ -0,0 +1,254 @@ +import React from 'react'; +import { Keyboard, Dimensions, KeyboardAvoidingView, Platform, ScrollView, Text, View } from 'react-native'; +import { Button, FormInput, FormValidationMessage } from 'react-native-elements'; + +import { FormLabel } from '../../components/common'; +import { strings } from '../../locales/i18n'; +import { isEmpty } from '../../utils/aux'; +import { getErrorObject, showFlashErrorMessage } from '../../utils/errorHandler'; +import { fetchAuth } from '../../utils/fetch'; + +import colors from '../../config/colors.json'; +import errors from '../../config/errors'; +import endpoints from '../../config/apiEndpoints'; + +export default class SignupScreenTwo extends React.Component { + static navigationOptions = { + title: strings('userCreateFlow.createTagferId.title'), + headerStyle: { borderBottomWidth: 0 }, + headerTintColor: '#0D497E' + }; + + constructor(props) { + super(props); + this.state = { + tagferId: '', + error: '', + loading: false, + twitterDisabled: false + }; + + this.keyboardDidHide = this.keyboardDidHide.bind(this); + this.onChangeText = this.onChangeText.bind(this); + } + + componentDidMount() { + this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide); + } + + componentWillUnmount() { + this.keyboardDidHideListener.remove(); + } + + /** + * Poulates the state.error dynmically if there are errors as the user types. + */ + onChangeText(tagferId) { + let error = ''; + if (tagferId.indexOf('@') !== -1) { + error = 'Tagfer ID cannot contain @'; + this.input.shake(); + } + + this.setState({ tagferId, twitterDisabled: true, error }); + } + + /** + * Calls Tagfer API to check if the tagferId exists. + * Toggles the loading state. + * Displays an error if it does. + * Displays a flash message if there are any other errors. + */ + onSaveButtonPress() { + Keyboard.dismiss(); + this.setState({ loading: true }); + + const endpointWithParams = endpoints.tagferIdExists(this.state.tagferId.toLowerCase()); + + fetchAuth(endpointWithParams).then(res => { + this.setState({ loading: false }); + + if (res.error) { + showFlashErrorMessage(res.error); + } else if (res.result === true) { + this.input.shake(); + const errorObject = getErrorObject(errors.AUTH_UID_ALREADY_EXISTS); + this.setState({ error: errorObject.desc }); + } else { + this.props.navigation.navigate('signupThree'); + } + }).catch(() => { + this.setState({ loading: false }); + showFlashErrorMessage(errors.APP_NETWORK_ERROR); + }); + } + + /** + * Disables the twitter button to minimize distraction for UX. + */ + keyboardDidHide() { + this.setState({ twitterDisabled: false }); + } + + isButtonDisabled() { + return isEmpty(this.state.tagferId) || !isEmpty(this.state.error); + } + + isTwitterDisabled() { + return this.state.twitterDisabled || this.state.loading; + } + + renderContent() { + return ( + + + {/* INSTRUCTIONS */} + {strings('userCreateFlow.createTagferId.explanation')} + + {/* TEXT INPUT */} + + + (this.input = ref)} + /> + + + {this.state.error} + + + {/* EXAMPLES */} + {strings('userCreateFlow.createTagferId.tagferIDExamples')} + + {/* TWITTER BUTTON */} + + {strings('userCreateFlow.createTagferId.wantTwitter')} + + + +