diff options
author | Ivan Chen <ivan@tagg.id> | 2021-03-05 16:38:32 -0500 |
---|---|---|
committer | Ivan Chen <ivan@tagg.id> | 2021-03-05 16:38:32 -0500 |
commit | 1465df9621fb963ff873485ad927ff79ea547fa0 (patch) | |
tree | affcb43f37f263f3e0e555dd019dd952b62e1f0a /src/screens | |
parent | 2360e774d94e271d1d9db0d5b92b801b9325535e (diff) | |
parent | b1dee65ee7bb8e120fc38a495f4027905d300650 (diff) |
Merge branch 'master' into tma-634-badge-selection-screen
# Conflicts:
# src/components/taggs/SocialMediaInfo.tsx
Diffstat (limited to 'src/screens')
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} |