aboutsummaryrefslogtreecommitdiff
path: root/src/screens
diff options
context:
space:
mode:
authorIvan Chen <ivan@tagg.id>2021-03-05 16:38:32 -0500
committerIvan Chen <ivan@tagg.id>2021-03-05 16:38:32 -0500
commit1465df9621fb963ff873485ad927ff79ea547fa0 (patch)
treeaffcb43f37f263f3e0e555dd019dd952b62e1f0a /src/screens
parent2360e774d94e271d1d9db0d5b92b801b9325535e (diff)
parentb1dee65ee7bb8e120fc38a495f4027905d300650 (diff)
Merge branch 'master' into tma-634-badge-selection-screen
# Conflicts: # src/components/taggs/SocialMediaInfo.tsx
Diffstat (limited to 'src/screens')
-rw-r--r--src/screens/main/NotificationsScreen.tsx2
-rw-r--r--src/screens/onboarding/InvitationCodeVerification.tsx110
-rw-r--r--src/screens/onboarding/Login.tsx15
-rw-r--r--src/screens/onboarding/OnboardingStepOne.tsx263
-rw-r--r--src/screens/onboarding/OnboardingStepThree.tsx403
-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/screens/profile/CaptionScreen.tsx2
-rw-r--r--src/screens/profile/ProfileScreen.tsx2
-rw-r--r--src/screens/search/RequestContactsAccess.tsx28
-rw-r--r--src/screens/search/SearchScreen.tsx74
-rw-r--r--src/screens/search/mock.ts118
-rw-r--r--src/screens/suggestedPeople/AnimatedTutorial.tsx40
-rw-r--r--src/screens/suggestedPeople/SPBody.tsx264
-rw-r--r--src/screens/suggestedPeople/SuggestedPeopleScreen.tsx174
20 files changed, 1887 insertions, 294 deletions
diff --git a/src/screens/main/NotificationsScreen.tsx b/src/screens/main/NotificationsScreen.tsx
index 511680ea..aa53c4a9 100644
--- a/src/screens/main/NotificationsScreen.tsx
+++ b/src/screens/main/NotificationsScreen.tsx
@@ -153,7 +153,7 @@ const NotificationsScreen: React.FC = () => {
return (
<SafeAreaView>
- <StatusBar barStyle={'dark-content'} />
+ <StatusBar barStyle="dark-content" />
<View style={styles.header}>
<Text style={styles.headerText}>Notifications</Text>
</View>
diff --git a/src/screens/onboarding/InvitationCodeVerification.tsx b/src/screens/onboarding/InvitationCodeVerification.tsx
index a0213530..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,14 +61,20 @@ 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);
}
@@ -92,10 +93,6 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({
}
};
- const navigateToAddWaitList = () => {
- navigation.navigate('AddWaitlistUser');
- };
-
const Footer = () => (
<View style={styles.footer}>
<ArrowButton
@@ -110,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}
@@ -144,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 />
@@ -163,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: {
@@ -217,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..64a2a2f7
--- /dev/null
+++ b/src/screens/onboarding/OnboardingStepThree.tsx
@@ -0,0 +1,403 @@
+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 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} = 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) => {
+ 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,
+ };
+ }
+ };
+
+ return (
+ <Animated.ScrollView bounces={false}>
+ <Background
+ centered
+ gradientType={BackgroundGradientType.Light}
+ style={styles.container}>
+ <StatusBar barStyle="light-content" />
+ <RegistrationWizard style={styles.wizard} step="three" />
+ <View style={styles.profile}>
+ <SmallProfilePic />
+ <Image
+ source={require('../../assets/icons/purple-plus.png')}
+ style={styles.purplePlus}
+ />
+ </View>
+ <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/screens/profile/CaptionScreen.tsx b/src/screens/profile/CaptionScreen.tsx
index 91aaa617..01e859ba 100644
--- a/src/screens/profile/CaptionScreen.tsx
+++ b/src/screens/profile/CaptionScreen.tsx
@@ -13,7 +13,7 @@ import {
} from 'react-native';
import {Button} from 'react-native-elements';
import {useDispatch, useSelector} from 'react-redux';
-import {MainStackParams} from 'src/routes';
+import {MainStackParams} from '../../routes';
import {SearchBackground, TaggBigInput} from '../../components';
import {CaptionScreenHeader} from '../../components/';
import TaggLoadingIndicator from '../../components/common/TaggLoadingIndicator';
diff --git a/src/screens/profile/ProfileScreen.tsx b/src/screens/profile/ProfileScreen.tsx
index 9cdba555..5edc6277 100644
--- a/src/screens/profile/ProfileScreen.tsx
+++ b/src/screens/profile/ProfileScreen.tsx
@@ -46,7 +46,7 @@ const ProfileScreen: React.FC<ProfileOnboardingProps> = ({route}) => {
return (
<>
- <StatusBar />
+ <StatusBar barStyle="dark-content" />
<Content {...{y, userXId, screenType}} />
<TabsGradient />
</>
diff --git a/src/screens/search/RequestContactsAccess.tsx b/src/screens/search/RequestContactsAccess.tsx
index de023464..08548c69 100644
--- a/src/screens/search/RequestContactsAccess.tsx
+++ b/src/screens/search/RequestContactsAccess.tsx
@@ -21,21 +21,29 @@ const RequestContactsAccess: React.FC = () => {
const navigation = useNavigation();
const handleAllowAccess = async () => {
- checkPermission().then((permission) => {
+ try {
+ let permission = await checkPermission();
if (permission === 'undefined') {
- requestPermission().then((response) => {
- if (response === 'authorized' || response === 'denied') {
- navigation.navigate('Search');
- }
- });
+ await requestPermission();
}
- });
- await AsyncStorage.setItem('respondedToAccessContacts', 'true');
+ await AsyncStorage.setItem('respondedToAccessContacts', 'true');
+ navigation.navigate('Search');
+ } catch (err) {
+ console.log(
+ 'Unable to check and request permission to get access to user contacts',
+ );
+ }
};
const handleDontAllowAccess = async () => {
- await AsyncStorage.setItem('respondedToAccessContacts', 'true');
- navigation.navigate('Search');
+ try {
+ await AsyncStorage.setItem('respondedToAccessContacts', 'true');
+ navigation.navigate('Search');
+ } catch (err) {
+ console.log(
+ 'Unable to check and request permission to get access to user contacts',
+ );
+ }
};
return (
diff --git a/src/screens/search/SearchScreen.tsx b/src/screens/search/SearchScreen.tsx
index f0be7c9e..70733d7e 100644
--- a/src/screens/search/SearchScreen.tsx
+++ b/src/screens/search/SearchScreen.tsx
@@ -16,19 +16,16 @@ import {
SearchBackground,
SearchBar,
SearchHeader,
- SearchResults,
+ SearchResultList,
SearchResultsBackground,
TabsGradient,
} from '../../components';
import {SEARCH_ENDPOINT, TAGG_LIGHT_BLUE} from '../../constants';
+import {loadSearchResults} from '../../services';
import {loadRecentlySearched, resetScreenType} from '../../store/actions';
import {RootState} from '../../store/rootReducer';
-import {ProfilePreviewType, ScreenType, UserType} from '../../types';
+import {ProfilePreviewType, ScreenType} from '../../types';
import {SCREEN_HEIGHT, SCREEN_WIDTH, StatusBarHeight} from '../../utils';
-const NO_USER: UserType = {
- userId: '',
- username: '',
-};
/**
* Search Screen for user recommendations and a search
@@ -38,14 +35,27 @@ const NO_USER: UserType = {
const SearchScreen: React.FC = () => {
const {recentSearches} = useSelector((state: RootState) => state.taggUsers);
const [query, setQuery] = useState<string>('');
- const [results, setResults] = useState<Array<ProfilePreviewType>>([]);
+ const [results, setResults] = useState<Array<any> | undefined>(undefined);
const [recents, setRecents] = useState<Array<ProfilePreviewType>>(
recentSearches ?? [],
);
const [searching, setSearching] = useState(false);
const top = Animated.useValue(-SCREEN_HEIGHT);
const [refreshing, setRefreshing] = useState<boolean>(false);
+ const [keyboardVisible, setKeyboardVisible] = React.useState(
+ 'keyboardVisible',
+ );
+ useEffect(() => {
+ const showKeyboard = () => setKeyboardVisible('keyboardVisibleTrue');
+ Keyboard.addListener('keyboardWillShow', showKeyboard);
+ return () => Keyboard.removeListener('keyboardWillShow', showKeyboard);
+ }, []);
+ useEffect(() => {
+ const hideKeyboard = () => setKeyboardVisible('keyboardVisibleFalse');
+ Keyboard.addListener('keyboardWillHide', hideKeyboard);
+ return () => Keyboard.removeListener('keyboardWillHide', hideKeyboard);
+ }, []);
const dispatch = useDispatch();
const onRefresh = useCallback(() => {
@@ -60,31 +70,31 @@ const SearchScreen: React.FC = () => {
useEffect(() => {
if (query.length < 3) {
- setResults([]);
+ setResults(undefined);
return;
}
- const loadResults = async (q: string) => {
- try {
- const token = await AsyncStorage.getItem('token');
- const response = await fetch(`${SEARCH_ENDPOINT}?query=${q}`, {
- method: 'GET',
- headers: {
- Authorization: 'Token ' + token,
+ (async () => {
+ const searchResults = await loadSearchResults(
+ `${SEARCH_ENDPOINT}?query=${query}`,
+ );
+ if (query.length > 2) {
+ const categories = searchResults?.categories;
+ const users = searchResults?.users;
+ const sanitizedResult = [
+ {
+ title: 'categories',
+ data: categories,
+ },
+ {
+ title: 'users',
+ data: users,
},
- });
- const status = response.status;
- if (status === 200) {
- let searchResults = await response.json();
- setResults(searchResults);
- return;
- }
- setResults([]);
- } catch (error) {
- console.log(error);
- setResults([]);
+ ];
+ setResults(sanitizedResult);
+ } else {
+ setResults(undefined);
}
- };
- loadResults(query);
+ })();
}, [query]);
/**
@@ -139,7 +149,7 @@ const SearchScreen: React.FC = () => {
return (
<SearchBackground>
- <StatusBar />
+ <StatusBar barStyle="dark-content" />
<ScrollView
scrollEnabled={!searching}
keyboardShouldPersistTaps={'always'}
@@ -160,8 +170,9 @@ const SearchScreen: React.FC = () => {
{...{top, searching}}
/>
<Explore />
+
<SearchResultsBackground {...{top}}>
- {results.length === 0 && recents.length !== 0 ? (
+ {results === undefined && recents.length !== 0 ? (
<RecentSearches
sectionTitle="Recent"
sectionButtonTitle="Clear all"
@@ -170,8 +181,9 @@ const SearchScreen: React.FC = () => {
screenType={ScreenType.Search}
/>
) : (
- <SearchResults
+ <SearchResultList
{...{results}}
+ keyboardVisible={keyboardVisible === 'keyboardVisibleTrue'}
previewType={'Search'}
screenType={ScreenType.Search}
/>
diff --git a/src/screens/search/mock.ts b/src/screens/search/mock.ts
new file mode 100644
index 00000000..d9909b22
--- /dev/null
+++ b/src/screens/search/mock.ts
@@ -0,0 +1,118 @@
+const MockResults = () => {
+ return {
+ categories: [
+ {
+ id: 11,
+ name: "Brown '21",
+ category: 'Brown',
+ },
+ {
+ id: 12,
+ name: "Brown '22",
+ category: 'Brown',
+ },
+ {
+ id: 13,
+ name: "Brown '23",
+ category: null,
+ },
+ {
+ id: 14,
+ name: "Brown '24",
+ category: null,
+ },
+ ],
+ users: [
+ {
+ id: 'd5295557-59ce-49fc-aa8a-442874dbffc3',
+ username: 'foobar',
+ first_name: 'Foo',
+ last_name: 'Bar',
+ thumbnail_url:
+ 'https://tagg-dev.s3.us-east-2.amazonaws.com/thumbnails/smallProfilePicture/spp-d5295557-59ce-49fc-aa8a-442874dbffc3-thumbnail.jpg',
+ },
+ {
+ id: '31e93eb5-ccc9-4743-b053-eff368e23fa8',
+ username: 'foobar2',
+ first_name: 'Foo',
+ last_name: 'Bar',
+ thumbnail_url:
+ 'https://tagg-dev.s3.us-east-2.amazonaws.com/thumbnails/smallProfilePicture/spp-31e93eb5-ccc9-4743-b053-eff368e23fa8-thumbnail.jpg',
+ },
+ {
+ id: 'b1b68df9-97ac-48de-b00d-eab10a6a644a',
+ username: 'foobar3',
+ first_name: 'Foo',
+ last_name: 'Bar',
+ thumbnail_url:
+ 'https://tagg-dev.s3.us-east-2.amazonaws.com/thumbnails/smallProfilePicture/spp-b1b68df9-97ac-48de-b00d-eab10a6a644a-thumbnail.jpg',
+ },
+ {
+ id: 'b89c88b3-6b2f-4b6c-85d9-a03ff5396113',
+ username: 'foobar4',
+ first_name: 'Foo',
+ last_name: 'Bar',
+ thumbnail_url:
+ 'https://tagg-dev.s3.us-east-2.amazonaws.com/thumbnails/smallProfilePicture/spp-b89c88b3-6b2f-4b6c-85d9-a03ff5396113-thumbnail.jpg',
+ },
+ {
+ id: '73b4496a-0aa8-4115-98da-2070bf326134',
+ username: 'foobar5',
+ first_name: 'Foo',
+ last_name: 'Bar',
+ thumbnail_url:
+ 'https://tagg-dev.s3.us-east-2.amazonaws.com/thumbnails/smallProfilePicture/spp-73b4496a-0aa8-4115-98da-2070bf326134-thumbnail.jpg',
+ },
+ {
+ id: '329763b8-931e-4d4d-8a07-003374d38497',
+ username: 'foobar6',
+ first_name: 'Foo',
+ last_name: 'Bar',
+ thumbnail_url:
+ 'https://tagg-dev.s3.us-east-2.amazonaws.com/thumbnails/smallProfilePicture/spp-329763b8-931e-4d4d-8a07-003374d38497-thumbnail.jpg',
+ },
+ {
+ id: '9e82fea2-cddc-41e1-be05-6873f58138ca',
+ username: 'foobar7',
+ first_name: 'Foo',
+ last_name: 'Bar',
+ thumbnail_url:
+ 'https://tagg-dev.s3.us-east-2.amazonaws.com/thumbnails/smallProfilePicture/spp-9e82fea2-cddc-41e1-be05-6873f58138ca-thumbnail.jpg',
+ },
+ {
+ id: '6e5b8892-4384-45a1-bc0a-8f2c9d614fbc',
+ username: 'foobar8',
+ first_name: 'Foo',
+ last_name: 'Bar',
+ thumbnail_url:
+ 'https://tagg-dev.s3.us-east-2.amazonaws.com/thumbnails/smallProfilePicture/spp-6e5b8892-4384-45a1-bc0a-8f2c9d614fbc-thumbnail.jpg',
+ },
+ {
+ id: 'c49b01c6-9151-4654-8fae-834adfa15727',
+ username: 'foobar9',
+ first_name: 'Foo',
+ last_name: 'Bar',
+ thumbnail_url:
+ 'https://tagg-dev.s3.us-east-2.amazonaws.com/thumbnails/smallProfilePicture/spp-c49b01c6-9151-4654-8fae-834adfa15727-thumbnail.jpg',
+ },
+ {
+ id: '5b394d5b-62e3-405e-8ecd-7433517ef688',
+ username: 'foobar10',
+ first_name: 'Foo',
+ last_name: 'Bar',
+ thumbnail_url:
+ 'https://tagg-dev.s3.us-east-2.amazonaws.com/thumbnails/smallProfilePicture/spp-5b394d5b-62e3-405e-8ecd-7433517ef688-thumbnail.jpg',
+ },
+ {
+ id: '698e38f0-24ed-404c-9f0c-6a24e43af576',
+ username: 'fooo',
+ first_name: 'wefwef',
+ last_name: 'wefwef',
+ thumbnail_url:
+ 'https://tagg-dev.s3.us-east-2.amazonaws.com/thumbnails/smallProfilePicture/spp-698e38f0-24ed-404c-9f0c-6a24e43af576-thumbnail.jpg',
+ },
+ ],
+ };
+};
+
+export default MockResults;
diff --git a/src/screens/suggestedPeople/AnimatedTutorial.tsx b/src/screens/suggestedPeople/AnimatedTutorial.tsx
index f7d62cee..6e0f78ae 100644
--- a/src/screens/suggestedPeople/AnimatedTutorial.tsx
+++ b/src/screens/suggestedPeople/AnimatedTutorial.tsx
@@ -2,8 +2,11 @@ import {useNavigation} from '@react-navigation/native';
import React from 'react';
import {StyleSheet, Text, View} from 'react-native';
import {Image} from 'react-native-animatable';
+import {
+ PanGestureHandler,
+ TapGestureHandler,
+} from 'react-native-gesture-handler';
import {SafeAreaView} from 'react-native-safe-area-context';
-import GestureRecognizer from 'react-native-swipe-gestures';
import {useDispatch, useSelector} from 'react-redux';
import {suggestedPeopleAnimatedTutorialFinished} from '../../store/actions/user';
import {RootState} from '../../store/rootReducer';
@@ -18,29 +21,34 @@ const AnimatedTutorial: React.FC = () => {
dispatch(suggestedPeopleAnimatedTutorialFinished(user.userId));
navigation.pop();
};
+
+ // don't dismiss the tutorial if swipe gesture isn't sufficiently large
+ const activeOffsetY: number = -15;
+
return (
<SafeAreaView>
- <GestureRecognizer onSwipeUp={handleCloseAnimationTutorial}>
- <View style={styles.container}>
- <View style={styles.textContainer}>
- <Text style={styles.text}>
- {'Swipe up to discover more people!'}
- </Text>
+ <TapGestureHandler onEnded={handleCloseAnimationTutorial}>
+ <PanGestureHandler
+ onActivated={handleCloseAnimationTutorial}
+ {...{activeOffsetY}}>
+ <View>
+ <View style={styles.textContainer}>
+ <Text style={styles.text}>
+ {'Swipe up to discover more people!'}
+ </Text>
+ </View>
+ <Image
+ source={require('../../assets/gifs/swipe-animation.gif')}
+ style={styles.swipeGif}
+ />
</View>
- <Image
- source={require('../../assets/gifs/swipe-animation.gif')}
- style={styles.swipeGif}
- />
- </View>
- </GestureRecognizer>
+ </PanGestureHandler>
+ </TapGestureHandler>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
- container: {
- flexDirection: 'column',
- },
closeButton: {
top: '2.55%',
left: '5%',
diff --git a/src/screens/suggestedPeople/SPBody.tsx b/src/screens/suggestedPeople/SPBody.tsx
new file mode 100644
index 00000000..aa97dc94
--- /dev/null
+++ b/src/screens/suggestedPeople/SPBody.tsx
@@ -0,0 +1,264 @@
+import {useNavigation} from '@react-navigation/native';
+import React, {Fragment, useMemo} from 'react';
+import {StyleSheet, Text, View} from 'react-native';
+import {Image} from 'react-native-animatable';
+import {TouchableOpacity} from 'react-native-gesture-handler';
+import Animated from 'react-native-reanimated';
+import RequestedButton from '../../assets/ionicons/requested-button.svg';
+import {TaggsBar} from '../../components';
+import {MutualFriends} from '../../components/suggestedPeople';
+import {
+ ProfilePreviewType,
+ ScreenType,
+ SuggestedPeopleDataType,
+} from '../../types';
+import {normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';
+
+interface SPBodyProps {
+ item: SuggestedPeopleDataType;
+ index: number;
+ onAddFriend: (user: ProfilePreviewType) => Promise<void>;
+ onCancelRequest: (user: ProfilePreviewType) => void;
+ loggedInUserId: string;
+}
+
+const SPBody: React.FC<SPBodyProps> = ({
+ item: {user, mutual_friends, social_links, suggested_people_url, friendship},
+ index,
+ onAddFriend,
+ onCancelRequest,
+ loggedInUserId,
+}) => {
+ const firstItem = index === 0;
+ const screenType = ScreenType.SuggestedPeople;
+
+ const displayButton = () => {
+ switch (friendship.status) {
+ case 'friends':
+ return <Fragment />;
+ case 'requested':
+ if (friendship.requester_id === loggedInUserId) {
+ return (
+ <TouchableOpacity
+ style={styles.requestedButton}
+ onPress={() => onCancelRequest(user)}
+ disabled={false}>
+ <RequestedButton
+ width={SCREEN_WIDTH * 0.3}
+ height={SCREEN_HEIGHT * 0.085}
+ />
+ </TouchableOpacity>
+ );
+ } else {
+ return (
+ <TouchableOpacity style={styles.addButton} disabled={true}>
+ <Text style={styles.addButtonTitle}>{'Pending'}</Text>
+ </TouchableOpacity>
+ );
+ }
+ case 'no_record':
+ return (
+ <TouchableOpacity
+ style={styles.addButton}
+ onPress={() => onAddFriend(user)}
+ disabled={false}>
+ <Text style={styles.addButtonTitle}>{'Add Friend'}</Text>
+ </TouchableOpacity>
+ );
+ default:
+ return <Fragment />;
+ }
+ };
+
+ const backgroundImage = useMemo(
+ () => (
+ <Image
+ source={{
+ uri: suggested_people_url,
+ }}
+ style={styles.image}
+ />
+ ),
+ [suggested_people_url],
+ );
+ const navigation = useNavigation();
+
+ return (
+ <View>
+ {backgroundImage}
+ <View style={styles.mainContainer}>
+ <Text style={styles.title}>{firstItem && 'Suggested People'}</Text>
+ <View style={styles.body}>
+ <View style={styles.marginManager}>
+ <View style={styles.addUserContainer}>
+ <TouchableOpacity
+ onPress={() => {
+ navigation.push('Profile', {
+ userXId: user.id,
+ screenType,
+ });
+ }}
+ style={styles.nameInfoContainer}>
+ <Text style={styles.firstName}>{user.first_name}</Text>
+ <Text style={styles.username}>@{user.username}</Text>
+ </TouchableOpacity>
+ {user.id !== loggedInUserId && displayButton()}
+ </View>
+ </View>
+ <TaggsBar
+ y={Animated.useValue(0)}
+ userXId={user.id === loggedInUserId ? undefined : user.id}
+ profileBodyHeight={0}
+ screenType={screenType}
+ whiteRing={true}
+ linkedSocials={social_links}
+ />
+ <View style={styles.marginManager}>
+ <MutualFriends user={user} friends={mutual_friends} />
+ </View>
+ </View>
+ </View>
+ </View>
+ );
+};
+
+const styles = StyleSheet.create({
+ mainContainer: {
+ flexDirection: 'column',
+ width: SCREEN_WIDTH,
+ height: SCREEN_HEIGHT,
+ paddingVertical: '15%',
+ paddingBottom: '20%',
+ justifyContent: 'space-between',
+ alignSelf: 'center',
+ },
+ marginManager: {marginHorizontal: '5%'},
+ image: {
+ position: 'absolute',
+ width: SCREEN_WIDTH,
+ height: SCREEN_HEIGHT,
+ zIndex: 0,
+ },
+ title: {
+ zIndex: 1,
+ paddingTop: '3%',
+ alignSelf: 'center',
+ fontSize: normalize(22),
+ lineHeight: normalize(26),
+ fontWeight: '800',
+ letterSpacing: normalize(3),
+ color: '#FFFEFE',
+ textShadowColor: 'rgba(0, 0, 0, 0.4)',
+ textShadowOffset: {width: normalize(2), height: normalize(2)},
+ textShadowRadius: normalize(2),
+ },
+ firstName: {
+ color: '#fff',
+ fontWeight: '800',
+ fontSize: normalize(24),
+ lineHeight: normalize(29),
+ textShadowColor: 'rgba(0, 0, 0, 0.3)',
+ textShadowOffset: {width: normalize(2), height: normalize(2)},
+ textShadowRadius: normalize(2),
+ letterSpacing: normalize(2.5),
+ alignSelf: 'baseline',
+ },
+ username: {
+ color: '#fff',
+ fontWeight: '600',
+ fontSize: normalize(15),
+ lineHeight: normalize(18),
+ textShadowColor: 'rgba(0, 0, 0, 0.3)',
+ textShadowOffset: {width: normalize(2), height: normalize(2)},
+ textShadowRadius: normalize(2),
+ letterSpacing: normalize(2),
+ },
+ nameInfoContainer: {},
+ addButton: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: SCREEN_WIDTH * 0.3,
+ height: SCREEN_WIDTH * 0.085,
+ padding: 0,
+ borderWidth: 2,
+ borderColor: '#fff',
+ borderRadius: 1,
+ marginLeft: '1%',
+ marginTop: '4%',
+ shadowColor: 'rgb(0, 0, 0)',
+ shadowRadius: 2,
+ shadowOffset: {width: 2, height: 2},
+ shadowOpacity: 0.5,
+ },
+ addButtonTitle: {
+ color: 'white',
+ padding: 0,
+ fontSize: normalize(15),
+ lineHeight: normalize(18),
+ fontWeight: 'bold',
+ textAlign: 'center',
+ letterSpacing: normalize(1),
+ },
+ addUserContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'flex-start',
+ marginBottom: '5%',
+ },
+ requestedButton: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: SCREEN_WIDTH * 0.3,
+ height: SCREEN_WIDTH * 0.085,
+ padding: 0,
+ borderWidth: 2,
+ borderColor: 'transparent',
+ borderRadius: 1,
+ marginLeft: '1%',
+ marginTop: '4%',
+ shadowColor: 'rgb(0, 0, 0)',
+ shadowRadius: 2,
+ shadowOffset: {width: 2, height: 2},
+ shadowOpacity: 0.5,
+ },
+ requestedButtonTitle: {
+ backgroundColor: 'transparent',
+ fontSize: normalize(15),
+ lineHeight: normalize(18),
+ fontWeight: 'bold',
+ textAlign: 'center',
+ letterSpacing: normalize(1),
+ },
+ body: {},
+
+ button: {
+ justifyContent: 'center',
+ alignItems: 'center',
+ width: SCREEN_WIDTH * 0.4,
+ aspectRatio: 154 / 33,
+ borderWidth: 2,
+ borderColor: '#fff',
+ borderRadius: 3,
+ marginRight: '2%',
+ marginLeft: '1%',
+ },
+ transparentBG: {
+ backgroundColor: 'transparent',
+ },
+ lightBlueBG: {
+ backgroundColor: '#fff',
+ },
+ label: {
+ fontSize: normalize(15),
+ fontWeight: '700',
+ letterSpacing: 1,
+ },
+ blueLabel: {
+ color: '#fff',
+ },
+ whiteLabel: {
+ color: 'white',
+ },
+});
+
+export default SPBody;
diff --git a/src/screens/suggestedPeople/SuggestedPeopleScreen.tsx b/src/screens/suggestedPeople/SuggestedPeopleScreen.tsx
index c2aab1b5..911474cd 100644
--- a/src/screens/suggestedPeople/SuggestedPeopleScreen.tsx
+++ b/src/screens/suggestedPeople/SuggestedPeopleScreen.tsx
@@ -6,6 +6,7 @@ import React, {
useEffect,
useState,
useMemo,
+ useRef,
} from 'react';
import {
FlatList,
@@ -21,7 +22,12 @@ import {Image} from 'react-native-animatable';
import {TouchableOpacity} from 'react-native-gesture-handler';
import Animated from 'react-native-reanimated';
import {useDispatch, useSelector, useStore} from 'react-redux';
-import {TabsGradient, TaggsBar} from '../../components';
+import {
+ TabsGradient,
+ TaggsBar,
+ TaggLoadingIndicator,
+ Background,
+} from '../../components';
import {MutualFriends} from '../../components/suggestedPeople';
import {SP_PAGE_SIZE} from '../../constants';
import SuggestedPeopleOnboardingStackScreen from '../../routes/suggestedPeopleOnboarding/SuggestedPeopleOnboardingStackScreen';
@@ -35,6 +41,7 @@ import {
ProfilePreviewType,
ScreenType,
SuggestedPeopleDataType,
+ BackgroundGradientType,
} from '../../types';
import {
fetchUserX,
@@ -45,7 +52,7 @@ import {
SCREEN_WIDTH,
} from '../../utils';
import {userXInStore} from './../../utils/';
-
+import SPBody from './SPBody';
/**
* Bare bones for suggested people consisting of:
* * Image, title, name, username, add friend button [w/o functionality]
@@ -70,6 +77,15 @@ const SuggestedPeopleScreen: React.FC = () => {
const [refreshing, setRefreshing] = useState(false);
const [shouldResetData, setShouldResetData] = useState(false);
const [hideStatusBar, setHideStatusBar] = useState(false);
+ // boolean for showing/hiding loading indicator
+ const [loading, setLoading] = useState(true);
+
+ // set loading to false once there are people to display
+ useEffect(() => {
+ people.length ? setLoading(false) : setLoading(true);
+ }, [people]);
+
+ const stausBarRef = useRef(hideStatusBar);
// loads data and append it to users based on current page
useEffect(() => {
@@ -114,6 +130,9 @@ const SuggestedPeopleScreen: React.FC = () => {
loadNextPage().then((newUsers) => {
loadUserDataToStore(newUsers.map((ppl) => ppl.user));
+ if (shouldResetData) {
+ setPeople([]);
+ }
setPeople(shouldResetData ? newUsers : people.concat(newUsers));
setShouldResetData(false);
});
@@ -152,11 +171,17 @@ const SuggestedPeopleScreen: React.FC = () => {
}
};
navigateToAnimatedTutorial();
+ StatusBar.setHidden(stausBarRef.current);
+ StatusBar.setBarStyle('light-content');
+ return () => {
+ StatusBar.setHidden(false);
+ StatusBar.setBarStyle('dark-content');
+ };
}, [navigation, suggested_people_linked]),
);
const updateDisplayedUser = async (
- suggested: SuggestedPeopleDataType,
+ user: ProfilePreviewType,
status: FriendshipStatusType,
requester_id: string,
) => {
@@ -166,136 +191,57 @@ const SuggestedPeopleScreen: React.FC = () => {
};
setDisplayedUser(localDisplayedUser);
- people.map((item) => {
- if (item.user.id === suggested.user.id) {
- item.friendship.status = status;
- item.friendship.requester_id = requester_id;
- }
- });
- };
-
- const onAddFriend = async (suggested: SuggestedPeopleDataType) => {
- handleAddFriend(screenType, suggested.user, dispatch, state);
- updateDisplayedUser(suggested, 'requested', loggedInUserId);
+ setPeople(
+ people.map((item) => {
+ if (item.user.id === user.id) {
+ item.friendship.status = status;
+ item.friendship.requester_id = requester_id;
+ }
+ return item;
+ }),
+ );
};
- const onCancelRequest = (suggested: SuggestedPeopleDataType) => {
- dispatch(cancelFriendRequest(suggested.user.id));
- updateDisplayedUser(suggested, 'no_record', '');
+ const onAddFriend = async (user: ProfilePreviewType) => {
+ handleAddFriend(screenType, user, dispatch, state);
+ updateDisplayedUser(user, 'requested', loggedInUserId);
};
- const displayButton = (suggested: SuggestedPeopleDataType) => {
- setDisplayedUser(suggested);
- const friendship: FriendshipType = suggested.friendship;
- switch (friendship.status) {
- case 'friends':
- return <Fragment />;
- case 'requested':
- if (friendship.requester_id === loggedInUserId) {
- return (
- <TouchableOpacity
- style={styles.requestedButton}
- onPress={() => onCancelRequest(suggested)}
- disabled={false}>
- <RequestedButton
- width={SCREEN_WIDTH * 0.3}
- height={SCREEN_HEIGHT * 0.085}
- />
- </TouchableOpacity>
- );
- } else {
- return (
- <TouchableOpacity style={styles.addButton} disabled={true}>
- <Text style={styles.addButtonTitle}>{'Pending'}</Text>
- </TouchableOpacity>
- );
- }
- case 'no_record':
- return (
- <TouchableOpacity
- style={styles.addButton}
- onPress={() => onAddFriend(suggested)}
- disabled={false}>
- <Text style={styles.addButtonTitle}>{'Add Friend'}</Text>
- </TouchableOpacity>
- );
- default:
- return <Fragment />;
- }
+ const onCancelRequest = (user: ProfilePreviewType) => {
+ dispatch(cancelFriendRequest(user.id));
+ updateDisplayedUser(user, 'no_record', '');
};
const onViewableItemsChanged = useCallback(
({viewableItems}: {viewableItems: ViewToken[]}) => {
setHideStatusBar(viewableItems[0].index !== 0);
+ stausBarRef.current = viewableItems[0].index !== 0;
},
[],
);
- const SPBody = memo(
- ({item}: {item: ListRenderItemInfo<SuggestedPeopleDataType>}) => {
- const data = item.item;
- const firstItem = item.index === 0;
- const backgroundImage = useMemo(
- () => (
- <Image
- source={{
- uri: data.suggested_people_url,
- }}
- style={styles.image}
- />
- ),
- [data.suggested_people_url],
- );
- return (
- <>
- <StatusBar barStyle={'light-content'} hidden={hideStatusBar} />
- {backgroundImage}
- <View style={styles.mainContainer}>
- <Text style={styles.title}>{firstItem && 'Suggested People'}</Text>
- <View style={styles.body}>
- <View style={styles.marginManager}>
- <View style={styles.addUserContainer}>
- <TouchableOpacity
- onPress={() => {
- navigation.push('Profile', {
- userXId: data.user.id,
- screenType,
- });
- }}
- style={styles.nameInfoContainer}>
- <Text style={styles.firstName}>{data.user.first_name}</Text>
- <Text style={styles.username}>@{data.user.username}</Text>
- </TouchableOpacity>
- {displayButton(data)}
- </View>
- </View>
- <TaggsBar
- y={y}
- userXId={
- data.user.id === loggedInUserId ? undefined : data.user.id
- }
- profileBodyHeight={0}
- screenType={screenType}
- whiteRing={true}
- linkedSocials={data.social_links}
- />
- <View style={styles.marginManager}>
- <MutualFriends user={data.user} friends={data.mutual_friends} />
- </View>
- </View>
- </View>
- </>
- );
- },
- );
-
return suggested_people_linked === 0 ? (
<SuggestedPeopleOnboardingStackScreen />
+ ) : loading ? (
+ <>
+ <TaggLoadingIndicator fullscreen />
+ <Background gradientType={BackgroundGradientType.Dark} />
+ </>
) : (
<>
<FlatList
data={people}
- renderItem={(item) => <SPBody item={item} />}
+ renderItem={(item) => {
+ return (
+ <SPBody
+ index={item.index}
+ item={item.item}
+ onAddFriend={onAddFriend}
+ onCancelRequest={onCancelRequest}
+ loggedInUserId={loggedInUserId}
+ />
+ );
+ }}
keyExtractor={(item, index) => index.toString()}
showsVerticalScrollIndicator={false}
onViewableItemsChanged={onViewableItemsChanged}