aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/assets/icons/purple-plus.pngbin0 -> 18370 bytes
-rw-r--r--src/components/onboarding/BirthDatePicker.tsx4
-rw-r--r--src/components/onboarding/RegistrationWizard.tsx22
-rw-r--r--src/constants/strings.ts6
-rw-r--r--src/routes/main/MainStackScreen.tsx16
-rw-r--r--src/routes/onboarding/OnboardingStackNavigator.tsx63
-rw-r--r--src/routes/onboarding/OnboardingStackScreen.tsx95
-rw-r--r--src/screens/onboarding/InvitationCodeVerification.tsx115
-rw-r--r--src/screens/onboarding/Login.tsx15
-rw-r--r--src/screens/onboarding/OnboardingStepOne.tsx263
-rw-r--r--src/screens/onboarding/OnboardingStepThree.tsx411
-rw-r--r--src/screens/onboarding/OnboardingStepTwo.tsx369
-rw-r--r--src/screens/onboarding/PasswordReset.tsx1
-rw-r--r--src/screens/onboarding/PasswordResetRequest.tsx29
-rw-r--r--src/screens/onboarding/PhoneVerification.tsx225
-rw-r--r--src/screens/onboarding/Verification.tsx51
-rw-r--r--src/screens/onboarding/WelcomeScreen.tsx7
-rw-r--r--src/screens/onboarding/index.ts4
-rw-r--r--src/services/UserProfileService.ts45
19 files changed, 1486 insertions, 255 deletions
diff --git a/src/assets/icons/purple-plus.png b/src/assets/icons/purple-plus.png
new file mode 100644
index 00000000..8b2ce903
--- /dev/null
+++ b/src/assets/icons/purple-plus.png
Binary files differ
diff --git a/src/components/onboarding/BirthDatePicker.tsx b/src/components/onboarding/BirthDatePicker.tsx
index 6bef5798..c3a975dc 100644
--- a/src/components/onboarding/BirthDatePicker.tsx
+++ b/src/components/onboarding/BirthDatePicker.tsx
@@ -46,7 +46,7 @@ const BirthDatePicker = React.forwardRef(
{...props}>
{(updated || props.showPresetdate) && date
? moment(date).format('MM-DD-YYYY')
- : 'Date of Birth'}
+ : 'Birthday'}
</Text>
</TouchableOpacity>
<Modal visible={!hidden} transparent={true} animationType="fade">
@@ -92,7 +92,7 @@ const styles = StyleSheet.create({
input: {
height: 40,
fontSize: 16,
- paddingTop: '2%',
+ paddingTop: 8,
fontWeight: '600',
borderColor: '#fffdfd',
borderWidth: 2,
diff --git a/src/components/onboarding/RegistrationWizard.tsx b/src/components/onboarding/RegistrationWizard.tsx
index 437e7cfb..3c6ca80e 100644
--- a/src/components/onboarding/RegistrationWizard.tsx
+++ b/src/components/onboarding/RegistrationWizard.tsx
@@ -37,16 +37,6 @@ const RegistrationWizard = (props: RegistrationWizardProps) => {
<View
style={props.step === 'three' ? stepActiveStyle : stepStyle}
/>
- <View style={styles.progress} />
- <View style={props.step === 'four' ? stepActiveStyle : stepStyle} />
- <View style={styles.progress} />
- <View style={props.step === 'five' ? stepActiveStyle : stepStyle} />
- <View style={styles.progress} />
- <View style={props.step === 'six' ? stepActiveStyle : stepStyle} />
- <View style={styles.progress} />
- <View
- style={props.step === 'seven' ? stepActiveStyle : stepStyle}
- />
</View>
</Animatable.View>
)}
@@ -60,16 +50,6 @@ const RegistrationWizard = (props: RegistrationWizardProps) => {
<View
style={props.step === 'three' ? stepActiveStyle : stepStyle}
/>
- <View style={styles.progress} />
- <View style={props.step === 'four' ? stepActiveStyle : stepStyle} />
- <View style={styles.progress} />
- <View style={props.step === 'five' ? stepActiveStyle : stepStyle} />
- <View style={styles.progress} />
- <View style={props.step === 'six' ? stepActiveStyle : stepStyle} />
- <View style={styles.progress} />
- <View
- style={props.step === 'seven' ? stepActiveStyle : stepStyle}
- />
</View>
</Animatable.View>
)}
@@ -94,7 +74,7 @@ const styles = StyleSheet.create({
backgroundColor: '#e1f0ff',
},
progress: {
- width: '10%',
+ width: '35%',
height: 2,
backgroundColor: '#e1f0ff',
},
diff --git a/src/constants/strings.ts b/src/constants/strings.ts
index 353e0d02..104cc198 100644
--- a/src/constants/strings.ts
+++ b/src/constants/strings.ts
@@ -14,6 +14,7 @@ export const ERROR_DELETED_OBJECT = 'Oh sad! Looks like the comment / moment was
export const ERROR_DOUBLE_CHECK_CONNECTION = 'Please double-check your network connection and retry';
export const ERROR_DUP_OLD_PWD = 'You may not use a previously used password';
export const ERROR_EMAIL_IN_USE = 'Email already in use, please try another one';
+export const ERROR_PHONE_IN_USE = 'Phone already in use, please try another one';
export const ERROR_FAILED_LOGIN_INFO = 'Login failed, please try re-entering your login information';
export const ERROR_FAILED_TO_COMMENT = 'Unable to post comment, refresh and try again!';
export const ERROR_FAILED_TO_DELETE_COMMENT = 'Unable to delete comment, refresh and try again!';
@@ -31,7 +32,10 @@ export const ERROR_PROFILE_CREATION_SHORT = 'Profile creation failed πŸ˜“';
export const ERROR_PWD_ACCOUNT = (str: string) => `Please make sure that the email / username entered is registered with us. You may contact our customer support at ${str}`;
export const ERROR_REGISTRATION = (str: string) => `Registration failed πŸ˜”, ${str}`;
export const ERROR_SELECT_CLASS_YEAR = 'Please select your Class Year';
+export const ERROR_SELECT_BIRTHDAY = 'Please select your birthday';
+export const ERROR_SELECT_GENDER = 'Please select your gender';
export const ERROR_SERVER_DOWN = 'mhm, looks like our servers are down, please refresh and try again in a few mins';
+export const ERROR_TWILIO_SERVER_ERROR = 'mhm, looks like that is an invalid phone number or our servers are down, please try again in a few mins';
export const ERROR_SOMETHING_WENT_WRONG = 'Oh dear, don’t worry someone will be held responsible for this error, In the meantime refresh the app';
export const ERROR_SOMETHING_WENT_WRONG_REFRESH = "Ha, looks like this one's on us, please refresh and try again";
export const ERROR_SOMETHING_WENT_WRONG_RELOAD = "You broke it, Just kidding! we don't know what happened... Please reload the app and try again";
@@ -50,6 +54,8 @@ export const SUCCESS_LINK = (str: string) => `Successfully linked ${str} πŸŽ‰`;
export const SUCCESS_PIC_UPLOAD = 'Beautiful, the picture was uploaded successfully!';
export const SUCCESS_PWD_RESET = 'Your password was reset successfully!';
export const SUCCESS_VERIFICATION_CODE_SENT = 'New verification code sent! Check your phone messages for your code';
+export const SUCCESS_INVITATION_CODE = 'Perfect! You entered a valid invitation code, you are now able to login and explore Tagg!';
+export const ERROR_NOT_ONBOARDED = 'You are now on waitlist, please enter your invitation code if you have one';
export const UP_TO_DATE = 'Up-to-Date!';
export const UPLOAD_MOMENT_PROMPT_ONE_MESSAGE = 'Post your first moment to\n continue building your digital\nidentity!';
export const UPLOAD_MOMENT_PROMPT_THREE_HEADER = 'Continue to build your profile';
diff --git a/src/routes/main/MainStackScreen.tsx b/src/routes/main/MainStackScreen.tsx
index acf0cd28..04f73985 100644
--- a/src/routes/main/MainStackScreen.tsx
+++ b/src/routes/main/MainStackScreen.tsx
@@ -80,20 +80,6 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => {
}
})();
- const modalStyle: StackNavigationOptions = {
- cardStyle: {backgroundColor: 'rgba(80,80,80,0.9)'},
- gestureDirection: 'vertical',
- cardOverlayEnabled: true,
- cardStyleInterpolator: ({current: {progress}}) => ({
- cardStyle: {
- opacity: progress.interpolate({
- inputRange: [0, 0.5, 0.9, 1],
- outputRange: [0, 0.25, 0.7, 1],
- }),
- },
- }),
- };
-
const tutorialModalStyle: StackNavigationOptions = {
cardStyle: {backgroundColor: 'rgba(0, 0, 0, 0.5)'},
gestureDirection: 'vertical',
@@ -263,7 +249,7 @@ export const headerBarOptions: (
),
});
-const modalStyle: StackNavigationOptions = {
+export const modalStyle: StackNavigationOptions = {
cardStyle: {backgroundColor: 'rgba(80,80,80,0.6)'},
gestureDirection: 'vertical',
cardOverlayEnabled: true,
diff --git a/src/routes/onboarding/OnboardingStackNavigator.tsx b/src/routes/onboarding/OnboardingStackNavigator.tsx
index 9f614f7c..0cdeecdf 100644
--- a/src/routes/onboarding/OnboardingStackNavigator.tsx
+++ b/src/routes/onboarding/OnboardingStackNavigator.tsx
@@ -1,46 +1,41 @@
import {createStackNavigator} from '@react-navigation/stack';
-import {
- CategorySelectionScreenType,
- TaggPopupType,
- UserType,
- VerificationScreenType,
-} from '../../types';
+import {TaggPopupType, VerificationScreenType} from '../../types';
export type OnboardingStackParams = {
- WelcomeScreen: undefined;
Login: undefined;
+ WelcomeScreen: undefined;
PasswordResetRequest: undefined;
- PasswordReset: {
- value: string;
- };
- InvitationCodeVerification: undefined;
- RegistrationOne: undefined;
- RegistrationTwo: {phone: string};
- RegistrationThree: {
- firstName: string;
- lastName: string;
- phone: string;
- email: string;
- };
- Checkpoint: {username: string; userId: string};
+ PasswordReset: {value: string};
Verification: {id: string; screenType: VerificationScreenType};
- ProfileOnboarding: {username: string; userId: string};
- SocialMedia: {username: string; userId: string};
- CategorySelection: {
- screenType: CategorySelectionScreenType;
- user: UserType;
- newCustomCategory: string | undefined;
- };
- CreateCustomCategory: {
- screenType: CategorySelectionScreenType;
- user: UserType;
- existingCategories: string[];
- };
+ // RegistrationOne: undefined;
+ // RegistrationTwo: {phone: string};
+ // RegistrationThree: {
+ // firstName: string;
+ // lastName: string;
+ // phone: string;
+ // email: string;
+ // };
+ // Checkpoint: {username: string; userId: string};
+ // ProfileOnboarding: {username: string; userId: string};
+ // SocialMedia: {username: string; userId: string};
+ // CategorySelection: {
+ // screenType: CategorySelectionScreenType;
+ // user: UserType;
+ // newCustomCategory: string | undefined;
+ // };
+ // CreateCustomCategory: {
+ // screenType: CategorySelectionScreenType;
+ // user: UserType;
+ // existingCategories: string[];
+ // };
TaggPopup: {
popupProps: TaggPopupType;
};
- AddWaitlistUser: undefined;
- WaitlistSuccess: undefined;
+ OnboardingStepOne: undefined;
+ PhoneVerification: {firstName: string; lastName: string; phone: string};
+ OnboardingStepTwo: {firstName: string; lastName: string; phone: string};
+ OnboardingStepThree: {userId: string; username: string};
+ InvitationCodeVerification: {userId: string};
};
export const OnboardingStack = createStackNavigator<OnboardingStackParams>();
diff --git a/src/routes/onboarding/OnboardingStackScreen.tsx b/src/routes/onboarding/OnboardingStackScreen.tsx
index 78f113cc..79171efd 100644
--- a/src/routes/onboarding/OnboardingStackScreen.tsx
+++ b/src/routes/onboarding/OnboardingStackScreen.tsx
@@ -1,24 +1,20 @@
+import {StackCardInterpolationProps} from '@react-navigation/stack';
import React from 'react';
-import {OnboardingStack} from './OnboardingStackNavigator';
+import TaggPopup from '../../components/common/TaggPopup';
import {
- Login,
InvitationCodeVerification,
- RegistrationOne,
- RegistrationTwo,
- RegistrationThree,
- Verification,
- ProfileOnboarding,
- Checkpoint,
- SocialMedia,
- PasswordResetRequest,
+ Login,
+ OnboardingStepThree,
+ OnboardingStepTwo,
PasswordReset,
+ PasswordResetRequest,
+ PhoneVerification,
+ Verification,
WelcomeScreen,
- CategorySelection,
- AddWaitlistUserScreen,
- WaitlistSuccessScreen,
} from '../../screens';
-import {StackCardInterpolationProps} from '@react-navigation/stack';
-import TaggPopup from '../../components/common/TaggPopup';
+import OnboardingStepOne from '../../screens/onboarding/OnboardingStepOne';
+import {modalStyle} from '../main';
+import {OnboardingStack} from './OnboardingStackNavigator';
const forFade = ({current}: StackCardInterpolationProps) => ({
cardStyle: {
@@ -37,6 +33,7 @@ const Onboarding: React.FC = () => {
options={{
gestureEnabled: false,
cardStyleInterpolator: forFade,
+ ...modalStyle,
}}
/>
<OnboardingStack.Screen
@@ -46,37 +43,13 @@ const Onboarding: React.FC = () => {
gestureEnabled: false,
}}
/>
- <OnboardingStack.Screen
- name="WelcomeScreen"
- component={WelcomeScreen}
- options={{
- gestureEnabled: false,
- }}
- />
- <OnboardingStack.Screen
- name="CategorySelection"
- component={CategorySelection}
- options={{
- gestureEnabled: false,
- }}
- />
+ <OnboardingStack.Screen name="WelcomeScreen" component={WelcomeScreen} />
<OnboardingStack.Screen
name="TaggPopup"
component={TaggPopup}
options={{
gestureEnabled: false,
- cardStyle: {
- backgroundColor: 'transparent',
- },
- cardOverlayEnabled: true,
- cardStyleInterpolator: ({current: {progress}}) => ({
- cardStyle: {
- opacity: progress.interpolate({
- inputRange: [0, 0.5, 0.9, 1],
- outputRange: [0, 0.25, 0.7, 1],
- }),
- },
- }),
+ ...modalStyle,
}}
/>
<OnboardingStack.Screen
@@ -84,42 +57,42 @@ const Onboarding: React.FC = () => {
component={PasswordReset}
options={{
gestureEnabled: false,
+ ...modalStyle,
}}
/>
<OnboardingStack.Screen
- name="InvitationCodeVerification"
- component={InvitationCodeVerification}
- />
- <OnboardingStack.Screen
- name="AddWaitlistUser"
- component={AddWaitlistUserScreen}
+ name="Verification"
+ component={Verification}
+ options={{
+ ...modalStyle,
+ }}
/>
<OnboardingStack.Screen
- name="WaitlistSuccess"
- component={WaitlistSuccessScreen}
+ name="OnboardingStepOne"
+ component={OnboardingStepOne}
/>
<OnboardingStack.Screen
- name="RegistrationOne"
- component={RegistrationOne}
+ name="PhoneVerification"
+ component={PhoneVerification}
+ options={{...modalStyle}}
/>
<OnboardingStack.Screen
- name="RegistrationTwo"
- component={RegistrationTwo}
+ name="OnboardingStepTwo"
+ component={OnboardingStepTwo}
+ options={{...modalStyle}}
/>
<OnboardingStack.Screen
- name="RegistrationThree"
- component={RegistrationThree}
+ name="OnboardingStepThree"
+ component={OnboardingStepThree}
+ options={{...modalStyle}}
/>
- <OnboardingStack.Screen name="Checkpoint" component={Checkpoint} />
- <OnboardingStack.Screen name="Verification" component={Verification} />
<OnboardingStack.Screen
- name="ProfileOnboarding"
- component={ProfileOnboarding}
+ name="InvitationCodeVerification"
+ component={InvitationCodeVerification}
options={{
- gestureEnabled: false,
+ ...modalStyle,
}}
/>
- <OnboardingStack.Screen name="SocialMedia" component={SocialMedia} />
</OnboardingStack.Navigator>
);
};
diff --git a/src/screens/onboarding/InvitationCodeVerification.tsx b/src/screens/onboarding/InvitationCodeVerification.tsx
index 903a9912..41d17f29 100644
--- a/src/screens/onboarding/InvitationCodeVerification.tsx
+++ b/src/screens/onboarding/InvitationCodeVerification.tsx
@@ -1,20 +1,7 @@
-import React from 'react';
-import {OnboardingStackParams} from '../../routes';
+import {RouteProp} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack';
-
-import {
- Background,
- RegistrationWizard,
- SubmitButton,
- ArrowButton,
- LoadingIndicator,
-} from '../../components';
-
-import {
- TAGG_LIGHT_PURPLE,
- VERIFY_INVITATION_CODE_ENDPOUNT,
-} from '../../constants';
-
+import React from 'react';
+import {Alert, KeyboardAvoidingView, StyleSheet, View} from 'react-native';
import {Text} from 'react-native-animatable';
import {
CodeField,
@@ -23,28 +10,35 @@ import {
useClearByFocusCell,
} from 'react-native-confirmation-code-field';
import {
- StyleSheet,
- View,
- KeyboardAvoidingView,
- Alert,
- Platform,
-} from 'react-native';
-
-import {BackgroundGradientType} from '../../types';
+ ArrowButton,
+ Background,
+ LoadingIndicator,
+ SubmitButton,
+} from '../../components';
+import {VERIFY_INVITATION_CODE_ENDPOUNT} from '../../constants';
import {
ERROR_DOUBLE_CHECK_CONNECTION,
ERROR_INVALID_INVITATION_CODE,
ERROR_INVLAID_CODE,
ERROR_VERIFICATION_FAILED_SHORT,
+ SUCCESS_INVITATION_CODE,
} from '../../constants/strings';
+import {OnboardingStackParams} from '../../routes';
+import {BackgroundGradientType} from '../../types';
+import {SCREEN_WIDTH} from '../../utils';
-type InvitationCodeVerificationScreenNavigationProp = StackNavigationProp<
+type InvitationCodeVerificationRouteProp = RouteProp<
+ OnboardingStackParams,
+ 'InvitationCodeVerification'
+>;
+type InvitationCodeVerificationNavigationProp = StackNavigationProp<
OnboardingStackParams,
'InvitationCodeVerification'
>;
interface InvitationCodeVerificationProps {
- navigation: InvitationCodeVerificationScreenNavigationProp;
+ navigation: InvitationCodeVerificationNavigationProp;
+ route: InvitationCodeVerificationRouteProp;
}
/**
@@ -53,6 +47,7 @@ interface InvitationCodeVerificationProps {
*/
const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({
+ route,
navigation,
}) => {
const [value, setValue] = React.useState('');
@@ -66,19 +61,28 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({
if (value.length === 6) {
try {
let verifyInviteCodeResponse = await fetch(
- VERIFY_INVITATION_CODE_ENDPOUNT + value + '/',
+ VERIFY_INVITATION_CODE_ENDPOUNT +
+ value +
+ '/?user_id=' +
+ route.params.userId,
{
method: 'DELETE',
},
);
if (verifyInviteCodeResponse.status === 200) {
- navigation.navigate('RegistrationOne');
+ navigation.navigate('Login');
+ setTimeout(() => {
+ Alert.alert(SUCCESS_INVITATION_CODE);
+ }, 500);
} else {
Alert.alert(ERROR_INVALID_INVITATION_CODE);
}
} catch (error) {
- Alert.alert(ERROR_VERIFICATION_FAILED_SHORT, ERROR_DOUBLE_CHECK_CONNECTION);
+ Alert.alert(
+ ERROR_VERIFICATION_FAILED_SHORT,
+ ERROR_DOUBLE_CHECK_CONNECTION,
+ );
return {
name: 'Verification error',
description: error,
@@ -89,10 +93,6 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({
}
};
- const navigateToAddWaitList = () => {
- navigation.navigate('AddWaitlistUser');
- };
-
const Footer = () => (
<View style={styles.footer}>
<ArrowButton
@@ -107,13 +107,8 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({
centered
style={styles.container}
gradientType={BackgroundGradientType.Light}>
- <RegistrationWizard style={styles.wizard} step="one" />
<KeyboardAvoidingView behavior="padding" style={styles.form}>
- <Text style={styles.formHeader}>Enter the code</Text>
- <Text style={styles.description}>
- Please enter the invitation code provided to you by us / your friend.
- (Use all caps.)
- </Text>
+ <Text style={styles.formHeader}>Enter Your Invitation Code</Text>
<CodeField
ref={ref}
{...valueProps}
@@ -141,13 +136,10 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({
accessibilityHint="Select this after entering your invitation code"
onPress={handleInvitationCodeVerification}
/>
- <View style={styles.noInviteCode}>
- <Text style={styles.inviteCodeText}>Don't have an invite? </Text>
- <Text style={styles.inviteCodeLink} onPress={navigateToAddWaitList}>
- {' '}
- Join the Waitlist
- </Text>
- </View>
+ <Text style={styles.youveBeenAddedLabel}>
+ You've been added to the waitlist! We'll notify you when you're at the
+ front of the line!
+ </Text>
<LoadingIndicator />
</KeyboardAvoidingView>
<Footer />
@@ -160,29 +152,17 @@ const styles = StyleSheet.create({
flex: 1,
alignItems: 'center',
justifyContent: 'center',
- },
- wizard: {
- marginTop: '3.5%',
- flex: 1,
- justifyContent: 'center',
+ borderWidth: 1,
},
form: {
alignItems: 'center',
justifyContent: 'flex-start',
- flex: 3,
},
formHeader: {
color: '#fff',
fontSize: 20,
fontWeight: 'bold',
alignSelf: 'flex-start',
- marginBottom: '6%',
- marginHorizontal: '10%',
- },
- description: {
- color: '#fff',
- fontWeight: '600',
- fontSize: 17,
marginHorizontal: '10%',
},
codeFieldRoot: {
@@ -214,22 +194,19 @@ const styles = StyleSheet.create({
width: '100%',
flexDirection: 'row',
justifyContent: 'space-around',
- ...Platform.select({
- ios: {
- bottom: '20%',
- },
- android: {
- bottom: '10%',
- },
- }),
},
noInviteCode: {
flexDirection: 'row',
justifyContent: 'center',
},
- inviteCodeText: {
- color: TAGG_LIGHT_PURPLE,
+ youveBeenAddedLabel: {
+ marginVertical: '5%',
+ width: SCREEN_WIDTH * 0.8,
+ color: 'white',
+ textAlign: 'center',
fontSize: 18,
+ fontWeight: '500',
+ marginBottom: '10%',
},
inviteCodeLink: {
color: 'white',
diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx
index 450c5039..2ca4172b 100644
--- a/src/screens/onboarding/Login.tsx
+++ b/src/screens/onboarding/Login.tsx
@@ -1,7 +1,7 @@
import AsyncStorage from '@react-native-community/async-storage';
import {RouteProp} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack';
-import React, {useEffect, useRef, useState} from 'react';
+import React, {useEffect, useRef} from 'react';
import {
Alert,
Image,
@@ -21,12 +21,13 @@ import {
ERROR_FAILED_LOGIN_INFO,
ERROR_INVALID_LOGIN,
ERROR_LOGIN_FAILED,
+ ERROR_NOT_ONBOARDED,
ERROR_SOMETHING_WENT_WRONG_REFRESH,
} from '../../constants/strings';
import {OnboardingStackParams} from '../../routes/onboarding';
import {fcmService} from '../../services';
import {RootState} from '../../store/rootReducer';
-import {BackgroundGradientType, UserType} from '../../types';
+import {BackgroundGradientType} from '../../types';
import {normalize, userLogin} from '../../utils';
import UpdateRequired from './UpdateRequired';
@@ -155,7 +156,7 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => {
let statusCode = response.status;
let data = await response.json();
- if (statusCode === 200) {
+ if (statusCode === 200 && data.isOnboarded) {
//Stores token received in the response into client's AsynStorage
try {
await AsyncStorage.setItem('token', data.token);
@@ -167,6 +168,13 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => {
console.log(data);
Alert.alert(ERROR_INVALID_LOGIN);
}
+ } else if (statusCode === 200 && !data.isOnboarded) {
+ navigation.navigate('InvitationCodeVerification', {
+ userId: data.UserID,
+ });
+ setTimeout(() => {
+ Alert.alert(ERROR_NOT_ONBOARDED);
+ }, 500);
} else if (statusCode === 401) {
Alert.alert(ERROR_FAILED_LOGIN_INFO);
} else {
@@ -192,7 +200,6 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => {
navigation.navigate('WelcomeScreen');
setForm({...form, attemptedSubmit: false});
};
-
/**
* Login screen forgot password button.
*/
diff --git a/src/screens/onboarding/OnboardingStepOne.tsx b/src/screens/onboarding/OnboardingStepOne.tsx
new file mode 100644
index 00000000..0fa7a6a5
--- /dev/null
+++ b/src/screens/onboarding/OnboardingStepOne.tsx
@@ -0,0 +1,263 @@
+import {StackNavigationProp} from '@react-navigation/stack';
+import React, {useMemo, useRef, useState} from 'react';
+import {
+ Alert,
+ KeyboardAvoidingView,
+ Platform,
+ StatusBar,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from 'react-native';
+import {
+ ArrowButton,
+ Background,
+ RegistrationWizard,
+ TaggInput,
+} from '../../components';
+import {nameRegex, phoneRegex} from '../../constants';
+import {
+ ERROR_NEXT_PAGE,
+ ERROR_PHONE_IN_USE,
+ ERROR_TWILIO_SERVER_ERROR,
+} from '../../constants/strings';
+import {OnboardingStackParams} from '../../routes';
+import {sendOtpStatusCode} from '../../services';
+import {BackgroundGradientType} from '../../types';
+import {SCREEN_HEIGHT} from '../../utils';
+
+type OnboardingStepOneNavigationProp = StackNavigationProp<
+ OnboardingStackParams,
+ 'OnboardingStepOne'
+>;
+interface OnboardingStepOneProps {
+ navigation: OnboardingStepOneNavigationProp;
+}
+
+const OnboardingStepOne: React.FC<OnboardingStepOneProps> = ({navigation}) => {
+ const lnameRef = useRef();
+ const emailRef = useRef();
+ const phoneRef = useRef();
+
+ const handleFocusChange = (field: string): void => {
+ switch (field) {
+ case 'lname':
+ const lnameField: any = lnameRef.current;
+ lnameField.focus();
+ break;
+ case 'email':
+ const emailField: any = emailRef.current;
+ emailField.focus();
+ break;
+ case 'phone':
+ const phoneField: any = phoneRef.current;
+ phoneField.focus();
+ break;
+ default:
+ return;
+ }
+ };
+
+ const [form, setForm] = useState({
+ fname: '',
+ lname: '',
+ phone: '',
+ isValidFname: false,
+ isValidLname: false,
+ isValidPhone: false,
+ attemptedSubmit: false,
+ token: '',
+ });
+
+ const handleFnameUpdate = (fname: string) => {
+ fname = fname.trim();
+ let isValidFname: boolean = nameRegex.test(fname);
+ setForm({
+ ...form,
+ fname,
+ isValidFname,
+ });
+ };
+
+ const handleLnameUpdate = (lname: string) => {
+ lname = lname.trim();
+ let isValidLname: boolean = nameRegex.test(lname);
+ setForm({
+ ...form,
+ lname,
+ isValidLname,
+ });
+ };
+
+ const handlePhoneUpdate = (phone: string) => {
+ phone = phone.trim();
+ let isValidPhone: boolean = phoneRegex.test(phone);
+ setForm({
+ ...form,
+ phone,
+ isValidPhone,
+ });
+ };
+
+ const goNext = async () => {
+ if (!form.attemptedSubmit) {
+ setForm({
+ ...form,
+ attemptedSubmit: true,
+ });
+ }
+ try {
+ if (form.isValidFname && form.isValidLname && form.isValidPhone) {
+ const code = await sendOtpStatusCode(form.phone);
+ if (code) {
+ switch (code) {
+ case 200:
+ navigation.navigate('PhoneVerification', {
+ firstName: form.fname,
+ lastName: form.lname,
+ phone: form.phone,
+ });
+ break;
+ case 409:
+ Alert.alert(ERROR_PHONE_IN_USE);
+ break;
+ default:
+ Alert.alert(ERROR_TWILIO_SERVER_ERROR);
+ }
+ }
+ } else {
+ setForm({...form, attemptedSubmit: false});
+ setTimeout(() => setForm({...form, attemptedSubmit: true}));
+ }
+ } catch (error) {
+ Alert.alert(ERROR_NEXT_PAGE);
+ return {
+ name: 'Navigation error',
+ description: error,
+ };
+ }
+ };
+
+ const footer = useMemo(
+ () => (
+ <View style={styles.footer}>
+ <ArrowButton
+ direction="backward"
+ onPress={() => navigation.navigate('Login')}
+ />
+ <TouchableOpacity onPress={goNext}>
+ <ArrowButton
+ direction="forward"
+ disabled={
+ !(form.isValidFname && form.isValidLname && form.isValidPhone)
+ }
+ onPress={goNext}
+ />
+ </TouchableOpacity>
+ </View>
+ ),
+ [form.isValidFname, form.isValidLname, form.isValidPhone],
+ );
+
+ return (
+ <Background
+ style={styles.container}
+ gradientType={BackgroundGradientType.Light}>
+ <StatusBar barStyle="light-content" />
+ <RegistrationWizard style={styles.wizard} step="one" />
+ <KeyboardAvoidingView
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
+ style={styles.container}>
+ <View>
+ <Text style={styles.formHeader}>ENTER NAME</Text>
+ </View>
+ <TaggInput
+ accessibilityHint="Enter your first name."
+ accessibilityLabel="First name input field."
+ placeholder="First Name"
+ autoCompleteType="name"
+ textContentType="name"
+ returnKeyType="next"
+ onChangeText={handleFnameUpdate}
+ onSubmitEditing={() => handleFocusChange('lname')}
+ blurOnSubmit={false}
+ valid={form.isValidFname}
+ invalidWarning="Please enter a valid first name."
+ attemptedSubmit={form.attemptedSubmit}
+ width={280}
+ />
+ <TaggInput
+ accessibilityHint="Enter your last name."
+ accessibilityLabel="Last name input field."
+ placeholder="Last Name"
+ autoCompleteType="name"
+ textContentType="name"
+ returnKeyType="next"
+ onChangeText={handleLnameUpdate}
+ blurOnSubmit={false}
+ ref={lnameRef}
+ valid={form.isValidLname}
+ invalidWarning="Please enter a valid last name."
+ attemptedSubmit={form.attemptedSubmit}
+ width={280}
+ />
+ <TaggInput
+ maxLength={10} // currently only support US phone numbers
+ accessibilityHint="Enter your phone number."
+ accessibilityLabel="Phone number input field."
+ placeholder="Phone Number"
+ autoCompleteType="tel"
+ textContentType="telephoneNumber"
+ autoCapitalize="none"
+ keyboardType="number-pad"
+ onChangeText={handlePhoneUpdate}
+ blurOnSubmit={false}
+ ref={phoneRef}
+ valid={form.isValidPhone}
+ invalidWarning={'Please enter a valid 10 digit number.'}
+ attemptedSubmit={form.attemptedSubmit}
+ width={280}
+ onSubmitEditing={goNext}
+ />
+ </KeyboardAvoidingView>
+ {footer}
+ </Background>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ wizard: {
+ position: 'absolute',
+ top: SCREEN_HEIGHT * 0.1,
+ },
+ formHeader: {
+ color: '#fff',
+ fontSize: 30,
+ fontWeight: '600',
+ marginBottom: '16%',
+ },
+ load: {
+ top: '5%',
+ },
+ footer: {
+ width: '100%',
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ ...Platform.select({
+ ios: {
+ bottom: '20%',
+ },
+ android: {
+ bottom: '10%',
+ },
+ }),
+ },
+});
+
+export default OnboardingStepOne;
diff --git a/src/screens/onboarding/OnboardingStepThree.tsx b/src/screens/onboarding/OnboardingStepThree.tsx
new file mode 100644
index 00000000..f832539d
--- /dev/null
+++ b/src/screens/onboarding/OnboardingStepThree.tsx
@@ -0,0 +1,411 @@
+import AsyncStorage from '@react-native-community/async-storage';
+import {RouteProp} from '@react-navigation/native';
+import {StackNavigationProp} from '@react-navigation/stack';
+import moment from 'moment';
+import React, {useMemo} from 'react';
+import {
+ Alert,
+ Image,
+ StatusBar,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from 'react-native';
+import ImagePicker from 'react-native-image-crop-picker';
+import Animated from 'react-native-reanimated';
+import {
+ Background,
+ BirthDatePicker,
+ RegistrationWizard,
+ TaggDropDown,
+ TaggInput,
+} from '../../components';
+import {
+ CLASS_YEAR_LIST,
+ EDIT_PROFILE_ENDPOINT,
+ genderRegex,
+ TAGG_PURPLE,
+} from '../../constants';
+import {
+ ERROR_DOUBLE_CHECK_CONNECTION,
+ ERROR_PROFILE_CREATION_SHORT,
+ ERROR_SELECT_BIRTHDAY,
+ ERROR_SELECT_CLASS_YEAR,
+ ERROR_SELECT_GENDER,
+ ERROR_SOMETHING_WENT_WRONG_REFRESH,
+ ERROR_UPLOAD_SMALL_PROFILE_PIC,
+} from '../../constants/strings';
+import {OnboardingStackParams} from '../../routes/onboarding';
+import {BackgroundGradientType} from '../../types';
+import {normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
+
+type OnboardingStepThreeRouteProp = RouteProp<
+ OnboardingStackParams,
+ 'OnboardingStepThree'
+>;
+type OnboardingStepThreeNavigationProp = StackNavigationProp<
+ OnboardingStackParams,
+ 'OnboardingStepThree'
+>;
+interface OnboardingStepThreeProps {
+ route: OnboardingStepThreeRouteProp;
+ navigation: OnboardingStepThreeNavigationProp;
+}
+
+const OnboardingStepThree: React.FC<OnboardingStepThreeProps> = ({
+ route,
+ navigation,
+}) => {
+ const {userId, username} = route.params;
+ let emptyDate: string | undefined;
+ const [form, setForm] = React.useState({
+ smallPic: '',
+ birthdate: emptyDate,
+ gender: '',
+ isValidGender: true,
+ classYear: -1,
+ attemptedSubmit: false,
+ });
+ const [customGender, setCustomGender] = React.useState(false);
+
+ const classYearList = CLASS_YEAR_LIST.map((value) => ({
+ label: value,
+ value,
+ }));
+
+ /**
+ * Profile screen "Add profile picture" button
+ */
+ const SmallProfilePic = () => (
+ <TouchableOpacity
+ accessible={true}
+ accessibilityLabel="ADD PROFILE PICTURE"
+ onPress={goToGallerySmallPic}
+ style={styles.smallProfileUploader}>
+ {form.smallPic ? (
+ <Image source={{uri: form.smallPic}} style={styles.smallProfilePic} />
+ ) : (
+ <Text style={styles.smallProfileText}>ADD PROFILE PICTURE</Text>
+ )}
+ </TouchableOpacity>
+ );
+
+ const goToGallerySmallPic = () => {
+ ImagePicker.openPicker({
+ smartAlbums: [
+ 'Favorites',
+ 'RecentlyAdded',
+ 'SelfPortraits',
+ 'Screenshots',
+ 'UserLibrary',
+ ],
+ width: 580,
+ height: 580,
+ cropping: true,
+ cropperToolbarTitle: 'Select Profile Picture',
+ mediaType: 'photo',
+ cropperCircleOverlay: true,
+ }).then((picture) => {
+ if ('path' in picture) {
+ setForm({
+ ...form,
+ smallPic: picture.path,
+ });
+ }
+ });
+ };
+
+ const handleGenderUpdate = (gender: string) => {
+ if (gender === 'custom') {
+ setCustomGender(true);
+ } else {
+ setCustomGender(false);
+ let isValidGender: boolean = true;
+ setForm({
+ ...form,
+ gender,
+ isValidGender,
+ });
+ }
+ };
+
+ const handleClassYearUpdate = (value: string) => {
+ console.log('foooooo');
+ const classYear = Number.parseInt(value);
+ setForm({
+ ...form,
+ classYear,
+ });
+ };
+
+ const handleCustomGenderUpdate = (gender: string) => {
+ let isValidGender: boolean = genderRegex.test(gender);
+ gender = gender.replace(' ', '-');
+ setForm({
+ ...form,
+ gender,
+ isValidGender,
+ });
+ };
+
+ const handleBirthdateUpdate = (birthdate: Date) => {
+ setForm({
+ ...form,
+ birthdate: birthdate && moment(birthdate).format('YYYY-MM-DD'),
+ });
+ };
+
+ const handleSubmit = async () => {
+ if (!form.smallPic) {
+ Alert.alert(ERROR_UPLOAD_SMALL_PROFILE_PIC);
+ return;
+ }
+ if (form.classYear === -1) {
+ Alert.alert(ERROR_SELECT_CLASS_YEAR);
+ return;
+ }
+ if (form.birthdate === emptyDate) {
+ Alert.alert(ERROR_SELECT_BIRTHDAY);
+ return;
+ }
+ if (form.gender === '') {
+ Alert.alert(ERROR_SELECT_GENDER);
+ return;
+ }
+ if (!form.attemptedSubmit) {
+ setForm({
+ ...form,
+ attemptedSubmit: true,
+ });
+ }
+ let invalidFields: boolean = false;
+ const request = new FormData();
+ if (form.smallPic) {
+ request.append('smallProfilePicture', {
+ uri: form.smallPic,
+ name: 'small_profile_pic.jpg',
+ type: 'image/jpg',
+ });
+ }
+
+ if (form.birthdate) {
+ request.append('birthday', form.birthdate);
+ }
+
+ if (customGender) {
+ if (form.isValidGender) {
+ request.append('gender', form.gender);
+ } else {
+ setForm({...form, attemptedSubmit: false});
+ setTimeout(() => setForm({...form, attemptedSubmit: true}));
+ invalidFields = true;
+ }
+ } else {
+ if (form.isValidGender) {
+ request.append('gender', form.gender);
+ }
+ }
+
+ if (form.classYear !== -1) {
+ request.append('university_class', form.classYear);
+ }
+
+ if (invalidFields) {
+ return;
+ }
+
+ const endpoint = EDIT_PROFILE_ENDPOINT + `${userId}/`;
+ try {
+ const token = await AsyncStorage.getItem('token');
+ let response = await fetch(endpoint, {
+ method: 'PATCH',
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ Authorization: 'Token ' + token,
+ },
+ body: request,
+ });
+ console.log(route.params.userId);
+ let statusCode = response.status;
+ let data = await response.json();
+ if (statusCode === 200) {
+ navigation.navigate('InvitationCodeVerification', {
+ userId: route.params.userId,
+ });
+ } else if (statusCode === 400) {
+ Alert.alert(
+ 'Profile update failed. πŸ˜”',
+ data.error || 'Something went wrong! 😭',
+ );
+ } else {
+ Alert.alert(ERROR_SOMETHING_WENT_WRONG_REFRESH);
+ }
+ } catch (error) {
+ Alert.alert(ERROR_PROFILE_CREATION_SHORT, ERROR_DOUBLE_CHECK_CONNECTION);
+ return {
+ name: 'Profile creation error',
+ description: error,
+ };
+ }
+ };
+
+ const profilePics = useMemo(() => {
+ return (
+ <View style={styles.profile}>
+ <SmallProfilePic />
+ <Image
+ source={require('../../assets/icons/purple-plus.png')}
+ style={styles.purplePlus}
+ />
+ </View>
+ );
+ }, [form.largePic, form.smallPic]);
+
+ return (
+ <Animated.ScrollView bounces={false}>
+ <Background
+ centered
+ gradientType={BackgroundGradientType.Light}
+ style={styles.container}>
+ <StatusBar barStyle="light-content" />
+ <RegistrationWizard style={styles.wizard} step="three" />
+ {profilePics}
+ <View style={styles.contentContainer}>
+ <TaggDropDown
+ onValueChange={(value: string) => handleClassYearUpdate(value)}
+ items={classYearList}
+ placeholder={{
+ label: 'Class Year',
+ value: null,
+ color: '#ddd',
+ }}
+ />
+ <BirthDatePicker
+ handleBDUpdate={handleBirthdateUpdate}
+ width={280}
+ date={form.birthdate}
+ showPresetdate={false}
+ />
+ {customGender && (
+ <TaggInput
+ accessibilityHint="Custom"
+ accessibilityLabel="Gender input field."
+ placeholder="Enter your gender"
+ autoCompleteType="off"
+ textContentType="none"
+ autoCapitalize="none"
+ returnKeyType="next"
+ blurOnSubmit={false}
+ onChangeText={handleCustomGenderUpdate}
+ onSubmitEditing={() => handleSubmit()}
+ valid={form.isValidGender}
+ attemptedSubmit={form.attemptedSubmit}
+ invalidWarning={
+ 'Custom field can only contain letters and hyphens'
+ }
+ width={280}
+ />
+ )}
+ <TaggDropDown
+ onValueChange={(value: string) => handleGenderUpdate(value)}
+ items={[
+ {label: 'Male', value: 'male'},
+ {label: 'Female', value: 'female'},
+ {label: 'Custom', value: 'custom'},
+ ]}
+ placeholder={{
+ label: 'Gender',
+ value: null,
+ color: '#ddd',
+ }}
+ />
+ </View>
+ <View style={styles.footer}>
+ <TouchableOpacity onPress={handleSubmit} style={styles.submitBtn}>
+ <Text style={styles.submitBtnLabel}>Let's start!</Text>
+ </TouchableOpacity>
+ </View>
+ </Background>
+ </Animated.ScrollView>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ height: SCREEN_HEIGHT,
+ },
+ profile: {
+ marginTop: '10%',
+ marginBottom: '5%',
+ },
+ contentContainer: {
+ position: 'relative',
+ width: 280,
+ },
+ smallProfileUploader: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ padding: 20,
+ backgroundColor: '#E1F0FF',
+ height: normalize(150),
+ width: normalize(150),
+ borderRadius: normalize(150),
+ },
+ smallProfileText: {
+ textAlign: 'center',
+ fontSize: 14,
+ fontWeight: 'bold',
+ color: '#806DF4',
+ },
+ smallProfilePic: {
+ height: normalize(150),
+ width: normalize(150),
+ borderRadius: normalize(150),
+ borderWidth: 2,
+ borderColor: 'white',
+ },
+ submitBtn: {
+ backgroundColor: TAGG_PURPLE,
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: SCREEN_WIDTH / 2.5,
+ height: SCREEN_WIDTH / 10,
+ borderRadius: 5,
+ marginTop: '5%',
+ alignSelf: 'center',
+ },
+ submitBtnLabel: {
+ fontSize: 16,
+ fontWeight: '500',
+ color: '#fff',
+ },
+ goBack: {
+ textDecorationLine: 'underline',
+ color: '#fff',
+ fontSize: 15,
+ fontWeight: '600',
+ },
+ footer: {
+ marginTop: '3%',
+ alignItems: 'center',
+ justifyContent: 'space-around',
+ height: SCREEN_HEIGHT * 0.15,
+ },
+ wizard: {
+ position: 'absolute',
+ top: SCREEN_HEIGHT * 0.1,
+ },
+ purplePlus: {
+ position: 'absolute',
+ height: normalize(40),
+ width: normalize(40),
+ bottom: 0,
+ right: 0,
+ },
+});
+
+export default OnboardingStepThree;
diff --git a/src/screens/onboarding/OnboardingStepTwo.tsx b/src/screens/onboarding/OnboardingStepTwo.tsx
new file mode 100644
index 00000000..de869c99
--- /dev/null
+++ b/src/screens/onboarding/OnboardingStepTwo.tsx
@@ -0,0 +1,369 @@
+import AsyncStorage from '@react-native-community/async-storage';
+import {RouteProp} from '@react-navigation/native';
+import {StackNavigationProp} from '@react-navigation/stack';
+import React, {useMemo, useRef, useState} from 'react';
+import {
+ Alert,
+ KeyboardAvoidingView,
+ Platform,
+ StatusBar,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from 'react-native';
+import {
+ ArrowButton,
+ Background,
+ LoadingIndicator,
+ RegistrationWizard,
+ TaggInput,
+ TermsConditions,
+} from '../../components';
+import {emailRegex, passwordRegex, usernameRegex} from '../../constants';
+import {
+ ERROR_DOUBLE_CHECK_CONNECTION,
+ ERROR_REGISTRATION,
+ ERROR_SOMETHING_WENT_WRONG_REFRESH,
+} from '../../constants/strings';
+import {OnboardingStackParams} from '../../routes';
+import {sendRegister} from '../../services';
+import {BackgroundGradientType} from '../../types';
+import {SCREEN_HEIGHT} from '../../utils';
+
+type OnboardingStepTwoRouteProp = RouteProp<
+ OnboardingStackParams,
+ 'OnboardingStepTwo'
+>;
+type OnboardingStepTwoNavigationProp = StackNavigationProp<
+ OnboardingStackParams,
+ 'OnboardingStepTwo'
+>;
+interface OnboardingStepTwoProps {
+ route: OnboardingStepTwoRouteProp;
+ navigation: OnboardingStepTwoNavigationProp;
+}
+
+const OnboardingStepTwo: React.FC<OnboardingStepTwoProps> = ({
+ route,
+ navigation,
+}) => {
+ const emailRef = useRef();
+ const usernameRef = useRef();
+ const passwordRef = useRef();
+ const confirmRef = useRef();
+
+ const handleFocusChange = (field: string): void => {
+ switch (field) {
+ case 'email':
+ const emailField: any = emailRef.current;
+ emailField.focus();
+ break;
+ case 'username':
+ const usernameField: any = usernameRef.current;
+ usernameField.focus();
+ break;
+ case 'password':
+ const passwordField: any = passwordRef.current;
+ passwordField.focus();
+ break;
+ case 'confirm':
+ const confirmField: any = confirmRef.current;
+ confirmField.focus();
+ break;
+ default:
+ return;
+ }
+ };
+
+ // registration form state
+ const [form, setForm] = useState({
+ email: '',
+ username: '',
+ password: '',
+ confirm: '',
+ isValidEmail: false,
+ isValidUsername: false,
+ isValidPassword: false,
+ passwordsMatch: false,
+ tcAccepted: false,
+ attemptedSubmit: false,
+ });
+
+ const handleEmailUpdate = (email: string) => {
+ email = email.trim();
+ let isValidEmail: boolean = emailRegex.test(email);
+ setForm({
+ ...form,
+ email,
+ isValidEmail,
+ });
+ };
+
+ const handleUsernameUpdate = (username: string) => {
+ let isValidUsername: boolean = usernameRegex.test(username);
+ setForm({
+ ...form,
+ username,
+ isValidUsername,
+ });
+ };
+
+ const handlePasswordUpdate = (password: string) => {
+ let isValidPassword: boolean = passwordRegex.test(password);
+ let passwordsMatch: boolean = form.password === form.confirm;
+ setForm({
+ ...form,
+ password,
+ isValidPassword,
+ passwordsMatch,
+ });
+ };
+
+ const handleConfirmUpdate = (confirm: string) => {
+ let passwordsMatch: boolean = form.password === confirm;
+ setForm({
+ ...form,
+ confirm,
+ passwordsMatch,
+ });
+ };
+
+ const handleTcUpdate = (tcAccepted: boolean) => {
+ setForm({
+ ...form,
+ tcAccepted,
+ });
+ };
+
+ const handleRegister = async () => {
+ if (!form.attemptedSubmit) {
+ setForm({
+ ...form,
+ attemptedSubmit: true,
+ });
+ }
+ try {
+ if (
+ form.isValidEmail &&
+ form.isValidUsername &&
+ form.isValidPassword &&
+ form.passwordsMatch
+ ) {
+ if (form.tcAccepted) {
+ const response = await sendRegister(
+ route.params.firstName,
+ route.params.lastName,
+ route.params.phone,
+ form.email,
+ form.username,
+ form.password,
+ );
+ if (response) {
+ const data = await response.json();
+ switch (response.status) {
+ case 201:
+ await AsyncStorage.setItem('token', data.token);
+ navigation.navigate('OnboardingStepThree', {
+ userId: data.UserID,
+ username: form.username,
+ });
+ break;
+ case 400:
+ Alert.alert(ERROR_REGISTRATION(Object.values(data)));
+ break;
+ default:
+ console.log('fooo');
+ Alert.alert(ERROR_SOMETHING_WENT_WRONG_REFRESH);
+ break;
+ }
+ } else {
+ console.log('barrr');
+ Alert.alert(ERROR_SOMETHING_WENT_WRONG_REFRESH);
+ }
+ } else {
+ Alert.alert(
+ 'Terms and conditions',
+ 'You must first agree to the terms and conditions.',
+ );
+ }
+ } else {
+ setForm({...form, attemptedSubmit: false});
+ setTimeout(() => setForm({...form, attemptedSubmit: true}));
+ }
+ } catch (error) {
+ Alert.alert(ERROR_REGISTRATION(ERROR_DOUBLE_CHECK_CONNECTION));
+ return {
+ name: 'Registration error',
+ description: error,
+ };
+ }
+ };
+
+ const footer = useMemo(
+ () => (
+ <View style={styles.footer}>
+ <ArrowButton
+ direction="backward"
+ onPress={() =>
+ navigation.navigate('PhoneVerification', {...route.params})
+ }
+ />
+ <TouchableOpacity onPress={handleRegister}>
+ <ArrowButton
+ direction="forward"
+ disabled={
+ !(
+ form.isValidUsername &&
+ form.isValidPassword &&
+ form.passwordsMatch &&
+ form.tcAccepted
+ )
+ }
+ onPress={handleRegister}
+ />
+ </TouchableOpacity>
+ </View>
+ ),
+ [
+ form.isValidEmail,
+ form.isValidUsername,
+ form.isValidPassword,
+ form.passwordsMatch,
+ form.tcAccepted,
+ ],
+ );
+
+ return (
+ <Background
+ style={styles.container}
+ gradientType={BackgroundGradientType.Light}>
+ <StatusBar barStyle="light-content" />
+ <RegistrationWizard style={styles.wizard} step="two" />
+ <KeyboardAvoidingView
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
+ style={styles.container}>
+ <View>
+ <Text style={styles.formHeader}>SIGN UP</Text>
+ </View>
+ <TaggInput
+ accessibilityHint="Enter your email."
+ accessibilityLabel="Email input field."
+ placeholder="Email"
+ autoCompleteType="email"
+ textContentType="emailAddress"
+ autoCapitalize="none"
+ returnKeyType="next"
+ keyboardType="email-address"
+ onChangeText={handleEmailUpdate}
+ blurOnSubmit={false}
+ ref={emailRef}
+ valid={form.isValidEmail}
+ invalidWarning={'Please enter a valid email address.'}
+ attemptedSubmit={form.attemptedSubmit}
+ width={280}
+ />
+ <TaggInput
+ accessibilityHint="Enter a username."
+ accessibilityLabel="Username input field."
+ placeholder="Username"
+ autoCompleteType="username"
+ textContentType="username"
+ autoCapitalize="none"
+ returnKeyType="next"
+ onChangeText={handleUsernameUpdate}
+ onSubmitEditing={() => handleFocusChange('password')}
+ blurOnSubmit={false}
+ ref={usernameRef}
+ valid={form.isValidUsername}
+ invalidWarning={
+ 'Username must beΒ at least 6 characters and contain only alphanumerics.'
+ }
+ attemptedSubmit={form.attemptedSubmit}
+ width={280}
+ />
+ <TaggInput
+ accessibilityHint="Enter a password."
+ accessibilityLabel="Password input field."
+ placeholder="Password"
+ autoCompleteType="password"
+ textContentType="oneTimeCode"
+ returnKeyType="next"
+ onChangeText={handlePasswordUpdate}
+ onSubmitEditing={() => handleFocusChange('confirm')}
+ blurOnSubmit={false}
+ secureTextEntry
+ ref={passwordRef}
+ valid={form.isValidPassword}
+ invalidWarning={
+ 'Password must be at least 8 characters & contain at least one of a-z, A-Z, 0-9, and a special character.'
+ }
+ attemptedSubmit={form.attemptedSubmit}
+ width={280}
+ />
+ <TaggInput
+ accessibilityHint={'Re-enter your password.'}
+ accessibilityLabel={'Password confirmation input field.'}
+ placeholder={'Confirm Password'}
+ autoCompleteType="password"
+ textContentType="oneTimeCode"
+ returnKeyType={form.tcAccepted ? 'go' : 'default'}
+ onChangeText={handleConfirmUpdate}
+ onSubmitEditing={handleRegister}
+ secureTextEntry
+ ref={confirmRef}
+ valid={form.passwordsMatch}
+ invalidWarning={'Passwords must match.'}
+ attemptedSubmit={form.attemptedSubmit}
+ width={280}
+ />
+ <LoadingIndicator />
+ <TermsConditions
+ style={styles.tc}
+ accepted={form.tcAccepted}
+ onChange={handleTcUpdate}
+ />
+ </KeyboardAvoidingView>
+ {footer}
+ </Background>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ wizard: {
+ position: 'absolute',
+ top: SCREEN_HEIGHT * 0.1,
+ },
+ formHeader: {
+ color: '#fff',
+ fontSize: 30,
+ fontWeight: '600',
+ marginBottom: '16%',
+ },
+ tc: {
+ marginVertical: '5%',
+ },
+ load: {
+ top: '5%',
+ },
+ footer: {
+ width: '100%',
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ ...Platform.select({
+ ios: {
+ bottom: '20%',
+ },
+ android: {
+ bottom: '10%',
+ },
+ }),
+ },
+});
+
+export default OnboardingStepTwo;
diff --git a/src/screens/onboarding/PasswordReset.tsx b/src/screens/onboarding/PasswordReset.tsx
index 11ca60d5..fab77b72 100644
--- a/src/screens/onboarding/PasswordReset.tsx
+++ b/src/screens/onboarding/PasswordReset.tsx
@@ -227,6 +227,7 @@ const styles = StyleSheet.create({
fontWeight: '600',
fontSize: 17,
marginHorizontal: '10%',
+ marginBottom: '10%',
},
footer: {
width: '100%',
diff --git a/src/screens/onboarding/PasswordResetRequest.tsx b/src/screens/onboarding/PasswordResetRequest.tsx
index cf086f59..a63eae81 100644
--- a/src/screens/onboarding/PasswordResetRequest.tsx
+++ b/src/screens/onboarding/PasswordResetRequest.tsx
@@ -1,28 +1,25 @@
-import React, {useState, useRef} from 'react';
import {RouteProp} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack';
+import React, {useState} from 'react';
import {
- View,
- Text,
- StyleSheet,
- StatusBar,
Alert,
+ KeyboardAvoidingView,
Platform,
+ StatusBar,
+ StyleSheet,
+ Text,
TouchableOpacity,
- KeyboardAvoidingView,
+ View,
} from 'react-native';
-
-import {OnboardingStackParams} from '../../routes';
-
+import {trackPromise} from 'react-promise-tracker';
import {
ArrowButton,
- TaggInput,
Background,
LoadingIndicator,
+ TaggInput,
} from '../../components';
-
-import {trackPromise} from 'react-promise-tracker';
import {emailRegex, usernameRegex} from '../../constants';
+import {OnboardingStackParams} from '../../routes';
import {handlePasswordResetRequest} from '../../services';
import {BackgroundGradientType, VerificationScreenType} from '../../types';
@@ -123,14 +120,12 @@ const PasswordResetRequest: React.FC<PasswordResetRequestProps> = ({
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.container}>
<View>
- <Text style={styles.description}>
- Enter your registered username / email
- </Text>
+ <Text style={styles.description}>Enter your registered username</Text>
</View>
<TaggInput
- accessibilityHint="Enter a username / email"
+ accessibilityHint="Enter a username"
accessibilityLabel="Input field."
- placeholder="Username / Email"
+ placeholder="Username"
autoCompleteType="username"
textContentType="username"
autoCapitalize="none"
diff --git a/src/screens/onboarding/PhoneVerification.tsx b/src/screens/onboarding/PhoneVerification.tsx
new file mode 100644
index 00000000..6ec511b3
--- /dev/null
+++ b/src/screens/onboarding/PhoneVerification.tsx
@@ -0,0 +1,225 @@
+import {RouteProp} from '@react-navigation/native';
+import {StackNavigationProp} from '@react-navigation/stack';
+import React, {useMemo} from 'react';
+import {
+ Alert,
+ KeyboardAvoidingView,
+ Platform,
+ StyleSheet,
+ TouchableOpacity,
+ View,
+} from 'react-native';
+import {Text} from 'react-native-animatable';
+import {
+ CodeField,
+ Cursor,
+ useBlurOnFulfill,
+ useClearByFocusCell,
+} from 'react-native-confirmation-code-field';
+import {trackPromise} from 'react-promise-tracker';
+import {
+ ArrowButton,
+ Background,
+ LoadingIndicator,
+ RegistrationWizard,
+ SubmitButton,
+} from '../../components';
+import {codeRegex} from '../../constants';
+import {
+ ERROR_INVALID_VERIFICATION_CODE_FORMAT,
+ ERROR_SOMETHING_WENT_WRONG,
+} from '../../constants/strings';
+import {OnboardingStackParams} from '../../routes';
+import {sendOtp, verifyOtp} from '../../services';
+import {BackgroundGradientType} from '../../types';
+import {SCREEN_HEIGHT} from '../../utils';
+
+type PhoneVerificationRouteProp = RouteProp<
+ OnboardingStackParams,
+ 'PhoneVerification'
+>;
+type PhoneVerificationNavigationProp = StackNavigationProp<
+ OnboardingStackParams,
+ 'PhoneVerification'
+>;
+interface PhoneVerificationProps {
+ route: PhoneVerificationRouteProp;
+ navigation: PhoneVerificationNavigationProp;
+}
+
+const PhoneVerification: React.FC<PhoneVerificationProps> = ({
+ route,
+ navigation,
+}) => {
+ const [value, setValue] = React.useState('');
+ const ref = useBlurOnFulfill({value, cellCount: 6});
+ const [valueProps, getCellOnLayoutHandler] = useClearByFocusCell({
+ value,
+ setValue,
+ });
+ const {phone} = route.params;
+
+ const handleVerification = async () => {
+ if (!codeRegex.test(value)) {
+ Alert.alert(ERROR_INVALID_VERIFICATION_CODE_FORMAT);
+ return;
+ }
+ try {
+ const success = await trackPromise(verifyOtp(phone, value));
+ if (success) {
+ navigation.navigate('OnboardingStepTwo', {
+ ...route.params,
+ });
+ }
+ } catch (error) {
+ console.log(error);
+ Alert.alert(ERROR_SOMETHING_WENT_WRONG);
+ }
+ };
+
+ const footer = useMemo(
+ () => (
+ <View style={styles.footer}>
+ <ArrowButton
+ direction="backward"
+ onPress={() => navigation.navigate('OnboardingStepOne')}
+ />
+ </View>
+ ),
+ [],
+ );
+
+ return (
+ <Background
+ centered
+ style={styles.container}
+ gradientType={BackgroundGradientType.Light}>
+ <RegistrationWizard style={styles.wizard} step="one" />
+ <KeyboardAvoidingView behavior="padding" style={styles.form}>
+ <Text style={styles.formHeader}>Enter 6 digit code</Text>
+ <Text style={styles.description}>
+ We sent a 6 digit verification code to the phone number you provided.
+ </Text>
+ <CodeField
+ ref={ref}
+ {...valueProps}
+ value={value}
+ onChangeText={setValue}
+ cellCount={6}
+ rootStyle={styles.codeFieldRoot}
+ keyboardType="number-pad"
+ textContentType="oneTimeCode"
+ renderCell={({index, symbol, isFocused}) => (
+ <View
+ onLayout={getCellOnLayoutHandler(index)}
+ key={index}
+ style={[styles.cellRoot, isFocused && styles.focusCell]}>
+ <Text style={styles.cellText}>
+ {symbol || (isFocused ? <Cursor /> : null)}
+ </Text>
+ </View>
+ )}
+ />
+ <SubmitButton
+ text="Verify"
+ color="#fff"
+ style={styles.button}
+ accessibilityLabel="Verify"
+ accessibilityHint="Select this after entering your phone number verification code"
+ onPress={handleVerification}
+ />
+ <TouchableOpacity onPress={() => sendOtp(phone)}>
+ <Text style={styles.resend}>Resend Code</Text>
+ </TouchableOpacity>
+ <LoadingIndicator />
+ </KeyboardAvoidingView>
+ {footer}
+ </Background>
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ wizard: {
+ position: 'absolute',
+ top: SCREEN_HEIGHT * 0.1,
+ },
+ form: {
+ top: '20%',
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ flex: 3,
+ },
+ formPasswordVerification: {
+ alignItems: 'center',
+ justifyContent: 'flex-start',
+ flex: 3,
+ top: '35%',
+ },
+ formHeader: {
+ color: '#fff',
+ fontSize: 20,
+ fontWeight: 'bold',
+ alignSelf: 'flex-start',
+ marginBottom: '6%',
+ marginHorizontal: '10%',
+ },
+ description: {
+ color: '#fff',
+ fontWeight: '600',
+ fontSize: 17,
+ marginHorizontal: '10%',
+ },
+ resend: {
+ textDecorationLine: 'underline',
+ color: '#fff',
+ fontSize: 15,
+ fontWeight: '600',
+ },
+ codeFieldRoot: {
+ width: 280,
+ marginHorizontal: 'auto',
+ marginVertical: '15%',
+ },
+ cellRoot: {
+ width: 40,
+ height: 60,
+ justifyContent: 'center',
+ alignItems: 'center',
+ borderBottomColor: '#fff',
+ borderBottomWidth: 1,
+ },
+ cellText: {
+ color: '#fff',
+ fontSize: 48,
+ textAlign: 'center',
+ },
+ focusCell: {
+ borderBottomColor: '#78a0ef',
+ borderBottomWidth: 2,
+ },
+ button: {
+ marginVertical: '5%',
+ },
+ loadingIndicator: {
+ marginVertical: '5%',
+ },
+ footer: {
+ width: '100%',
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ ...Platform.select({
+ ios: {
+ bottom: '20%',
+ },
+ android: {
+ bottom: '10%',
+ },
+ }),
+ },
+});
+export default PhoneVerification;
diff --git a/src/screens/onboarding/Verification.tsx b/src/screens/onboarding/Verification.tsx
index 0fbe0d91..dda18364 100644
--- a/src/screens/onboarding/Verification.tsx
+++ b/src/screens/onboarding/Verification.tsx
@@ -1,16 +1,14 @@
-import React from 'react';
-
-import {OnboardingStackParams} from '../../routes';
import {RouteProp} from '@react-navigation/native';
import {StackNavigationProp} from '@react-navigation/stack';
+import React from 'react';
import {
- Background,
- RegistrationWizard,
- SubmitButton,
- ArrowButton,
- LoadingIndicator,
-} from '../../components';
-
+ Alert,
+ KeyboardAvoidingView,
+ Platform,
+ StyleSheet,
+ TouchableOpacity,
+ View,
+} from 'react-native';
import {Text} from 'react-native-animatable';
import {
CodeField,
@@ -18,22 +16,27 @@ import {
useBlurOnFulfill,
useClearByFocusCell,
} from 'react-native-confirmation-code-field';
-import {
- StyleSheet,
- View,
- TouchableOpacity,
- KeyboardAvoidingView,
- Alert,
- Platform,
-} from 'react-native';
import {trackPromise} from 'react-promise-tracker';
-import {BackgroundGradientType, VerificationScreenType} from '../../types';
+import {
+ ArrowButton,
+ Background,
+ LoadingIndicator,
+ RegistrationWizard,
+ SubmitButton,
+} from '../../components';
+import {codeRegex} from '../../constants';
+import {
+ ERROR_INVALID_VERIFICATION_CODE_FORMAT,
+ ERROR_SOMETHING_WENT_WRONG,
+} from '../../constants/strings';
+import {OnboardingStackParams} from '../../routes';
import {
handlePasswordCodeVerification,
+ handlePasswordResetRequest,
sendOtp,
verifyOtp,
- handlePasswordResetRequest,
} from '../../services';
+import {BackgroundGradientType, VerificationScreenType} from '../../types';
type VerificationScreenRouteProp = RouteProp<
OnboardingStackParams,
@@ -48,12 +51,6 @@ interface VerificationProps {
navigation: VerificationScreenNavigationProp;
}
-import {codeRegex} from '../../constants';
-import {
- ERROR_INVALID_VERIFICATION_CODE_FORMAT,
- ERROR_SOMETHING_WENT_WRONG,
-} from '../../constants/strings';
-
const Verification: React.FC<VerificationProps> = ({route, navigation}) => {
const [value, setValue] = React.useState('');
const ref = useBlurOnFulfill({value, cellCount: 6});
@@ -217,7 +214,7 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'flex-start',
flex: 3,
- top: '35%',
+ top: '25%',
},
formHeader: {
color: '#fff',
diff --git a/src/screens/onboarding/WelcomeScreen.tsx b/src/screens/onboarding/WelcomeScreen.tsx
index ae31f933..c36a6e05 100644
--- a/src/screens/onboarding/WelcomeScreen.tsx
+++ b/src/screens/onboarding/WelcomeScreen.tsx
@@ -16,9 +16,6 @@ interface WelcomeScreenProps {
}
const WelcomeScreen: React.FC<WelcomeScreenProps> = ({navigation}) => {
- const handleNext = () => {
- navigation.navigate('InvitationCodeVerification');
- };
return (
<Background
style={styles.container}
@@ -37,7 +34,9 @@ const WelcomeScreen: React.FC<WelcomeScreenProps> = ({navigation}) => {
</Text>
</View>
<TaggSquareButton
- onPress={handleNext}
+ onPress={() => {
+ navigation.navigate('OnboardingStepOne');
+ }}
title={'Next'}
buttonStyle={'large'}
buttonColor={'purple'}
diff --git a/src/screens/onboarding/index.ts b/src/screens/onboarding/index.ts
index 596683e5..49d7cfb9 100644
--- a/src/screens/onboarding/index.ts
+++ b/src/screens/onboarding/index.ts
@@ -15,3 +15,7 @@ export {default as AddWaitlistUserScreen} from './AddWaitlistUserScreen';
export {default as WaitlistSuccessScreen} from './WaitlistSuccessScreen';
export {default as CreateCustomCategory} from './CreateCustomCategory';
export {default as UpdateRequired} from './UpdateRequired';
+export {default as OnboardingStepOne} from './OnboardingStepOne';
+export {default as PhoneVerification} from './PhoneVerification';
+export {default as OnboardingStepTwo} from './OnboardingStepTwo';
+export {default as OnboardingStepThree} from './OnboardingStepThree';
diff --git a/src/services/UserProfileService.ts b/src/services/UserProfileService.ts
index bfc4933f..dd77db9f 100644
--- a/src/services/UserProfileService.ts
+++ b/src/services/UserProfileService.ts
@@ -11,6 +11,7 @@ import {
PROFILE_INFO_ENDPOINT,
PROFILE_PHOTO_ENDPOINT,
PROFILE_PHOTO_THUMBNAIL_ENDPOINT,
+ REGISTER_ENDPOINT,
SEND_OTP_ENDPOINT,
TAGG_CUSTOMER_SUPPORT,
VERIFY_OTP_ENDPOINT,
@@ -292,7 +293,6 @@ export const verifyOtp = async (phone: string, otp: string) => {
export const sendOtp = async (phone: string) => {
try {
- console.log(phone);
let response = await fetch(SEND_OTP_ENDPOINT, {
method: 'POST',
body: JSON.stringify({
@@ -313,3 +313,46 @@ export const sendOtp = async (phone: string) => {
return false;
}
};
+
+export const sendOtpStatusCode = async (phone: string) => {
+ try {
+ let response = await fetch(SEND_OTP_ENDPOINT, {
+ method: 'POST',
+ body: JSON.stringify({
+ phone_number: '+1' + phone,
+ }),
+ });
+
+ return response.status;
+ } catch (error) {
+ console.log(error);
+ return undefined;
+ }
+};
+
+export const sendRegister = async (
+ firstName: string,
+ lastName: string,
+ phone: string,
+ email: string,
+ username: string,
+ password: string,
+) => {
+ try {
+ const response = await fetch(REGISTER_ENDPOINT, {
+ method: 'POST',
+ body: JSON.stringify({
+ first_name: firstName,
+ last_name: lastName,
+ email: email,
+ phone_number: phone,
+ username: username,
+ password: password,
+ }),
+ });
+ return response;
+ } catch (error) {
+ console.log(error);
+ return undefined;
+ }
+};