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 | |
parent | 2360e774d94e271d1d9db0d5b92b801b9325535e (diff) | |
parent | b1dee65ee7bb8e120fc38a495f4027905d300650 (diff) |
Merge branch 'master' into tma-634-badge-selection-screen
# Conflicts:
# src/components/taggs/SocialMediaInfo.tsx
75 files changed, 2410 insertions, 501 deletions
diff --git a/ios/Frontend/Images.xcassets/AppIcon.appiconset/120-1.png b/ios/Frontend/Images.xcassets/AppIcon.appiconset/120-1.png Binary files differindex 3c4ea26e..c28e2cb7 100644 --- a/ios/Frontend/Images.xcassets/AppIcon.appiconset/120-1.png +++ b/ios/Frontend/Images.xcassets/AppIcon.appiconset/120-1.png diff --git a/ios/Frontend/Images.xcassets/AppIcon.appiconset/120.png b/ios/Frontend/Images.xcassets/AppIcon.appiconset/120.png Binary files differindex 1e399016..c28e2cb7 100644 --- a/ios/Frontend/Images.xcassets/AppIcon.appiconset/120.png +++ b/ios/Frontend/Images.xcassets/AppIcon.appiconset/120.png diff --git a/ios/Frontend/Images.xcassets/AppIcon.appiconset/180.png b/ios/Frontend/Images.xcassets/AppIcon.appiconset/180.png Binary files differindex 039c9352..48ae0c80 100644 --- a/ios/Frontend/Images.xcassets/AppIcon.appiconset/180.png +++ b/ios/Frontend/Images.xcassets/AppIcon.appiconset/180.png diff --git a/ios/Frontend/Images.xcassets/AppIcon.appiconset/40.png b/ios/Frontend/Images.xcassets/AppIcon.appiconset/40.png Binary files differindex deb7b94f..f129b763 100644 --- a/ios/Frontend/Images.xcassets/AppIcon.appiconset/40.png +++ b/ios/Frontend/Images.xcassets/AppIcon.appiconset/40.png diff --git a/ios/Frontend/Images.xcassets/AppIcon.appiconset/58.png b/ios/Frontend/Images.xcassets/AppIcon.appiconset/58.png Binary files differindex b1526abe..8fb85f02 100644 --- a/ios/Frontend/Images.xcassets/AppIcon.appiconset/58.png +++ b/ios/Frontend/Images.xcassets/AppIcon.appiconset/58.png diff --git a/ios/Frontend/Images.xcassets/AppIcon.appiconset/60.png b/ios/Frontend/Images.xcassets/AppIcon.appiconset/60.png Binary files differindex cfb29fe9..022b1c17 100644 --- a/ios/Frontend/Images.xcassets/AppIcon.appiconset/60.png +++ b/ios/Frontend/Images.xcassets/AppIcon.appiconset/60.png diff --git a/ios/Frontend/Images.xcassets/AppIcon.appiconset/80.png b/ios/Frontend/Images.xcassets/AppIcon.appiconset/80.png Binary files differindex 86708166..866b421d 100644 --- a/ios/Frontend/Images.xcassets/AppIcon.appiconset/80.png +++ b/ios/Frontend/Images.xcassets/AppIcon.appiconset/80.png diff --git a/ios/Frontend/Images.xcassets/AppIcon.appiconset/87.png b/ios/Frontend/Images.xcassets/AppIcon.appiconset/87.png Binary files differindex bc9b852b..afe863d0 100644 --- a/ios/Frontend/Images.xcassets/AppIcon.appiconset/87.png +++ b/ios/Frontend/Images.xcassets/AppIcon.appiconset/87.png diff --git a/ios/Frontend/Images.xcassets/AppIcon.appiconset/appstore.png b/ios/Frontend/Images.xcassets/AppIcon.appiconset/appstore.png Binary files differindex 133c1517..fd942ff0 100644 --- a/ios/Frontend/Images.xcassets/AppIcon.appiconset/appstore.png +++ b/ios/Frontend/Images.xcassets/AppIcon.appiconset/appstore.png diff --git a/package.json b/package.json index bfd1febf..0451e743 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,6 @@ "react-native-snap-carousel": "^3.9.1", "react-native-splash-screen": "^3.2.0", "react-native-svg": "^12.1.0", - "react-native-swipe-gestures": "^1.0.5", "react-native-vector-icons": "^7.0.0", "react-promise-tracker": "^2.1.0", "react-redux": "^7.2.2", diff --git a/src/assets/icons/purple-plus.png b/src/assets/icons/purple-plus.png Binary files differnew file mode 100644 index 00000000..8b2ce903 --- /dev/null +++ b/src/assets/icons/purple-plus.png diff --git a/src/assets/images/bwbadges.png b/src/assets/images/bwbadges.png Binary files differnew file mode 100644 index 00000000..3a337775 --- /dev/null +++ b/src/assets/images/bwbadges.png diff --git a/src/assets/images/bwbadges@2x.png b/src/assets/images/bwbadges@2x.png Binary files differnew file mode 100644 index 00000000..60c2f995 --- /dev/null +++ b/src/assets/images/bwbadges@2x.png diff --git a/src/assets/images/bwbadges@3x.png b/src/assets/images/bwbadges@3x.png Binary files differnew file mode 100644 index 00000000..874c0c4d --- /dev/null +++ b/src/assets/images/bwbadges@3x.png diff --git a/src/assets/images/search.png b/src/assets/images/search.png Binary files differnew file mode 100644 index 00000000..ba9906ba --- /dev/null +++ b/src/assets/images/search.png diff --git a/src/assets/images/search@2x.png b/src/assets/images/search@2x.png Binary files differnew file mode 100644 index 00000000..fa133ae1 --- /dev/null +++ b/src/assets/images/search@2x.png diff --git a/src/assets/images/search@3x.png b/src/assets/images/search@3x.png Binary files differnew file mode 100644 index 00000000..3ea4ce15 --- /dev/null +++ b/src/assets/images/search@3x.png diff --git a/src/components/comments/CommentsContainer.tsx b/src/components/comments/CommentsContainer.tsx index c72da2b7..3dc8a71c 100644 --- a/src/components/comments/CommentsContainer.tsx +++ b/src/components/comments/CommentsContainer.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useRef, useState} from 'react'; import {StyleSheet} from 'react-native'; import {FlatList} from 'react-native-gesture-handler'; import {useDispatch, useSelector} from 'react-redux'; -import {CommentTile} from '.'; +import CommentTile from './CommentTile'; import {getComments} from '../../services'; import {updateReplyPosted} from '../../store/actions'; import {RootState} from '../../store/rootReducer'; diff --git a/src/components/common/GenericMoreInfoDrawer.tsx b/src/components/common/GenericMoreInfoDrawer.tsx index ff32a464..0928ed44 100644 --- a/src/components/common/GenericMoreInfoDrawer.tsx +++ b/src/components/common/GenericMoreInfoDrawer.tsx @@ -9,7 +9,7 @@ import { ViewStyle, } from 'react-native'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; -import {BottomDrawer} from '.'; +import BottomDrawer from './BottomDrawer'; import {TAGG_LIGHT_BLUE} from '../../constants'; import {normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; diff --git a/src/components/common/SocialLinkModal.tsx b/src/components/common/SocialLinkModal.tsx index 20061cd0..a8c18225 100644 --- a/src/components/common/SocialLinkModal.tsx +++ b/src/components/common/SocialLinkModal.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {Modal, StyleSheet, Text, TouchableOpacity, View} from 'react-native'; import {TextInput} from 'react-native-gesture-handler'; import {ScreenType} from '../../types'; -import {SocialIcon} from '.'; +import SocialIcon from './SocialIcon'; import CloseIcon from '../../assets/ionicons/close-outline.svg'; import {normalize, SCREEN_WIDTH} from '../../utils'; import CenteredView from './CenteredView'; diff --git a/src/components/common/TaggPopup.tsx b/src/components/common/TaggPopup.tsx index b5ac32ec..8b6865fd 100644 --- a/src/components/common/TaggPopup.tsx +++ b/src/components/common/TaggPopup.tsx @@ -3,7 +3,7 @@ import {StackNavigationProp} from '@react-navigation/stack'; import * as React from 'react'; import {Platform, Text, StyleSheet, TouchableOpacity} from 'react-native'; import {Image, View} from 'react-native-animatable'; -import {ArrowButton} from '..'; +import {ArrowButton} from '../onboarding'; import {OnboardingStackParams} from '../../routes'; import {SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; import CloseIcon from '../../assets/ionicons/close-outline.svg'; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 95854ba8..e1543cd8 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -15,7 +15,7 @@ export {default as ComingSoon} from './ComingSoon'; export {default as PostCarousel} from './PostCarousel'; export {default as TaggDatePicker} from './TaggDatePicker'; export {default as BottomDrawer} from './BottomDrawer'; -export {default as TaggLoadingTndicator} from './TaggLoadingIndicator'; +export {default as TaggLoadingIndicator} from './TaggLoadingIndicator'; export {default as GenericMoreInfoDrawer} from './GenericMoreInfoDrawer'; export {default as TaggPopUp} from './TaggPopup'; export {default as TaggPrompt} from './TaggPrompt'; diff --git a/src/components/moments/MomentPostHeader.tsx b/src/components/moments/MomentPostHeader.tsx index 810ccea9..aad776e8 100644 --- a/src/components/moments/MomentPostHeader.tsx +++ b/src/components/moments/MomentPostHeader.tsx @@ -1,6 +1,6 @@ import React, {useState} from 'react'; import {StyleSheet, Text, View, ViewProps} from 'react-native'; -import {MomentMoreInfoDrawer} from '..'; +import {MomentMoreInfoDrawer} from '../profile'; import {loadUserMoments} from '../../store/actions'; import {useDispatch, useSelector} from 'react-redux'; import {ScreenType} from '../../types'; diff --git a/src/components/onboarding/BirthDatePicker.tsx b/src/components/onboarding/BirthDatePicker.tsx index 6bef5798..c3a975dc 100644 --- a/src/components/onboarding/BirthDatePicker.tsx +++ b/src/components/onboarding/BirthDatePicker.tsx @@ -46,7 +46,7 @@ const BirthDatePicker = React.forwardRef( {...props}> {(updated || props.showPresetdate) && date ? moment(date).format('MM-DD-YYYY') - : 'Date of Birth'} + : 'Birthday'} </Text> </TouchableOpacity> <Modal visible={!hidden} transparent={true} animationType="fade"> @@ -92,7 +92,7 @@ const styles = StyleSheet.create({ input: { height: 40, fontSize: 16, - paddingTop: '2%', + paddingTop: 8, fontWeight: '600', borderColor: '#fffdfd', borderWidth: 2, diff --git a/src/components/onboarding/LinkSocialMedia.tsx b/src/components/onboarding/LinkSocialMedia.tsx index f3915752..eb3cf218 100644 --- a/src/components/onboarding/LinkSocialMedia.tsx +++ b/src/components/onboarding/LinkSocialMedia.tsx @@ -7,7 +7,7 @@ import { TouchableOpacity, TouchableOpacityProps, } from 'react-native'; -import {SocialLinkModal} from '..'; +import {SocialLinkModal} from '../common'; import { INTEGRATED_SOCIAL_LIST, SOCIAL_FONT_COLORS, diff --git a/src/components/onboarding/RegistrationWizard.tsx b/src/components/onboarding/RegistrationWizard.tsx index 437e7cfb..3c6ca80e 100644 --- a/src/components/onboarding/RegistrationWizard.tsx +++ b/src/components/onboarding/RegistrationWizard.tsx @@ -37,16 +37,6 @@ const RegistrationWizard = (props: RegistrationWizardProps) => { <View style={props.step === 'three' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View style={props.step === 'four' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View style={props.step === 'five' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View style={props.step === 'six' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View - style={props.step === 'seven' ? stepActiveStyle : stepStyle} - /> </View> </Animatable.View> )} @@ -60,16 +50,6 @@ const RegistrationWizard = (props: RegistrationWizardProps) => { <View style={props.step === 'three' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View style={props.step === 'four' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View style={props.step === 'five' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View style={props.step === 'six' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View - style={props.step === 'seven' ? stepActiveStyle : stepStyle} - /> </View> </Animatable.View> )} @@ -94,7 +74,7 @@ const styles = StyleSheet.create({ backgroundColor: '#e1f0ff', }, progress: { - width: '10%', + width: '35%', height: 2, backgroundColor: '#e1f0ff', }, diff --git a/src/components/profile/Content.tsx b/src/components/profile/Content.tsx index e75ae949..ae9f9564 100644 --- a/src/components/profile/Content.tsx +++ b/src/components/profile/Content.tsx @@ -13,7 +13,7 @@ import { import {TouchableOpacity} from 'react-native-gesture-handler'; import Animated from 'react-native-reanimated'; import {useDispatch, useSelector, useStore} from 'react-redux'; -import {Cover} from '.'; +import Cover from './Cover'; import GreyPlusLogo from '../../assets/icons/grey-plus-logo.svg'; import {COVER_HEIGHT, TAGG_LIGHT_BLUE} from '../../constants'; import { diff --git a/src/components/profile/Friends.tsx b/src/components/profile/Friends.tsx index 02a7460a..7c7265c5 100644 --- a/src/components/profile/Friends.tsx +++ b/src/components/profile/Friends.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {View, StyleSheet, ScrollView, Text} from 'react-native'; import {ProfilePreviewType, ScreenType} from '../../types'; -import {ProfilePreview} from '..'; +import {ProfilePreview} from '../profile'; import {normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; import {TAGG_LIGHT_BLUE} from '../../constants'; import {RootState} from '../../store/rootReducer'; diff --git a/src/components/profile/ProfileHeader.tsx b/src/components/profile/ProfileHeader.tsx index 7dad2a68..e5bd9d93 100644 --- a/src/components/profile/ProfileHeader.tsx +++ b/src/components/profile/ProfileHeader.tsx @@ -1,7 +1,7 @@ import React, {useState} from 'react'; import {StyleSheet, Text, View} from 'react-native'; import {useSelector} from 'react-redux'; -import {UniversityIcon} from '.'; +import UniversityIcon from './UniversityIcon'; import {PROFILE_CUTOUT_TOP_Y} from '../../constants'; import {RootState} from '../../store/rootreducer'; import {ScreenType} from '../../types'; diff --git a/src/components/profile/ProfilePreview.tsx b/src/components/profile/ProfilePreview.tsx index 02ab94e7..f08335a1 100644 --- a/src/components/profile/ProfilePreview.tsx +++ b/src/components/profile/ProfilePreview.tsx @@ -16,6 +16,7 @@ import {loadImageFromURL} from '../../services'; import {RootState} from '../../store/rootreducer'; import {PreviewType, ProfilePreviewType, ScreenType} from '../../types'; import { + addUserToRecentlyViewed, checkIfUserIsBlocked, fetchUserX, isIPhoneX, @@ -37,12 +38,14 @@ interface ProfilePreviewProps extends ViewProps { profilePreview: ProfilePreviewType; previewType: PreviewType; screenType: ScreenType; + setMFDrawer?: Function; } const ProfilePreview: React.FC<ProfilePreviewProps> = ({ profilePreview: {username, first_name, last_name, id, thumbnail_url}, previewType, screenType, + setMFDrawer, }) => { const navigation = useNavigation(); const {user: loggedInUser} = useSelector((state: RootState) => state.user); @@ -87,39 +90,7 @@ const ProfilePreview: React.FC<ProfilePreviewProps> = ({ return; } if (previewType !== 'Comment') { - const jsonValue = await AsyncStorage.getItem( - '@recently_searched_users', - ); - let recentlySearchedList = - jsonValue != null ? JSON.parse(jsonValue) : null; - if (recentlySearchedList) { - if (recentlySearchedList.length > 0) { - if ( - recentlySearchedList.some( - (saved_user: ProfilePreviewType) => saved_user.id === id, - ) - ) { - console.log('User already in recently searched.'); - } else { - if (recentlySearchedList.length >= 10) { - recentlySearchedList.pop(); - } - recentlySearchedList.unshift(user); - } - } - } else { - recentlySearchedList = [user]; - } - - try { - let recentlySearchedListString = JSON.stringify(recentlySearchedList); - await AsyncStorage.setItem( - '@recently_searched_users', - recentlySearchedListString, - ); - } catch (e) { - console.log(e); - } + await addUserToRecentlyViewed(user) } const userXId = @@ -139,6 +110,11 @@ const ProfilePreview: React.FC<ProfilePreviewProps> = ({ ); } + // Close Mutual Friends drawer on suggested people upon navigation + if (setMFDrawer) { + setMFDrawer(false); + } + navigation.push('Profile', { userXId, screenType, diff --git a/src/components/search/ExploreSection.tsx b/src/components/search/ExploreSection.tsx index 025c8c3c..e888370e 100644 --- a/src/components/search/ExploreSection.tsx +++ b/src/components/search/ExploreSection.tsx @@ -14,7 +14,7 @@ interface ExploreSectionProps { users: ProfilePreviewType[]; } const ExploreSection: React.FC<ExploreSectionProps> = ({title, users}) => { - return users.length !== 0 ? ( + return users && users.length !== 0 ? ( <View style={styles.container}> <Text style={styles.header}>{title}</Text> <FlatList diff --git a/src/components/search/RecentSearches.tsx b/src/components/search/RecentSearches.tsx index bebf6bcf..6fb9fca9 100644 --- a/src/components/search/RecentSearches.tsx +++ b/src/components/search/RecentSearches.tsx @@ -5,10 +5,12 @@ import { TouchableOpacity, StyleSheet, TouchableOpacityProps, + ScrollView, } from 'react-native'; import {PreviewType, ProfilePreviewType, ScreenType} from '../../types'; import {TAGG_LIGHT_BLUE} from '../../constants'; import SearchResults from './SearchResults'; +import {SCREEN_HEIGHT} from '../../utils'; interface RecentSearchesProps extends TouchableOpacityProps { sectionTitle: PreviewType; @@ -21,7 +23,9 @@ interface RecentSearchesProps extends TouchableOpacityProps { */ const RecentSearches: React.FC<RecentSearchesProps> = (props) => { return ( - <View style={styles.mainContainer}> + <ScrollView + style={styles.mainContainer} + contentContainerStyle={{paddingBottom: SCREEN_HEIGHT * 0.1}}> <View style={styles.container}> <Text style={styles.title}>{props.sectionTitle}</Text> {props.sectionButtonTitle && ( @@ -35,13 +39,14 @@ const RecentSearches: React.FC<RecentSearchesProps> = (props) => { previewType={props.sectionTitle} screenType={props.screenType} /> - </View> + </ScrollView> ); }; const styles = StyleSheet.create({ mainContainer: { marginLeft: '3%', + padding: 20, }, container: { flexDirection: 'row', diff --git a/src/components/search/SearchResultCell.tsx b/src/components/search/SearchResultCell.tsx new file mode 100644 index 00000000..705fb5c9 --- /dev/null +++ b/src/components/search/SearchResultCell.tsx @@ -0,0 +1,187 @@ +import {useNavigation} from '@react-navigation/native'; +import React, {useEffect, useState} from 'react'; +import {Alert, Image, StyleSheet, Text, View} from 'react-native'; +import {TouchableOpacity} from 'react-native-gesture-handler'; +import {useDispatch, useStore} from 'react-redux'; +import {ERROR_UNABLE_TO_VIEW_PROFILE} from '../../constants/strings'; +import {loadImageFromURL} from '../../services'; +import {RootState} from '../../store/rootReducer'; +import {ProfilePreviewType, ScreenType, UserType} from '../../types'; +import {normalize, SCREEN_WIDTH} from '../../utils'; +import { + addUserToRecentlyViewed, + checkIfUserIsBlocked, + defaultUserProfile, + fetchUserX, + userXInStore, +} from '../../utils/users'; + +interface SearchResults { + profileData: ProfilePreviewType; + loggedInUser: UserType; +} + +const SearchResultsCell: React.FC<SearchResults> = ({ + profileData: { + id, + name, + username, + first_name, + last_name, + thumbnail_url, + category, + }, + loggedInUser, +}) => { + const [avatar, setAvatar] = useState<string | undefined>(undefined); + useEffect(() => { + (async () => { + if (thumbnail_url !== undefined) { + try { + const response = await loadImageFromURL(thumbnail_url); + if (response) { + setAvatar(response); + } + } catch (error) { + console.log('Error while downloading ', error); + throw error; + } + } + })(); + }, [thumbnail_url]); + + const dispatch = useDispatch(); + const state: RootState = useStore().getState(); + const navigation = useNavigation(); + const addToRecentlyStoredAndNavigateToProfile = async () => { + try { + //If the logged in user is blocked by the user being viewed, do not proceed. + const isUserBlocked = await checkIfUserIsBlocked( + id, + dispatch, + loggedInUser, + ); + if (isUserBlocked) { + Alert.alert(ERROR_UNABLE_TO_VIEW_PROFILE); + return; + } + + await addUserToRecentlyViewed({ + id, + first_name, + last_name, + thumbnail_url, + username, + }); + + const userXId = loggedInUser.username === username ? undefined : id; + + /** + * Dispatch an event to Fetch the user details only if we're navigating to + * a userX's profile. + * If the user is already present in store, do not fetch again. + * Finally, Navigate to profile of the user selected. + */ + if (userXId && !userXInStore(state, ScreenType.Search, id)) { + await fetchUserX( + dispatch, + {userId: id, username: username}, + ScreenType.Search, + ); + } + + navigation.navigate('Profile', { + userXId, + screenType: ScreenType.Search, + }); + } catch (e) { + console.log(e); + } + }; + + const userCell = () => { + return ( + <TouchableOpacity + onPress={addToRecentlyStoredAndNavigateToProfile} + style={styles.cellContainer}> + <Image + defaultSource={defaultUserProfile()} + source={{uri: avatar}} + style={styles.imageContainer} + /> + <View style={[styles.initialTextContainer, styles.multiText]}> + <Text style={styles.initialTextStyle}>{`@${username}`}</Text> + <Text style={styles.secondaryTextStyle}> + {first_name + ' ' + last_name} + </Text> + </View> + </TouchableOpacity> + ); + }; + + const searchIcon = () => { + return require('../../assets/images/search.png'); + }; + + const universityIcon = () => { + return require('../../assets/images/bwbadges.png'); + }; + + const categoryCell = () => { + return ( + <TouchableOpacity style={styles.cellContainer}> + <View style={[styles.imageContainer, styles.categoryBackground]}> + <Image + resizeMode="contain" + source={category === 'Brown' ? universityIcon() : searchIcon()} + style={styles.categoryImage} + /> + </View> + <View style={styles.initialTextContainer}> + <Text style={styles.initialTextStyle}>{name}</Text> + </View> + </TouchableOpacity> + ); + }; + + return name === undefined ? userCell() : categoryCell(); +}; + +const styles = StyleSheet.create({ + cellContainer: { + flexDirection: 'row', + marginHorizontal: SCREEN_WIDTH * 0.08, + marginBottom: SCREEN_WIDTH * 0.08, + }, + imageContainer: { + width: SCREEN_WIDTH * 0.112, + height: SCREEN_WIDTH * 0.112, + borderRadius: (SCREEN_WIDTH * 0.112) / 2, + }, + categoryBackground: { + backgroundColor: 'rgba(196, 196, 196, 0.45)', + justifyContent: 'center', + alignItems: 'center', + }, + categoryImage: { + width: '40%', + height: '40%', + }, + initialTextContainer: { + marginLeft: SCREEN_WIDTH * 0.08, + flexDirection: 'column', + justifyContent: 'center', + }, + initialTextStyle: { + fontWeight: '500', + fontSize: normalize(14), + }, + secondaryTextStyle: { + fontWeight: '500', + fontSize: normalize(12), + color: '#828282', + }, + multiText: {justifyContent: 'space-between'}, +}); + +export default SearchResultsCell; diff --git a/src/components/search/SearchResultList.tsx b/src/components/search/SearchResultList.tsx new file mode 100644 index 00000000..a3d9c8c5 --- /dev/null +++ b/src/components/search/SearchResultList.tsx @@ -0,0 +1,93 @@ +import React, {useEffect, useState} from 'react'; +import {SectionList, StyleSheet, Text, View} from 'react-native'; +import {useSelector} from 'react-redux'; +import {RootState} from 'src/store/rootreducer'; +import {NO_RESULTS_FOUND} from '../../constants/strings'; +import {PreviewType, ScreenType} from '../../types'; +import {normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; +import SearchResultsCell from './SearchResultCell'; + +interface SearchResultsProps { + results: Array<any> | undefined; + keyboardVisible: boolean; + previewType: PreviewType; + screenType: ScreenType; +} + +const sectionHeader: React.FC<Boolean> = (showBorder: Boolean) => { + if (showBorder) { + return <View style={styles.sectionHeaderStyle} />; + } + return null; +}; + +const SearchResultList: React.FC<SearchResultsProps> = ({ + results, + keyboardVisible, +}) => { + const [showSection, setShowSection] = useState(true); + const [showEmptyView, setshowEmptyView] = useState(false); + const {user: loggedInUser} = useSelector((state: RootState) => state.user); + + useEffect(() => { + if (results && results.length > 0) { + setshowEmptyView( + results[0].data.length === 0 && results[1].data.length === 0, + ); + } + }, [results]); + + return ( + <View style={styles.container}> + {showEmptyView && ( + <View style={styles.noResultsTextContainer}> + <Text style={styles.noResultsTextStyle}>{NO_RESULTS_FOUND}</Text> + </View> + )} + {!showEmptyView && ( + <SectionList + style={[ + {width: SCREEN_WIDTH}, + keyboardVisible ? styles.keyboardOpen : {}, + ]} + contentContainerStyle={{paddingBottom: SCREEN_HEIGHT * 0.1}} + sections={results} + keyExtractor={(item, index) => item.id + index} + renderItem={({item}) => ( + <SearchResultsCell profileData={item} loggedInUser={loggedInUser} /> + )} + renderSectionHeader={({section: {title, data}}) => { + if (title === 'categories' && data.length === 0) { + setShowSection(false); + } + return sectionHeader(title !== 'categories' && showSection); + }} + /> + )} + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + marginTop: SCREEN_HEIGHT * 0.02, + }, + sectionHeaderStyle: { + width: '100%', + height: 0.5, + marginBottom: normalize(24), + backgroundColor: '#C4C4C4', + }, + keyboardOpen: {marginBottom: SCREEN_HEIGHT * 0.3}, + noResultsTextContainer: { + justifyContent: 'center', + flexDirection: 'row', + width: SCREEN_WIDTH, + }, + noResultsTextStyle: { + fontWeight: '500', + fontSize: normalize(14), + }, +}); + +export default SearchResultList; diff --git a/src/components/search/SearchResultsBackground.tsx b/src/components/search/SearchResultsBackground.tsx index 77b1821d..c5fcc6fb 100644 --- a/src/components/search/SearchResultsBackground.tsx +++ b/src/components/search/SearchResultsBackground.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import Animated, {interpolate} from 'react-native-reanimated'; import {StyleSheet} from 'react-native'; +import Animated, {interpolate} from 'react-native-reanimated'; import {SCREEN_HEIGHT, SCREEN_WIDTH, StatusBarHeight} from '../../utils'; interface SearchResultsBackgroundProps { @@ -21,11 +21,9 @@ const SearchResultsBackground: React.FC<SearchResultsBackgroundProps> = ({ return ( <Animated.View style={[styles.container, {opacity: opacityBackground, top}]}> - <Animated.ScrollView - contentContainerStyle={styles.contentContainer} - style={[styles.results, {opacity: opacityContent}]}> + <Animated.View style={[styles.results, {opacity: opacityContent}]}> {children} - </Animated.ScrollView> + </Animated.View> </Animated.View> ); }; @@ -34,7 +32,6 @@ const styles = StyleSheet.create({ flex: 1, height: SCREEN_HEIGHT, width: SCREEN_WIDTH, - padding: 20, position: 'absolute', backgroundColor: '#fff', zIndex: 0, diff --git a/src/components/search/index.ts b/src/components/search/index.ts index 08052f77..7418f0ba 100644 --- a/src/components/search/index.ts +++ b/src/components/search/index.ts @@ -3,5 +3,6 @@ export {default as SearchHeader} from './SearchHeader'; export {default as SearchBar} from './SearchBar'; export {default as Explore} from './Explore'; export {default as SearchResultsBackground} from './SearchResultsBackground'; +export {default as SearchResultList} from './SearchResultList'; export {default as SearchResults} from './SearchResults'; export {default as DiscoverUsers} from './DiscoverUsers'; diff --git a/src/components/suggestedPeople/MutualFriends.tsx b/src/components/suggestedPeople/MutualFriends.tsx index fdda104a..f72104d4 100644 --- a/src/components/suggestedPeople/MutualFriends.tsx +++ b/src/components/suggestedPeople/MutualFriends.tsx @@ -70,6 +70,7 @@ const MutualFriends: React.FC<MutualFriendsProps> = ({ previewType={'Suggested People Drawer'} screenType={ScreenType.SuggestedPeople} profilePreview={profilePreview} + setMFDrawer={setDrawerVisible} /> ))} </ScrollView> diff --git a/src/components/taggs/SocialMediaInfo.tsx b/src/components/taggs/SocialMediaInfo.tsx index f05ad503..5497226c 100644 --- a/src/components/taggs/SocialMediaInfo.tsx +++ b/src/components/taggs/SocialMediaInfo.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {StyleSheet, Text, View} from 'react-native'; import {ScreenType} from '../../types'; -import {SocialIcon} from '..'; +import {SocialIcon} from '../common'; import {handleOpenSocialUrlOnBrowser} from '../../utils'; interface SocialMediaInfoProps { diff --git a/src/constants/api.ts b/src/constants/api.ts index dcc2032d..0fc846c3 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -4,7 +4,7 @@ const BASE_URL: string = 'http://127.0.0.1:8000/'; // local server const API_URL: string = BASE_URL + 'api/'; export const LOGIN_ENDPOINT: string = API_URL + 'login/'; -export const VERSION_ENDPOINT: string = API_URL + 'version/'; +export const VERSION_ENDPOINT: string = API_URL + 'version/v2/'; export const REGISTER_ENDPOINT: string = API_URL + 'register/'; export const EDIT_PROFILE_ENDPOINT: string = API_URL + 'edit-profile/'; export const SEND_OTP_ENDPOINT: string = API_URL + 'send-otp/'; @@ -17,7 +17,7 @@ export const PROFILE_PHOTO_THUMBNAIL_ENDPOINT: string = export const GET_IG_POSTS_ENDPOINT: string = API_URL + 'posts-ig/'; export const GET_FB_POSTS_ENDPOINT: string = API_URL + 'posts-fb/'; export const GET_TWITTER_POSTS_ENDPOINT: string = API_URL + 'posts-twitter/'; -export const SEARCH_ENDPOINT: string = API_URL + 'search/'; +export const SEARCH_ENDPOINT: string = API_URL + 'search/v2/'; export const MOMENTS_ENDPOINT: string = API_URL + 'moments/'; export const MOMENT_THUMBNAIL_ENDPOINT: string = API_URL + 'moment-thumbnail/'; export const VERIFY_INVITATION_CODE_ENDPOUNT: string = API_URL + 'verify-code/'; diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 84106df0..72eb1b57 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -181,13 +181,15 @@ export const MOMENT_CATEGORY_BG_COLORS: string[] = [ '#4E7175', ]; +// order matters, this decides the order which it displays export const EXPLORE_SECTION_TITLES: ExploreSectionType[] = [ 'New to Tagg', 'People You May Know', 'Trending on Tagg', - "Brown '21", - "Brown '22", + "Brown '24", "Brown '23", + "Brown '22", + "Brown '21", ]; export const SP_WIDTH = 375; diff --git a/src/constants/strings.ts b/src/constants/strings.ts index 5ae19e9c..7652fccd 100644 --- a/src/constants/strings.ts +++ b/src/constants/strings.ts @@ -14,6 +14,7 @@ export const ERROR_DELETED_OBJECT = 'Oh sad! Looks like the comment / moment was export const ERROR_DOUBLE_CHECK_CONNECTION = 'Please double-check your network connection and retry'; export const ERROR_DUP_OLD_PWD = 'You may not use a previously used password'; export const ERROR_EMAIL_IN_USE = 'Email already in use, please try another one'; +export const ERROR_PHONE_IN_USE = 'Phone already in use, please try another one'; export const ERROR_FAILED_LOGIN_INFO = 'Login failed, please try re-entering your login information'; export const ERROR_FAILED_TO_COMMENT = 'Unable to post comment, refresh and try again!'; export const ERROR_FAILED_TO_DELETE_COMMENT = 'Unable to delete comment, refresh and try again!'; @@ -25,13 +26,16 @@ export const ERROR_INVALID_VERIFICATION_CODE_FORMAT = 'Please enter the 6 digit export const ERROR_INVLAID_CODE = 'The code entered is not valid!'; export const ERROR_LINK = (str: string) => `Unable to link with ${str}, Please check your login and try again`; export const ERROR_LOGIN = 'There was a problem logging you in, please refresh and try again'; -export const ERROR_LOGIN_FAILED = 'Login failed. Check your username and passoword, and try again'; +export const ERROR_LOGIN_FAILED = 'Login failed. Check your username and password, and try again'; export const ERROR_NEXT_PAGE = 'There was a problem while loading the next page 😓, try again in a couple minutes'; export const ERROR_PROFILE_CREATION_SHORT = 'Profile creation failed 😓'; export const ERROR_PWD_ACCOUNT = (str: string) => `Please make sure that the email / username entered is registered with us. You may contact our customer support at ${str}`; export const ERROR_REGISTRATION = (str: string) => `Registration failed 😔, ${str}`; export const ERROR_SELECT_CLASS_YEAR = 'Please select your Class Year'; +export const ERROR_SELECT_BIRTHDAY = 'Please select your birthday'; +export const ERROR_SELECT_GENDER = 'Please select your gender'; export const ERROR_SERVER_DOWN = 'mhm, looks like our servers are down, please refresh and try again in a few mins'; +export const ERROR_TWILIO_SERVER_ERROR = 'mhm, looks like that is an invalid phone number or our servers are down, please try again in a few mins'; export const ERROR_SOMETHING_WENT_WRONG = 'Oh dear, don’t worry someone will be held responsible for this error, In the meantime refresh the app'; export const ERROR_SOMETHING_WENT_WRONG_REFRESH = "Ha, looks like this one's on us, please refresh and try again"; export const ERROR_SOMETHING_WENT_WRONG_RELOAD = "You broke it, Just kidding! we don't know what happened... Please reload the app and try again"; @@ -45,11 +49,14 @@ export const ERROR_VERIFICATION_FAILED_SHORT = 'Verification failed 😓'; export const MARKED_AS_MSG = (str: string) => `Marked as ${str}`; export const MOMENT_DELETED_MSG = 'Moment deleted....Some moments have to go, to create space for greater ones'; export const NO_NEW_NOTIFICATIONS = 'You have no new notifications'; +export const NO_RESULTS_FOUND = 'No Results Found!'; export const SUCCESS_CATEGORY_DELETE = 'Category successfully deleted, but its memory will live on'; export const SUCCESS_LINK = (str: string) => `Successfully linked ${str} 🎉`; export const SUCCESS_PIC_UPLOAD = 'Beautiful, the picture was uploaded successfully!'; export const SUCCESS_PWD_RESET = 'Your password was reset successfully!'; export const SUCCESS_VERIFICATION_CODE_SENT = 'New verification code sent! Check your phone messages for your code'; +export const SUCCESS_INVITATION_CODE = 'Perfect! You entered a valid invitation code, you are now able to login and explore Tagg!'; +export const ERROR_NOT_ONBOARDED = 'You are now on waitlist, please enter your invitation code if you have one'; export const UP_TO_DATE = 'Up-to-Date!'; export const UPLOAD_MOMENT_PROMPT_ONE_MESSAGE = 'Post your first moment to\n continue building your digital\nidentity!'; export const UPLOAD_MOMENT_PROMPT_THREE_HEADER = 'Continue to build your profile'; diff --git a/src/routes/Routes.tsx b/src/routes/Routes.tsx index 1cbc9bc5..c7b9aeee 100644 --- a/src/routes/Routes.tsx +++ b/src/routes/Routes.tsx @@ -3,7 +3,7 @@ import React, {useEffect, useState} from 'react'; import DeviceInfo from 'react-native-device-info'; import SplashScreen from 'react-native-splash-screen'; import {useDispatch, useSelector} from 'react-redux'; -import {fcmService, getLiveVersion} from '../services'; +import {fcmService, getCurrentLiveVersions} from '../services'; import { updateNewNotificationReceived, updateNewVersionAvailable, @@ -54,8 +54,8 @@ const Routes: React.FC = () => { useEffect(() => { const checkVersion = async () => { - const liveVersion = await getLiveVersion(); - if (liveVersion && liveVersion !== DeviceInfo.getVersion()) { + const liveVersions = await getCurrentLiveVersions(); + if (liveVersions && !liveVersions.includes(DeviceInfo.getVersion())) { setNewVersionAvailable(true); dispatch(updateNewVersionAvailable(true)); } diff --git a/src/routes/main/MainStackScreen.tsx b/src/routes/main/MainStackScreen.tsx index 7b55d249..42b096f1 100644 --- a/src/routes/main/MainStackScreen.tsx +++ b/src/routes/main/MainStackScreen.tsx @@ -1,7 +1,7 @@ import AsyncStorage from '@react-native-community/async-storage'; import {RouteProp} from '@react-navigation/native'; import {StackNavigationOptions} from '@react-navigation/stack'; -import React, {useState} from 'react'; +import React, {useEffect, useState} from 'react'; import {StyleSheet, Text} from 'react-native'; import {normalize} from 'react-native-elements'; import BackIcon from '../../assets/icons/back-arrow.svg'; @@ -51,15 +51,23 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => { const isSearchTab = screenType === ScreenType.Search; const isNotificationsTab = screenType === ScreenType.Notifications; const isSuggestedPeopleTab = screenType === ScreenType.SuggestedPeople; - - AsyncStorage.getItem('respondedToAccessContacts').then((value) => - setRespondedToAccessContacts(value ? value : 'false'), - ); - const [respondedToAccessContacts, setRespondedToAccessContacts] = useState( - 'false', + 'true', ); + const loadResponseToAccessContacts = () => { + AsyncStorage.getItem('respondedToAccessContacts') + .then((value) => { + setRespondedToAccessContacts(value ? value : 'false'); + }) + .catch((error) => { + console.log('Something went wrong', error); + setRespondedToAccessContacts('true'); + }); + }; + + loadResponseToAccessContacts(); + const initialRouteName = (() => { switch (screenType) { case ScreenType.Profile: @@ -73,20 +81,6 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => { } })(); - const modalStyle: StackNavigationOptions = { - cardStyle: {backgroundColor: 'rgba(80,80,80,0.9)'}, - gestureDirection: 'vertical', - cardOverlayEnabled: true, - cardStyleInterpolator: ({current: {progress}}) => ({ - cardStyle: { - opacity: progress.interpolate({ - inputRange: [0, 0.5, 0.9, 1], - outputRange: [0, 0.25, 0.7, 1], - }), - }, - }), - }; - const tutorialModalStyle: StackNavigationOptions = { cardStyle: {backgroundColor: 'rgba(0, 0, 0, 0.5)'}, gestureDirection: 'vertical', @@ -263,7 +257,7 @@ export const headerBarOptions: ( ), }); -const modalStyle: StackNavigationOptions = { +export const modalStyle: StackNavigationOptions = { cardStyle: {backgroundColor: 'rgba(80,80,80,0.6)'}, gestureDirection: 'vertical', cardOverlayEnabled: true, diff --git a/src/routes/onboarding/OnboardingStackNavigator.tsx b/src/routes/onboarding/OnboardingStackNavigator.tsx index 9f614f7c..0cdeecdf 100644 --- a/src/routes/onboarding/OnboardingStackNavigator.tsx +++ b/src/routes/onboarding/OnboardingStackNavigator.tsx @@ -1,46 +1,41 @@ import {createStackNavigator} from '@react-navigation/stack'; -import { - CategorySelectionScreenType, - TaggPopupType, - UserType, - VerificationScreenType, -} from '../../types'; +import {TaggPopupType, VerificationScreenType} from '../../types'; export type OnboardingStackParams = { - WelcomeScreen: undefined; Login: undefined; + WelcomeScreen: undefined; PasswordResetRequest: undefined; - PasswordReset: { - value: string; - }; - InvitationCodeVerification: undefined; - RegistrationOne: undefined; - RegistrationTwo: {phone: string}; - RegistrationThree: { - firstName: string; - lastName: string; - phone: string; - email: string; - }; - Checkpoint: {username: string; userId: string}; + PasswordReset: {value: string}; Verification: {id: string; screenType: VerificationScreenType}; - ProfileOnboarding: {username: string; userId: string}; - SocialMedia: {username: string; userId: string}; - CategorySelection: { - screenType: CategorySelectionScreenType; - user: UserType; - newCustomCategory: string | undefined; - }; - CreateCustomCategory: { - screenType: CategorySelectionScreenType; - user: UserType; - existingCategories: string[]; - }; + // RegistrationOne: undefined; + // RegistrationTwo: {phone: string}; + // RegistrationThree: { + // firstName: string; + // lastName: string; + // phone: string; + // email: string; + // }; + // Checkpoint: {username: string; userId: string}; + // ProfileOnboarding: {username: string; userId: string}; + // SocialMedia: {username: string; userId: string}; + // CategorySelection: { + // screenType: CategorySelectionScreenType; + // user: UserType; + // newCustomCategory: string | undefined; + // }; + // CreateCustomCategory: { + // screenType: CategorySelectionScreenType; + // user: UserType; + // existingCategories: string[]; + // }; TaggPopup: { popupProps: TaggPopupType; }; - AddWaitlistUser: undefined; - WaitlistSuccess: undefined; + OnboardingStepOne: undefined; + PhoneVerification: {firstName: string; lastName: string; phone: string}; + OnboardingStepTwo: {firstName: string; lastName: string; phone: string}; + OnboardingStepThree: {userId: string; username: string}; + InvitationCodeVerification: {userId: string}; }; export const OnboardingStack = createStackNavigator<OnboardingStackParams>(); diff --git a/src/routes/onboarding/OnboardingStackScreen.tsx b/src/routes/onboarding/OnboardingStackScreen.tsx index 78f113cc..79171efd 100644 --- a/src/routes/onboarding/OnboardingStackScreen.tsx +++ b/src/routes/onboarding/OnboardingStackScreen.tsx @@ -1,24 +1,20 @@ +import {StackCardInterpolationProps} from '@react-navigation/stack'; import React from 'react'; -import {OnboardingStack} from './OnboardingStackNavigator'; +import TaggPopup from '../../components/common/TaggPopup'; import { - Login, InvitationCodeVerification, - RegistrationOne, - RegistrationTwo, - RegistrationThree, - Verification, - ProfileOnboarding, - Checkpoint, - SocialMedia, - PasswordResetRequest, + Login, + OnboardingStepThree, + OnboardingStepTwo, PasswordReset, + PasswordResetRequest, + PhoneVerification, + Verification, WelcomeScreen, - CategorySelection, - AddWaitlistUserScreen, - WaitlistSuccessScreen, } from '../../screens'; -import {StackCardInterpolationProps} from '@react-navigation/stack'; -import TaggPopup from '../../components/common/TaggPopup'; +import OnboardingStepOne from '../../screens/onboarding/OnboardingStepOne'; +import {modalStyle} from '../main'; +import {OnboardingStack} from './OnboardingStackNavigator'; const forFade = ({current}: StackCardInterpolationProps) => ({ cardStyle: { @@ -37,6 +33,7 @@ const Onboarding: React.FC = () => { options={{ gestureEnabled: false, cardStyleInterpolator: forFade, + ...modalStyle, }} /> <OnboardingStack.Screen @@ -46,37 +43,13 @@ const Onboarding: React.FC = () => { gestureEnabled: false, }} /> - <OnboardingStack.Screen - name="WelcomeScreen" - component={WelcomeScreen} - options={{ - gestureEnabled: false, - }} - /> - <OnboardingStack.Screen - name="CategorySelection" - component={CategorySelection} - options={{ - gestureEnabled: false, - }} - /> + <OnboardingStack.Screen name="WelcomeScreen" component={WelcomeScreen} /> <OnboardingStack.Screen name="TaggPopup" component={TaggPopup} options={{ gestureEnabled: false, - cardStyle: { - backgroundColor: 'transparent', - }, - cardOverlayEnabled: true, - cardStyleInterpolator: ({current: {progress}}) => ({ - cardStyle: { - opacity: progress.interpolate({ - inputRange: [0, 0.5, 0.9, 1], - outputRange: [0, 0.25, 0.7, 1], - }), - }, - }), + ...modalStyle, }} /> <OnboardingStack.Screen @@ -84,42 +57,42 @@ const Onboarding: React.FC = () => { component={PasswordReset} options={{ gestureEnabled: false, + ...modalStyle, }} /> <OnboardingStack.Screen - name="InvitationCodeVerification" - component={InvitationCodeVerification} - /> - <OnboardingStack.Screen - name="AddWaitlistUser" - component={AddWaitlistUserScreen} + name="Verification" + component={Verification} + options={{ + ...modalStyle, + }} /> <OnboardingStack.Screen - name="WaitlistSuccess" - component={WaitlistSuccessScreen} + name="OnboardingStepOne" + component={OnboardingStepOne} /> <OnboardingStack.Screen - name="RegistrationOne" - component={RegistrationOne} + name="PhoneVerification" + component={PhoneVerification} + options={{...modalStyle}} /> <OnboardingStack.Screen - name="RegistrationTwo" - component={RegistrationTwo} + name="OnboardingStepTwo" + component={OnboardingStepTwo} + options={{...modalStyle}} /> <OnboardingStack.Screen - name="RegistrationThree" - component={RegistrationThree} + name="OnboardingStepThree" + component={OnboardingStepThree} + options={{...modalStyle}} /> - <OnboardingStack.Screen name="Checkpoint" component={Checkpoint} /> - <OnboardingStack.Screen name="Verification" component={Verification} /> <OnboardingStack.Screen - name="ProfileOnboarding" - component={ProfileOnboarding} + name="InvitationCodeVerification" + component={InvitationCodeVerification} options={{ - gestureEnabled: false, + ...modalStyle, }} /> - <OnboardingStack.Screen name="SocialMedia" component={SocialMedia} /> </OnboardingStack.Navigator> ); }; diff --git a/src/routes/suggestedPeopleOnboarding/SuggestedPeopleOnboardingStackScreen.tsx b/src/routes/suggestedPeopleOnboarding/SuggestedPeopleOnboardingStackScreen.tsx index 75764a15..61cc694c 100644 --- a/src/routes/suggestedPeopleOnboarding/SuggestedPeopleOnboardingStackScreen.tsx +++ b/src/routes/suggestedPeopleOnboarding/SuggestedPeopleOnboardingStackScreen.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import {SuggestedPeopleOnboardingStack} from '.'; +import {SuggestedPeopleOnboardingStack} from './SuggestedPeopleOnboardingStackNavigator'; import { SuggestedPeopleWelcomeScreen, SuggestedPeopleUploadPictureScreen, 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} diff --git a/src/services/CommonService.ts b/src/services/CommonService.ts index 9fa7417f..5bc1174d 100644 --- a/src/services/CommonService.ts +++ b/src/services/CommonService.ts @@ -22,7 +22,7 @@ export const loadImageFromURL = async (url: string) => { } }; -export const getLiveVersion = async () => { +export const getCurrentLiveVersions = async () => { try { const response = await fetch(VERSION_ENDPOINT, {method: 'GET'}); return response.status === 200 ? await response.json() : undefined; diff --git a/src/services/ExploreService.ts b/src/services/ExploreService.ts index 980258be..dc58bdd0 100644 --- a/src/services/ExploreService.ts +++ b/src/services/ExploreService.ts @@ -50,6 +50,7 @@ export const getAllExploreSections = async () => { "Brown '21": data.categories.brown_21, "Brown '22": data.categories.brown_22, "Brown '23": data.categories.brown_23, + "Brown '24": data.categories.brown_24, }; return exploreSections; diff --git a/src/services/SearchService.ts b/src/services/SearchService.ts new file mode 100644 index 00000000..7b97f9a7 --- /dev/null +++ b/src/services/SearchService.ts @@ -0,0 +1,22 @@ +import AsyncStorage from '@react-native-community/async-storage'; + +export const loadSearchResults = async (url: string) => { + try { + const token = await AsyncStorage.getItem('token'); + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: 'Token ' + token, + }, + }); + const {status} = response; + if (status === 200) { + const searchResults = await response.json(); + return searchResults; + } + } catch (error) { + console.log(error); + throw error; + } + return {}; +}; diff --git a/src/services/UserProfileService.ts b/src/services/UserProfileService.ts index bfc4933f..dd77db9f 100644 --- a/src/services/UserProfileService.ts +++ b/src/services/UserProfileService.ts @@ -11,6 +11,7 @@ import { PROFILE_INFO_ENDPOINT, PROFILE_PHOTO_ENDPOINT, PROFILE_PHOTO_THUMBNAIL_ENDPOINT, + REGISTER_ENDPOINT, SEND_OTP_ENDPOINT, TAGG_CUSTOMER_SUPPORT, VERIFY_OTP_ENDPOINT, @@ -292,7 +293,6 @@ export const verifyOtp = async (phone: string, otp: string) => { export const sendOtp = async (phone: string) => { try { - console.log(phone); let response = await fetch(SEND_OTP_ENDPOINT, { method: 'POST', body: JSON.stringify({ @@ -313,3 +313,46 @@ export const sendOtp = async (phone: string) => { return false; } }; + +export const sendOtpStatusCode = async (phone: string) => { + try { + let response = await fetch(SEND_OTP_ENDPOINT, { + method: 'POST', + body: JSON.stringify({ + phone_number: '+1' + phone, + }), + }); + + return response.status; + } catch (error) { + console.log(error); + return undefined; + } +}; + +export const sendRegister = async ( + firstName: string, + lastName: string, + phone: string, + email: string, + username: string, + password: string, +) => { + try { + const response = await fetch(REGISTER_ENDPOINT, { + method: 'POST', + body: JSON.stringify({ + first_name: firstName, + last_name: lastName, + email: email, + phone_number: phone, + username: username, + password: password, + }), + }); + return response; + } catch (error) { + console.log(error); + return undefined; + } +}; diff --git a/src/services/index.ts b/src/services/index.ts index ef71233a..28e03e0e 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -12,3 +12,4 @@ export * from './WaitlistUserService'; export * from './CommonService'; export * from './CommentService'; export * from './SuggestedPeopleService'; +export * from './SearchService'; diff --git a/src/store/initialStates.ts b/src/store/initialStates.ts index 4b61a2b1..1a3db433 100644 --- a/src/store/initialStates.ts +++ b/src/store/initialStates.ts @@ -79,6 +79,7 @@ export const EMPTY_EXPLORE_SECTIONS: Record< "Brown '21": EMPTY_PROFILE_PREVIEW_LIST, "Brown '22": EMPTY_PROFILE_PREVIEW_LIST, "Brown '23": EMPTY_PROFILE_PREVIEW_LIST, + "Brown '24": EMPTY_PROFILE_PREVIEW_LIST, }; export const NO_TAGG_USERS = { diff --git a/src/types/types.ts b/src/types/types.ts index 3ad787f2..7cd11f7a 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -126,7 +126,8 @@ export type ExploreSectionType = | 'Trending on Tagg' | "Brown '21" | "Brown '22" - | "Brown '23"; + | "Brown '23" + | "Brown '24"; /** * Redux store to have a Record of ScreenType (Search, Profile, Home etc) mapped to diff --git a/src/utils/common.ts b/src/utils/common.ts index 6a8b66d3..30122e79 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -29,7 +29,7 @@ export const handleOpenSocialUrlOnBrowser = ( //Returns university class just like we would like to display on profile page export const getUniversityClass = (universityClass: number) => { - return `Class of ${(universityClass % 2000).toString()}'`; + return `Class of '${(universityClass % 2000).toString()}`; }; export const getDateAge: ( diff --git a/src/utils/users.ts b/src/utils/users.ts index ca917ae4..653c941e 100644 --- a/src/utils/users.ts +++ b/src/utils/users.ts @@ -159,3 +159,40 @@ export const checkIfUserIsBlocked = async ( } return await isUserBlocked(userId, loggedInUser.userId, token); }; + +export const defaultUserProfile = () => { + const defaultImage = require('../assets/images/avatar-placeholder.png'); + return defaultImage; +}; + +export const addUserToRecentlyViewed = async (user: ProfilePreviewType) => { + const jsonValue = await AsyncStorage.getItem('@recently_searched_users'); + let recentlySearchedList = jsonValue != null ? JSON.parse(jsonValue) : null; + if (recentlySearchedList) { + if (recentlySearchedList.length > 0) { + if ( + recentlySearchedList.some( + (saved_user: ProfilePreviewType) => saved_user.id === user.id, + ) + ) { + console.log('User already in recently searched.'); + } else { + if (recentlySearchedList.length >= 10) { + recentlySearchedList.pop(); + } + recentlySearchedList.unshift(user); + } + } + } else { + recentlySearchedList = [user]; + } + try { + let recentlySearchedListString = JSON.stringify(recentlySearchedList); + await AsyncStorage.setItem( + '@recently_searched_users', + recentlySearchedListString, + ); + } catch (e) { + console.log(e); + } +}; |