Tagfer ID Creation

This commit is contained in:
Omar 2018-12-29 00:21:43 -08:00
parent b55af52e94
commit f65886664d
8 changed files with 406 additions and 87 deletions

View File

@ -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;

View File

@ -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);

View File

@ -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",

View File

@ -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}
/>
<FormInput
value={this.state.email}
onChangeText={email => this.setState({ email })}
placeholder={strings('common.contactFields.email_or_tagferid_placeholder')}
autoCorrect={false}

View File

@ -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}
/>
<FormInput
value={this.state.id}
onChangeText={id => this.setState({ id })}
placeholder={strings('common.contactFields.email_or_tagferid_placeholder')}
autoCorrect={false}

View File

@ -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 {
<Spacer />
<FormLabel
required
labelText={strings('common.contactFields.firstName_labelText').toUpperCase()}
labelText={strings('common.contactFields.firstName_labelText')}
textStyle={styles.labelStyle}
/>
<FormInput
@ -115,7 +99,7 @@ export default class SignupScreenOne extends Component {
<FormLabel
required
labelText={strings('common.contactFields.lastName_labelText').toUpperCase()}
labelText={strings('common.contactFields.lastName_labelText')}
textStyle={styles.labelStyle}
/>
<FormInput
@ -130,23 +114,27 @@ export default class SignupScreenOne extends Component {
<FormLabel
required
labelText={strings('common.contactFields.email_labelText').toUpperCase()}
labelText={strings('common.contactFields.email_labelText')}
textStyle={styles.labelStyle}
/>
<FormInput
value={this.state.email}
onChangeText={email => 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)}
/>
<Spacer />
<View style={styles.formValidationStyle}>
<FormValidationMessage>{this.state.error}</FormValidationMessage>
</View>
<FormLabel
required
labelText={strings('userCreateFlow.signUp.password').toUpperCase()}
labelText={strings('userCreateFlow.signUp.password')}
textStyle={styles.labelStyle}
containerStyle={{ marginTop: 0 }}
/>
<FormInput
secureTextEntry
@ -157,9 +145,9 @@ export default class SignupScreenOne extends Component {
autoCapitalize='none'
inputStyle={styles.inputTextStyle}
/>
<Spacer />
<View style={{ marginTop: 20 }}>
{/* TERMS AND CONDITIONS */}
<View style={{ marginTop: 35 }}>
<Text style={styles.freeTextStyle}>
{strings('userCreateFlow.termsLeft')}
</Text>
@ -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}
/>
</View>
</View>
@ -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'
}
};

View File

@ -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 (
<ScrollView contentContainerStyle={{ flexGrow: 1, backgroundColor: colors.white }} keyboardShouldPersistTaps="handled">
<View style={{ flex: 1 }}>
{/* INSTRUCTIONS */}
<Text style={styles.instructions}>{strings('userCreateFlow.createTagferId.explanation')}</Text>
{/* TEXT INPUT */}
<FormLabel
required
labelText={strings('common.contactFields.tagferId_labelText')}
textStyle={styles.labelStyle}
/>
<FormInput
value={this.state.tagferId}
onChangeText={this.onChangeText}
placeholder={strings('userCreateFlow.createTagferId.tagferIdField_description')}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.inputTextStyle}
ref={ref => (this.input = ref)}
/>
<View style={styles.formValidationStyle}>
<FormValidationMessage>{this.state.error}</FormValidationMessage>
</View>
{/* EXAMPLES */}
<Text style={styles.examplesStyle}>{strings('userCreateFlow.createTagferId.tagferIDExamples')}</Text>
{/* TWITTER BUTTON */}
<View style={styles.twitterTextContainerStyle}>
<Text style={styles.twitterTextStyle}>{strings('userCreateFlow.createTagferId.wantTwitter')}</Text>
</View>
<View style={styles.twitterButtonContainerStyle}>
<Button
title={strings('userCreateFlow.createTagferId.twitterButton')}
buttonStyle={styles.twitterButtonStyle}
fontSize={15}
disabled={this.isTwitterDisabled()}
icon={styles.twitterIconStyle}
/>
</View>
</View>
{/* SAVE AND CONTINUE BUTTON */}
<Button
onPress={this.onSaveButtonPress.bind(this)}
title={strings('common.buttons.saveAndContinue')}
buttonStyle={styles.buttonStyle}
disabled={this.isButtonDisabled()}
loading={this.state.loading}
fontSize={20}
iconRight={styles.chevronIconStyle}
/>
</ScrollView>
);
}
render() {
if (Platform.OS === 'android') {
return this.renderContent();
}
return (
<KeyboardAvoidingView style={{ flex: 1 }} behavior="padding">
{this.renderContent()}
</KeyboardAvoidingView>
);
}
}
const SCREEN_WIDTH = Dimensions.get('window').width;
const styles = {
instructions: {
left: 20,
width: '90%',
marginTop: 20,
marginBottom: 30,
fontSize: 16,
color: colors.grey,
textAlign: 'justify'
},
formValidationStyle: {
width: '100%',
alignItems: 'flex-end',
marginBottom: 0
},
buttonStyle: {
width: SCREEN_WIDTH * 0.9,
marginTop: 15,
marginBottom: 30,
borderRadius: 5,
backgroundColor: colors.buttonBlue
},
chevronIconStyle: {
name: 'chevron-right',
color: colors.white,
size: 30,
style: { marginLeft: 0 }
},
twitterTextContainerStyle: {
marginTop: 50,
marginLeft: 30,
marginRight: 30
},
twitterTextStyle: {
fontSize: 17,
textAlign: 'center',
color: colors.black
},
twitterButtonContainerStyle: {
marginTop: 15,
flex: 1,
flexDirection: 'row',
justifyContent: 'center'
},
twitterButtonStyle: {
width: SCREEN_WIDTH * 0.85,
height: 50,
borderRadius: 25,
backgroundColor: '#1DA1F2'
},
twitterIconStyle: {
name: 'twitter',
type: 'font-awesome',
color: colors.white,
size: 25
},
inputTextStyle: {
marginLeft: 15,
color: colors.black
},
examplesStyle: {
left: 20,
width: '90%',
marginTop: 0,
fontSize: 16,
color: colors.grey,
textAlign: 'justify',
},
freeCenteredTextStyle: {
textAlign: 'center',
fontSize: 17
},
labelStyle: {
color: colors.labelBlue,
textTransform: 'uppercase'
}
};

