aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorIvan Chen <ivan@tagg.id>2021-03-05 16:38:32 -0500
committerIvan Chen <ivan@tagg.id>2021-03-05 16:38:32 -0500
commit1465df9621fb963ff873485ad927ff79ea547fa0 (patch)
treeaffcb43f37f263f3e0e555dd019dd952b62e1f0a /src
parent2360e774d94e271d1d9db0d5b92b801b9325535e (diff)
parentb1dee65ee7bb8e120fc38a495f4027905d300650 (diff)
Merge branch 'master' into tma-634-badge-selection-screen
# Conflicts: # src/components/taggs/SocialMediaInfo.tsx
Diffstat (limited to 'src')
-rw-r--r--src/assets/icons/purple-plus.pngbin0 -> 18370 bytes
-rw-r--r--src/assets/images/bwbadges.pngbin0 -> 528 bytes
-rw-r--r--src/assets/images/bwbadges@2x.pngbin0 -> 991 bytes
-rw-r--r--src/assets/images/bwbadges@3x.pngbin0 -> 1438 bytes
-rw-r--r--src/assets/images/search.pngbin0 -> 354 bytes
-rw-r--r--src/assets/images/search@2x.pngbin0 -> 643 bytes
-rw-r--r--src/assets/images/search@3x.pngbin0 -> 901 bytes
-rw-r--r--src/components/comments/CommentsContainer.tsx2
-rw-r--r--src/components/common/GenericMoreInfoDrawer.tsx2
-rw-r--r--src/components/common/SocialLinkModal.tsx2
-rw-r--r--src/components/common/TaggPopup.tsx2
-rw-r--r--src/components/common/index.ts2
-rw-r--r--src/components/moments/MomentPostHeader.tsx2
-rw-r--r--src/components/onboarding/BirthDatePicker.tsx4
-rw-r--r--src/components/onboarding/LinkSocialMedia.tsx2
-rw-r--r--src/components/onboarding/RegistrationWizard.tsx22
-rw-r--r--src/components/profile/Content.tsx2
-rw-r--r--src/components/profile/Friends.tsx2
-rw-r--r--src/components/profile/ProfileHeader.tsx2
-rw-r--r--src/components/profile/ProfilePreview.tsx42
-rw-r--r--src/components/search/ExploreSection.tsx2
-rw-r--r--src/components/search/RecentSearches.tsx9
-rw-r--r--src/components/search/SearchResultCell.tsx187
-rw-r--r--src/components/search/SearchResultList.tsx93
-rw-r--r--src/components/search/SearchResultsBackground.tsx9
-rw-r--r--src/components/search/index.ts1
-rw-r--r--src/components/suggestedPeople/MutualFriends.tsx1
-rw-r--r--src/components/taggs/SocialMediaInfo.tsx2
-rw-r--r--src/constants/api.ts4
-rw-r--r--src/constants/constants.ts6
-rw-r--r--src/constants/strings.ts9
-rw-r--r--src/routes/Routes.tsx6
-rw-r--r--src/routes/main/MainStackScreen.tsx38
-rw-r--r--src/routes/onboarding/OnboardingStackNavigator.tsx63
-rw-r--r--src/routes/onboarding/OnboardingStackScreen.tsx95
-rw-r--r--src/routes/suggestedPeopleOnboarding/SuggestedPeopleOnboardingStackScreen.tsx2
-rw-r--r--src/screens/main/NotificationsScreen.tsx2
-rw-r--r--src/screens/onboarding/InvitationCodeVerification.tsx110
-rw-r--r--src/screens/onboarding/Login.tsx15
-rw-r--r--src/screens/onboarding/OnboardingStepOne.tsx263
-rw-r--r--src/screens/onboarding/OnboardingStepThree.tsx403
-rw-r--r--src/screens/onboarding/OnboardingStepTwo.tsx369
-rw-r--r--src/screens/onboarding/PasswordReset.tsx1
-rw-r--r--src/screens/onboarding/PasswordResetRequest.tsx29
-rw-r--r--src/screens/onboarding/PhoneVerification.tsx225
-rw-r--r--src/screens/onboarding/Verification.tsx51
-rw-r--r--src/screens/onboarding/WelcomeScreen.tsx7
-rw-r--r--src/screens/onboarding/index.ts4
-rw-r--r--src/screens/profile/CaptionScreen.tsx2
-rw-r--r--src/screens/profile/ProfileScreen.tsx2
-rw-r--r--src/screens/search/RequestContactsAccess.tsx28
-rw-r--r--src/screens/search/SearchScreen.tsx74
-rw-r--r--src/screens/search/mock.ts118
-rw-r--r--src/screens/suggestedPeople/AnimatedTutorial.tsx40
-rw-r--r--src/screens/suggestedPeople/SPBody.tsx264
-rw-r--r--src/screens/suggestedPeople/SuggestedPeopleScreen.tsx174
-rw-r--r--src/services/CommonService.ts2
-rw-r--r--src/services/ExploreService.ts1
-rw-r--r--src/services/SearchService.ts22
-rw-r--r--src/services/UserProfileService.ts45
-rw-r--r--src/services/index.ts1
-rw-r--r--src/store/initialStates.ts1
-rw-r--r--src/types/types.ts3
-rw-r--r--src/utils/common.ts2
-rw-r--r--src/utils/users.ts37
65 files changed, 2410 insertions, 500 deletions
diff --git a/src/assets/icons/purple-plus.png b/src/assets/icons/purple-plus.png
new file mode 100644
index 00000000..8b2ce903
--- /dev/null
+++ b/src/assets/icons/purple-plus.png
Binary files differ
diff --git a/src/assets/images/bwbadges.png b/src/assets/images/bwbadges.png
new file mode 100644
index 00000000..3a337775
--- /dev/null
+++ b/src/assets/images/bwbadges.png
Binary files differ
diff --git a/src/assets/images/bwbadges@2x.png b/src/assets/images/bwbadges@2x.png
new file mode 100644
index 00000000..60c2f995
--- /dev/null
+++ b/src/assets/images/bwbadges@2x.png
Binary files differ
diff --git a/src/assets/images/bwbadges@3x.png b/src/assets/images/bwbadges@3x.png
new file mode 100644
index 00000000..874c0c4d
--- /dev/null
+++ b/src/assets/images/bwbadges@3x.png
Binary files differ
diff --git a/src/assets/images/search.png b/src/assets/images/search.png
new file mode 100644
index 00000000..ba9906ba
--- /dev/null
+++ b/src/assets/images/search.png
Binary files differ
diff --git a/src/assets/images/search@2x.png b/src/assets/images/search@2x.png
new file mode 100644
index 00000000..fa133ae1
--- /dev/null
+++ b/src/assets/images/search@2x.png
Binary files differ
diff --git a/src/assets/images/search@3x.png b/src/assets/images/search@3x.png
new file mode 100644
index 00000000..3ea4ce15
--- /dev/null
+++ b/src/assets/images/search@3x.png
Binary files differ
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);
+ }
+};