View File

@ -2,34 +2,81 @@ import { showMessage } from 'react-native-flash-message';
import errors from '../config/errors';
export function getErrorDescription(error) {
export function getErrorObject(error) {
switch (error) {
case errors.AUTH_INVALID_EMAIL: return 'Invalid email format used.';
case errors.AUTH_INVALID_PHONE_NUMBER: return 'Invalid phone number format is used.';
case errors.AUTH_USER_NOT_FOUND: return 'User is not recognized.';
case errors.AUTH_WRONG_PASSWORD: return 'Wrong password.';
case errors.AUTH_UID_ALREADY_EXISTS: return 'TagferID already exists.';
case errors.AUTH_EMAIL_ALREADY_EXISTS: return 'Email already exists.';
case errors.AUTH_PHONE_ALREADY_EXISTS: return 'Phone number already exists.';
case errors.AUTH_PHONE_NOT_CACHED: return 'Please go back and make another verification code request';
case errors.AUTH_VERIFICATION_CODE_MISMATCH: return 'Verification code mismatch';
case errors.APP_NETWORK_ERROR: return 'Network error. Check your connection.';
case errors.APP_NETWORK_TIMEOUT: return 'Timed out while issuing the request. Check your network.';
case errors.APP_UNABLE_TO_PARSE_RESPONSE: return 'Unable to get the response.';
case errors.MISSING_BODY_ATTRIBUTES: return 'Invalid request. Please try again.';
default: return 'Oops, an error occured! Please try again.';
case errors.AUTH_INVALID_EMAIL: return {
type: 'Authentication Error',
desc: 'Invalid email format used.'
};
case errors.AUTH_INVALID_PHONE_NUMBER: return {
type: 'Authentication Error',
desc: 'Invalid phone number format is used.'
};
case errors.AUTH_USER_NOT_FOUND: return {
type: 'Authentication Error',
desc: 'User is not recognized.'
};
case errors.AUTH_WRONG_PASSWORD: return {
type: 'Authentication Error',
desc: 'Wrong password.'
};
case errors.AUTH_UID_ALREADY_EXISTS: return {
type: 'Authentication Error',
desc: 'Tagfer ID already exists.'
};
case errors.AUTH_EMAIL_ALREADY_EXISTS: return {
type: 'Authentication Error',
desc: 'Email already exists.'
};
case errors.AUTH_PHONE_ALREADY_EXISTS: return {
type: 'Authentication Error',
desc: 'Phone number already exists.'
};
case errors.AUTH_PHONE_NOT_CACHED: return {
type: 'Authentication Error',
desc: 'Please go back and make another verification code request'
};
case errors.AUTH_VERIFICATION_CODE_MISMATCH: return {
type: 'Authentication Error',
desc: 'Verification code mismatch'
};
case errors.APP_NETWORK_ERROR: return {
type: 'Network Error',
desc: 'Request failed, check your internet connection.'
};
case errors.APP_NETWORK_TIMEOUT: return {
type: 'Network Error',
desc: 'Timed out while issuing the request. Check your network.'
};
case errors.APP_UNABLE_TO_PARSE_RESPONSE: return {
type: 'Network Error',
desc: 'Unable to get the response.'
};
default: return {
type: 'Error',
desc: 'Oops, an error occured! Please try again.'
};
}
}
/**
* Display a flash message at the top of the screen.
* @param {String} error one of the error codes in @see config/error.js
*/
export function showAuthErrorMessage(error) {
export function showFlashErrorMessage(error) {
const errorObject = getErrorObject(error);
showMessage({
message: 'Authentication Error',
description: getErrorDescription(error),
type: 'danger',
message: errorObject.type,
description: errorObject.desc,
type: getFlashMessageType(errorObject.type),
icon: 'auto',
duration: 3000
});
}
function getFlashMessageType(errorType) {
if (errorType === 'Network Error') {
return 'warning';
}
return 'danger';
}