diff options
Diffstat (limited to 'src')
42 files changed, 934 insertions, 345 deletions
diff --git a/src/assets/icons/findFriends/find-friends-blue-icon.svg b/src/assets/icons/findFriends/find-friends-blue-icon.svg new file mode 100644 index 00000000..26ca145d --- /dev/null +++ b/src/assets/icons/findFriends/find-friends-blue-icon.svg @@ -0,0 +1 @@ +<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216 216"><defs><style>.cls-1{fill:#08e2e2;}</style></defs><path class="cls-1" d="M104.28,135.51a58.59,58.59,0,1,0-8.85,8.86l21.79,21.26,9-8.27ZM58.62,145a46.22,46.22,0,1,1,46.21-46.21A46.23,46.23,0,0,1,58.62,145Z"/><path class="cls-1" d="M216,137.23a7.87,7.87,0,0,1-7.87,7.87H185.18v23a7.81,7.81,0,0,1-15.62,0v-23H146.62a7.87,7.87,0,0,1,0-15.73h22.94v-23a7.81,7.81,0,1,1,15.62,0v23h22.95A7.88,7.88,0,0,1,216,137.23Z"/></svg>
\ No newline at end of file diff --git a/src/assets/icons/invite-friends-prompt-icon.png b/src/assets/icons/invite-friends-prompt-icon.png Binary files differnew file mode 100644 index 00000000..b9422b9f --- /dev/null +++ b/src/assets/icons/invite-friends-prompt-icon.png diff --git a/src/components/common/TaggPrompt.tsx b/src/components/common/TaggPrompt.tsx index 75f3009b..d65e30c6 100644 --- a/src/components/common/TaggPrompt.tsx +++ b/src/components/common/TaggPrompt.tsx @@ -7,7 +7,7 @@ import {normalize, SCREEN_HEIGHT} from '../../utils';  type TaggPromptProps = {    messageHeader: string;    messageBody: string | Element; -  logoType: 'plus' | 'tagg'; +  logoType: 'plus' | 'tagg' | 'invite_friends';    hideCloseButton?: boolean;    noPadding?: boolean;    onClose: () => void; @@ -29,6 +29,8 @@ const TaggPrompt: React.FC<TaggPromptProps> = ({      switch (logoType) {        case 'plus':          return require('../../assets/icons/plus-logo.png'); +      case 'invite_friends': +        return require('../../assets/icons/invite-friends-prompt-icon.png');        case 'tagg':        default:          return require('../../assets/images/logo-purple.png'); diff --git a/src/components/friends/InviteFriendTile.tsx b/src/components/friends/InviteFriendTile.tsx new file mode 100644 index 00000000..95ebf16a --- /dev/null +++ b/src/components/friends/InviteFriendTile.tsx @@ -0,0 +1,135 @@ +import React, {useEffect, useState} from 'react'; +import { +  Alert, +  StyleSheet, +  Text, +  TouchableOpacity, +  TouchableWithoutFeedback, +  View, +} from 'react-native'; +import {TAGG_LIGHT_BLUE} from '../../constants'; +import {ERROR_SOMETHING_WENT_WRONG} from '../../constants/strings'; +import {inviteFriendService} from '../../services'; +import {normalize} from '../../utils'; + +interface InviteFriendTileProps { +  item: Object; +} + +const InviteFriendTile: React.FC<InviteFriendTileProps> = ({item}) => { +  const [invited, setInvited] = useState<boolean>(false); +  const [formatedPhoneNumber, setFormattedPhoneNumber] = useState<string>(''); +  const handleInviteFriend = async () => { +    const response = await inviteFriendService( +      item.phoneNumber, +      item.firstName, +      item.lastName, +    ); +    if (response) { +      setInvited(response); +    } else { +      Alert.alert(ERROR_SOMETHING_WENT_WRONG); +    } +  }; + +  useEffect(() => { +    const formatPhoneNumer = () => { +      const unformatted_number: string = item.phoneNumber; +      const part_one: string = unformatted_number.substring(2, 5); +      const part_two: string = unformatted_number.substring(5, 8); +      const part_three: string = unformatted_number.substring( +        8, +        unformatted_number.length, +      ); +      const temp = '(' + part_one + ')' + part_two + '-' + part_three; +      setFormattedPhoneNumber(temp); +    }; +    formatPhoneNumer(); +  }); + +  return ( +    <TouchableWithoutFeedback> +      <View style={styles.container}> +        <View style={styles.bodyContainer}> +          <Text style={styles.label}> +            {item.firstName + ' ' + item.lastName} +          </Text> +          <Text style={styles.phoneNumber}>{formatedPhoneNumber}</Text> +        </View> +        <TouchableOpacity +          disabled={invited} +          style={[ +            styles.button, +            invited ? styles.pendingButton : styles.inviteButton, +          ]} +          onPress={handleInviteFriend}> +          <Text +            style={[ +              styles.buttonTitle, +              invited ? styles.pendingButtonTitle : styles.inviteButtonTitle, +            ]}> +            {invited ? 'Pending' : 'Invite'} +          </Text> +        </TouchableOpacity> +      </View> +    </TouchableWithoutFeedback> +  ); +}; + +const styles = StyleSheet.create({ +  container: { +    flexDirection: 'row', +    alignItems: 'center', +    justifyContent: 'space-between', +    height: normalize(42), +    marginBottom: '5%', +  }, +  bodyContainer: { +    flexDirection: 'column', +    height: normalize(42), +    justifyContent: 'space-around', +  }, +  label: { +    fontWeight: '500', +    fontSize: normalize(14), +  }, +  phoneNumber: { +    fontSize: normalize(12), +    fontWeight: '400', +    color: '#6C6C6C', +    letterSpacing: normalize(0.1), +  }, +  button: { +    alignSelf: 'center', +    justifyContent: 'center', +    alignItems: 'center', +    width: 82, +    height: 25, +    borderWidth: 2, +    borderRadius: 2, +    padding: 0, +    borderColor: TAGG_LIGHT_BLUE, +  }, +  pendingButton: { +    backgroundColor: TAGG_LIGHT_BLUE, +  }, +  inviteButton: { +    backgroundColor: 'transparent', +  }, +  buttonTitle: { +    padding: 0, +    fontSize: normalize(11), +    fontWeight: '700', +    lineHeight: normalize(13.13), +    letterSpacing: normalize(0.6), +    paddingHorizontal: '3.8%', +  }, +  pendingButtonTitle: { +    color: 'white', +  }, +  inviteButtonTitle: { +    color: TAGG_LIGHT_BLUE, +  }, +}); + +export default InviteFriendTile; diff --git a/src/components/friends/index.ts b/src/components/friends/index.ts new file mode 100644 index 00000000..42727784 --- /dev/null +++ b/src/components/friends/index.ts @@ -0,0 +1 @@ +export {default as InviteFriendTile} from './InviteFriendTile'; diff --git a/src/components/moments/CaptionScreenHeader.tsx b/src/components/moments/CaptionScreenHeader.tsx index 46dfddfe..0638c128 100644 --- a/src/components/moments/CaptionScreenHeader.tsx +++ b/src/components/moments/CaptionScreenHeader.tsx @@ -21,18 +21,15 @@ const styles = StyleSheet.create({      flexDirection: 'row',      justifyContent: 'center',      alignItems: 'center', -    height: '5%',    },    headerContainer: { -    position: 'absolute', -    left: '50%', +    width: '90%',    },    header: { -    position: 'relative', -    right: '50%',      fontSize: 20,      fontWeight: 'bold',      color: 'white', +    textAlign: 'center',    },  });  export default CaptionScreenHeader; diff --git a/src/components/moments/IndividualMomentTitleBar.tsx b/src/components/moments/IndividualMomentTitleBar.tsx index 6cdfe0e8..88e0c308 100644 --- a/src/components/moments/IndividualMomentTitleBar.tsx +++ b/src/components/moments/IndividualMomentTitleBar.tsx @@ -18,7 +18,9 @@ const IndividualMomentTitleBar: React.FC<IndividualMomentTitleBarProps> = ({        <TouchableOpacity style={styles.closeButton} onPress={close}>          <CloseIcon height={'100%'} width={'100%'} color={'white'} />        </TouchableOpacity> -      <Text style={styles.header}>{title}</Text> +      <View style={styles.headerContainer}> +        <Text style={styles.header}>{title}</Text> +      </View>      </View>    );  }; @@ -30,6 +32,10 @@ const styles = StyleSheet.create({      justifyContent: 'center',      height: '5%',    }, +  headerContainer: { +    flexShrink: 1, +    marginLeft: '11%', +  },    header: {      color: 'white',      fontSize: normalize(18), diff --git a/src/components/moments/Moment.tsx b/src/components/moments/Moment.tsx index 10cf6070..2ac6aebb 100644 --- a/src/components/moments/Moment.tsx +++ b/src/components/moments/Moment.tsx @@ -12,7 +12,7 @@ import PlusIcon from '../../assets/icons/plus_icon-01.svg';  import BigPlusIcon from '../../assets/icons/plus_icon-02.svg';  import UpIcon from '../../assets/icons/up_icon.svg';  import {TAGG_LIGHT_BLUE} from '../../constants'; -import {ERROR_UPLOAD_MOMENT_SHORT} from '../../constants/strings'; +import {ERROR_UPLOAD} from '../../constants/strings';  import {normalize, SCREEN_WIDTH} from '../../utils';  import MomentTile from './MomentTile'; @@ -69,7 +69,7 @@ const Moment: React.FC<MomentProps> = ({        })        .catch((err) => {          if (err.code && err.code !== 'E_PICKER_CANCELLED') { -          Alert.alert(ERROR_UPLOAD_MOMENT_SHORT); +          Alert.alert(ERROR_UPLOAD);          }        });    }; @@ -81,14 +81,14 @@ const Moment: React.FC<MomentProps> = ({            {title}          </Text>          {!userXId ? ( -          <> +          <View style={{flexDirection: 'row'}}>              {showUpButton && move && (                <UpIcon                  width={19}                  height={19}                  onPress={() => move('up', title)}                  color={TAGG_LIGHT_BLUE} -                style={{marginLeft: 5}} +                style={{marginHorizontal: 4}}                />              )}              {showDownButton && move && ( @@ -97,31 +97,32 @@ const Moment: React.FC<MomentProps> = ({                  height={19}                  onPress={() => move('down', title)}                  color={TAGG_LIGHT_BLUE} -                style={{marginLeft: 5}} +                style={{marginHorizontal: 4}}                />              )} -          </> +          </View>          ) : (            <Fragment />          )} -        <View style={styles.flexer} /> +        {/* <View style={styles.flexer} /> */}          {!userXId ? ( -          <> +          <View style={{marginRight: 8, flexDirection: 'row'}}>              <PlusIcon                width={21}                height={21}                onPress={() => navigateToImagePicker()}                color={TAGG_LIGHT_BLUE} -              style={{marginRight: 10}} +              style={{marginHorizontal: 4}}              />              {shouldAllowDeletion && (                <DeleteIcon                  onPress={() => handleMomentCategoryDelete(title)}                  width={19}                  height={19} +                style={{marginHorizontal: 4}}                />              )} -          </> +          </View>          ) : (            <React.Fragment />          )} @@ -171,6 +172,7 @@ const styles = StyleSheet.create({      alignItems: 'center',    },    titleText: { +    width: '70%',      fontSize: normalize(16),      fontWeight: 'bold',      color: TAGG_LIGHT_BLUE, diff --git a/src/components/notifications/NotificationPrompts.tsx b/src/components/notifications/NotificationPrompts.tsx new file mode 100644 index 00000000..dc27925b --- /dev/null +++ b/src/components/notifications/NotificationPrompts.tsx @@ -0,0 +1,58 @@ +import React, {Fragment} from 'react'; +import {Image, StyleSheet, Text} from 'react-native'; +import {TaggPrompt} from '../common'; + +export const InviteFriendsPrompt: React.FC = () => { +  return ( +    <TaggPrompt +      messageHeader={'Invite Friends To Tagg!'} +      messageBody={ +        'A new feature that lets you invite your friends to Tagg. \nClick on your friends list to do so!' +      } +      logoType={'invite_friends'} +      hideCloseButton={true} +      noPadding={true} +      onClose={() => {}} +    /> +  ); +}; + +interface SPPromptNotificationProps { +  showSPNotifyPopUp: boolean; +} + +export const SPPromptNotification: React.FC<SPPromptNotificationProps> = ({ +  showSPNotifyPopUp, +}) => { +  return showSPNotifyPopUp ? ( +    <TaggPrompt +      messageHeader={'New Suggested People Page!'} +      messageBody={ +        <> +          <Text> +            A new page where you can discover new profiles. Just press the new{' '} +          </Text> +          <Image +            style={styles.icon} +            source={require('../../assets/navigationIcons/home.png')} +          /> +          <Text> button on the tab bar to check it out!</Text> +        </> +      } +      logoType={'tagg'} +      hideCloseButton={true} +      noPadding={true} +      onClose={() => {}} +    /> +  ) : ( +    <Fragment /> +  ); +}; + +const styles = StyleSheet.create({ +  icon: { +    width: 20, +    height: 20, +    tintColor: 'grey', +  }, +}); diff --git a/src/components/notifications/index.ts b/src/components/notifications/index.ts index 0260ce24..733b56f1 100644 --- a/src/components/notifications/index.ts +++ b/src/components/notifications/index.ts @@ -1 +1,2 @@  export {default as Notification} from './Notification'; +export {InviteFriendsPrompt} from './NotificationPrompts'; diff --git a/src/components/profile/Friends.tsx b/src/components/profile/Friends.tsx index 7c7265c5..ac724ae0 100644 --- a/src/components/profile/Friends.tsx +++ b/src/components/profile/Friends.tsx @@ -1,14 +1,23 @@ -import React from 'react'; -import {View, StyleSheet, ScrollView, Text} from 'react-native'; -import {ProfilePreviewType, ScreenType} from '../../types'; -import {ProfilePreview} from '../profile'; -import {normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; +import {useNavigation} from '@react-navigation/native'; +import React, {useEffect, useState} from 'react'; +import {Alert, Linking, ScrollView, StyleSheet, Text, View} from 'react-native'; +import {checkPermission} from 'react-native-contacts'; +import {TouchableOpacity} from 'react-native-gesture-handler'; +import {useDispatch, useSelector, useStore} from 'react-redux';  import {TAGG_LIGHT_BLUE} from '../../constants'; -import {RootState} from '../../store/rootReducer'; -import {useDispatch, useStore} from 'react-redux'; -import {handleUnfriend} from '../../utils/friends'; +import {usersFromContactsService} from '../../services';  import {NO_USER} from '../../store/initialStates'; -import {TouchableOpacity} from 'react-native-gesture-handler'; +import {RootState} from '../../store/rootReducer'; +import {ProfilePreviewType, ScreenType} from '../../types'; +import { +  extractContacts, +  normalize, +  SCREEN_HEIGHT, +  SCREEN_WIDTH, +} from '../../utils'; +import {handleAddFriend, handleUnfriend} from '../../utils/friends'; +import {ProfilePreview} from '../profile'; +import FindFriendsBlueIcon from '../../assets/icons/findFriends/find-friends-blue-icon.svg';  interface FriendsProps {    result: Array<ProfilePreviewType>; @@ -19,14 +28,101 @@ interface FriendsProps {  const Friends: React.FC<FriendsProps> = ({result, screenType, userId}) => {    const state: RootState = useStore().getState();    const dispatch = useDispatch(); -    const {user: loggedInUser = NO_USER} = state; +  const navigation = useNavigation(); +  const [usersFromContacts, setUsersFromContacts] = useState< +    ProfilePreviewType[] +  >([]); + +  useEffect(() => { +    const handleFindFriends = () => { +      extractContacts().then(async (contacts) => { +        const permission = await checkPermission(); +        if (permission === 'authorized') { +          let response = await usersFromContactsService(contacts); +          await setUsersFromContacts(response.existing_tagg_users); +        } else { +          console.log('Authorize access to contacts'); +        } +      }); +    }; +    handleFindFriends(); +  }, []); + +  const UsersFromContacts = () => ( +    <> +      {usersFromContacts?.splice(0, 2).map((profilePreview) => ( +        <View key={profilePreview.id} style={styles.container}> +          <View style={styles.friend}> +            <ProfilePreview +              {...{profilePreview}} +              previewType={'Friend'} +              screenType={screenType} +            /> +          </View> +          <TouchableOpacity +            style={styles.addFriendButton} +            onPress={() => { +              handleAddFriend(screenType, profilePreview, dispatch, state).then( +                (success) => { +                  if (success) { +                    let users = usersFromContacts; +                    setUsersFromContacts( +                      users.filter( +                        (user) => user.username !== profilePreview.username, +                      ), +                    ); +                  } +                }, +              ); +            }}> +            <Text style={styles.addFriendButtonTitle}>Add Friend</Text> +          </TouchableOpacity> +        </View> +      ))} +    </> +  );    return (      <> -      <View style={styles.subheader}> -        {/* <Text style={styles.subheaderText}>Friends</Text> */} -      </View> +      {loggedInUser.userId === userId && ( +        <View style={styles.subheader}> +          <View style={styles.addFriendHeaderContainer}> +            <Text style={[styles.subheaderText]}>Add Friends</Text> +            <TouchableOpacity +              style={styles.findFriendsButton} +              onPress={async () => { +                const permission = await checkPermission(); +                if (permission === 'authorized') { +                  navigation.navigate('InviteFriendsScreen', { +                    screenType: ScreenType.Profile, +                  }); +                } else { +                  Alert.alert( +                    '"Tagg" Would Like to Access Your Contacts', +                    'This helps you quickly get in touch with friends on the app and more', +                    [ +                      { +                        text: "Don't Allow", +                        style: 'cancel', +                      }, +                      {text: 'Allow', onPress: () => Linking.openSettings()}, +                    ], +                  ); +                } +              }}> +              <FindFriendsBlueIcon width={20} height={20} /> +              <Text style={styles.findFriendsSubheaderText}> +                Invite Friends +              </Text> +            </TouchableOpacity> +          </View> +          <UsersFromContacts /> +        </View> +      )} +      <Text style={[styles.subheaderText, styles.friendsSubheaderText]}> +        Friends +      </Text>        <ScrollView          keyboardShouldPersistTaps={'always'}          style={styles.scrollView} @@ -43,11 +139,11 @@ const Friends: React.FC<FriendsProps> = ({result, screenType, userId}) => {              </View>              {loggedInUser.userId === userId && (                <TouchableOpacity -                style={styles.button} +                style={styles.unfriendButton}                  onPress={() =>                    handleUnfriend(screenType, profilePreview, dispatch, state)                  }> -                <Text style={styles.buttonTitle}>Unfriend</Text> +                <Text style={styles.unfriendButtonTitle}>Unfriend</Text>                </TouchableOpacity>              )}            </View> @@ -63,12 +159,19 @@ const styles = StyleSheet.create({      alignSelf: 'center',      width: SCREEN_WIDTH * 0.85,    }, +  firstScrollView: {},    scrollViewContent: {      alignSelf: 'center',      paddingBottom: SCREEN_HEIGHT / 7,      width: SCREEN_WIDTH * 0.85,      marginTop: '1%',    }, +  addFriendHeaderContainer: { +    flexDirection: 'row', +    justifyContent: 'space-between', +    marginBottom: '3%', +    marginTop: '2%', +  },    header: {flexDirection: 'row'},    subheader: {      alignSelf: 'center', @@ -81,6 +184,20 @@ const styles = StyleSheet.create({      fontWeight: '600',      lineHeight: normalize(14.32),    }, +  findFriendsButton: {flexDirection: 'row'}, +  friendsSubheaderText: { +    alignSelf: 'center', +    width: SCREEN_WIDTH * 0.85, +    marginVertical: '1%', +    marginBottom: '2%', +  }, +  findFriendsSubheaderText: { +    marginLeft: '5%', +    color: '#08E2E2', +    fontSize: normalize(12), +    fontWeight: '600', +    lineHeight: normalize(14.32), +  },    container: {      alignSelf: 'center',      flexDirection: 'row', @@ -94,7 +211,7 @@ const styles = StyleSheet.create({      alignSelf: 'center',      height: '100%',    }, -  button: { +  addFriendButton: {      alignSelf: 'center',      justifyContent: 'center',      alignItems: 'center', @@ -104,9 +221,29 @@ const styles = StyleSheet.create({      borderWidth: 2,      borderRadius: 2,      padding: 0, -    backgroundColor: 'transparent', +    backgroundColor: TAGG_LIGHT_BLUE, +  }, +  addFriendButtonTitle: { +    color: 'white', +    padding: 0, +    fontSize: normalize(11), +    fontWeight: '700', +    lineHeight: normalize(13.13), +    letterSpacing: normalize(0.6), +    paddingHorizontal: '3.8%', +  }, +  unfriendButton: { +    alignSelf: 'center', +    justifyContent: 'center', +    alignItems: 'center', +    width: 82, +    height: '55%', +    borderColor: TAGG_LIGHT_BLUE, +    borderWidth: 2, +    borderRadius: 2, +    padding: 0,    }, -  buttonTitle: { +  unfriendButtonTitle: {      color: TAGG_LIGHT_BLUE,      padding: 0,      fontSize: normalize(11), diff --git a/src/components/search/SearchBar.tsx b/src/components/search/SearchBar.tsx index 1d0021ad..62bda77e 100644 --- a/src/components/search/SearchBar.tsx +++ b/src/components/search/SearchBar.tsx @@ -70,8 +70,6 @@ const SearchBar: React.FC<SearchBarProps> = ({      // TODO: FIGURE OUT WHY CHANGES IN placeholderId ARE NOT REFLECTED HERE      // my thought: the value is set when the function is defined, so it keeps      // its inital value of -1 forever. -    // console.log(`Previous ID: ${placeholderId}`); -    // console.log(`Next ID: ${nextId}`);      setPlaceholderId(nextId);    }; diff --git a/src/constants/api.ts b/src/constants/api.ts index d2d43063..6afdf384 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -34,6 +34,10 @@ export const DISCOVER_ENDPOINT: string = API_URL + 'discover/';  export const SEARCH_BUTTONS_ENDPOPINT: string = DISCOVER_ENDPOINT + 'search_buttons/';  export const WAITLIST_USER_ENDPOINT: string = API_URL + 'waitlist-user/';  export const COMMENT_THREAD_ENDPOINT: string = API_URL + 'reply/'; +export const USERS_FROM_CONTACTS_ENDPOINT: string = +  API_URL + 'user_contacts/find_friends/'; +export const INVITE_FRIEND_ENDPOINT: string = +API_URL + 'user_contacts/invite_friend/';  // Suggested People  export const SP_USERS_ENDPOINT: string = API_URL + 'suggested_people/'; diff --git a/src/constants/strings.ts b/src/constants/strings.ts index f289cfc1..cb442b7b 100644 --- a/src/constants/strings.ts +++ b/src/constants/strings.ts @@ -55,7 +55,7 @@ export const MOMENT_DELETED_MSG = 'Moment deleted....Some moments have to go, to  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_INVITATION_CODE = 'Perfect! You entered a valid invitation code, you are now able to login and explore Tagg!'; +export const SUCCESS_INVITATION_CODE = 'Welcome to Tagg!';  export const SUCCESS_LINK = (str: string) => `Successfully linked ${str} 🎉`;  export const SUCCESS_PIC_UPLOAD = 'Beautiful, the picture was uploaded successfully!';  export const SUCCESS_BADGES_UPDATE = 'Badges updated successfully!' diff --git a/src/routes/main/MainStackNavigator.tsx b/src/routes/main/MainStackNavigator.tsx index 142249ce..26d9943b 100644 --- a/src/routes/main/MainStackNavigator.tsx +++ b/src/routes/main/MainStackNavigator.tsx @@ -84,6 +84,10 @@ export type MainStackParams = {      badge_title: string;      badge_img: string;    }; +  InviteFriendsScreen: { +    screenType: ScreenType; +  }; +  SPWelcomeScreen: {};  };  export const MainStack = createStackNavigator<MainStackParams>(); diff --git a/src/routes/main/MainStackScreen.tsx b/src/routes/main/MainStackScreen.tsx index 35c306e5..8cefd3cc 100644 --- a/src/routes/main/MainStackScreen.tsx +++ b/src/routes/main/MainStackScreen.tsx @@ -15,6 +15,7 @@ import {    EditProfile,    FriendsListScreen,    IndividualMoment, +  InviteFriendsScreen,    MomentCommentsScreen,    MomentUploadPromptScreen,    NotificationsScreen, @@ -24,6 +25,7 @@ import {    SocialMediaTaggs,    SuggestedPeopleScreen,    SuggestedPeopleUploadPictureScreen, +  SuggestedPeopleWelcomeScreen,  } from '../../screens';  import MutualBadgeHolders from '../../screens/suggestedPeople/MutualBadgeHolders';  import {ScreenType} from '../../types'; @@ -221,6 +223,19 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => {          }}        />        <MainStack.Screen +        name="InviteFriendsScreen" +        component={InviteFriendsScreen} +        initialParams={{screenType}} +        options={{ +          ...headerBarOptions('black', 'Invites'), +        }} +      /> +      <MainStack.Screen +        name="RequestContactsAccess" +        component={RequestContactsAccess} +        initialParams={{screenType}} +      /> +      <MainStack.Screen          name="EditProfile"          component={EditProfile}          options={{ @@ -248,6 +263,13 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => {          component={MutualBadgeHolders}          options={{...modalStyle}}        /> +      <MainStack.Screen +        name="SPWelcomeScreen" +        component={SuggestedPeopleWelcomeScreen} +        options={{ +          ...headerBarOptions('white', ''), +        }} +      />      </MainStack.Navigator>    );  }; diff --git a/src/routes/onboarding/OnboardingStackNavigator.tsx b/src/routes/onboarding/OnboardingStackNavigator.tsx index 0cdeecdf..a51a6c86 100644 --- a/src/routes/onboarding/OnboardingStackNavigator.tsx +++ b/src/routes/onboarding/OnboardingStackNavigator.tsx @@ -35,7 +35,7 @@ export type OnboardingStackParams = {    PhoneVerification: {firstName: string; lastName: string; phone: string};    OnboardingStepTwo: {firstName: string; lastName: string; phone: string};    OnboardingStepThree: {userId: string; username: string}; -  InvitationCodeVerification: {userId: string}; +  InvitationCodeVerification: {userId: string; username: string};  };  export const OnboardingStack = createStackNavigator<OnboardingStackParams>(); diff --git a/src/routes/suggestedPeopleOnboarding/SuggestedPeopleOnboardingStackNavigator.tsx b/src/routes/suggestedPeopleOnboarding/SuggestedPeopleOnboardingStackNavigator.tsx deleted file mode 100644 index 30a83200..00000000 --- a/src/routes/suggestedPeopleOnboarding/SuggestedPeopleOnboardingStackNavigator.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import {createStackNavigator} from '@react-navigation/stack'; - -export type SuggestedPeopleOnboardingStackParams = { -  WelcomeScreen: undefined; -  UploadPicture: { -    editing: boolean; -  }; -  BadgeSelection: { -    editing: boolean; -  }; -}; - -export const SuggestedPeopleOnboardingStack = createStackNavigator<SuggestedPeopleOnboardingStackParams>(); diff --git a/src/routes/suggestedPeopleOnboarding/SuggestedPeopleOnboardingStackScreen.tsx b/src/routes/suggestedPeopleOnboarding/SuggestedPeopleOnboardingStackScreen.tsx deleted file mode 100644 index a02e8373..00000000 --- a/src/routes/suggestedPeopleOnboarding/SuggestedPeopleOnboardingStackScreen.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import {SuggestedPeopleOnboardingStack} from './SuggestedPeopleOnboardingStackNavigator'; -import { -  SuggestedPeopleWelcomeScreen, -  SuggestedPeopleUploadPictureScreen, -  BadgeSelection, -} from '../../screens'; -import {SCREEN_WIDTH} from '../../utils'; -import {headerBarOptions} from '../main'; - -const SuggestedPeopleOnboardingStackScreen: React.FC = () => { -  return ( -    <SuggestedPeopleOnboardingStack.Navigator -      initialRouteName="WelcomeScreen" -      screenOptions={{ -        headerShown: false, -        gestureResponseDistance: {horizontal: SCREEN_WIDTH * 0.6}, -      }}> -      <SuggestedPeopleOnboardingStack.Screen -        name="WelcomeScreen" -        component={SuggestedPeopleWelcomeScreen} -        options={{ -          ...headerBarOptions('white', ''), -        }} -      /> -      <SuggestedPeopleOnboardingStack.Screen -        name="UploadPicture" -        component={SuggestedPeopleUploadPictureScreen} -        initialParams={{editing: false}} -        options={{ -          ...headerBarOptions('white', ''), -        }} -      /> -      <SuggestedPeopleOnboardingStack.Screen -        name="BadgeSelection" -        component={BadgeSelection} -        initialParams={{editing: false}} -        options={{ -          ...headerBarOptions('white', ''), -        }} -      /> -    </SuggestedPeopleOnboardingStack.Navigator> -  ); -}; - -export default SuggestedPeopleOnboardingStackScreen; diff --git a/src/routes/suggestedPeopleOnboarding/index.ts b/src/routes/suggestedPeopleOnboarding/index.ts deleted file mode 100644 index df711493..00000000 --- a/src/routes/suggestedPeopleOnboarding/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './SuggestedPeopleOnboardingStackNavigator'; -export * from './SuggestedPeopleOnboardingStackScreen'; diff --git a/src/screens/badge/BadgeSelection.tsx b/src/screens/badge/BadgeSelection.tsx index cbd7dd88..335d4333 100644 --- a/src/screens/badge/BadgeSelection.tsx +++ b/src/screens/badge/BadgeSelection.tsx @@ -74,9 +74,11 @@ const BadgeSelection: React.FC<BadgeSelectionProps> = ({route}) => {                const success = await addBadgesService(selectedBadges);                if (success) {                  dispatch(suggestedPeopleBadgesFinished()); +                navigation.navigate('SuggestedPeople');                }              } else {                dispatch(suggestedPeopleBadgesFinished()); +              navigation.navigate('SuggestedPeople');              }            }          }}> diff --git a/src/screens/main/NotificationsScreen.tsx b/src/screens/main/NotificationsScreen.tsx index 501c44fc..68437f2b 100644 --- a/src/screens/main/NotificationsScreen.tsx +++ b/src/screens/main/NotificationsScreen.tsx @@ -21,7 +21,10 @@ import {TouchableOpacity} from 'react-native-gesture-handler';  import {SafeAreaView} from 'react-native-safe-area-context';  import {useDispatch, useSelector} from 'react-redux';  import {TabsGradient, TaggPrompt} from '../../components'; -import {Notification} from '../../components/notifications'; +import { +  InviteFriendsPrompt, +  Notification, +} from '../../components/notifications';  import {    loadUserNotifications,    updateNewNotificationReceived, @@ -252,30 +255,6 @@ const NotificationsScreen: React.FC = () => {      return null;    }; -  const SPPromptNotification: ReactElement = showSPNotifyPopUp ? ( -    <TaggPrompt -      messageHeader={'New Suggested People Page!'} -      messageBody={ -        <> -          <Text> -            A new page where you can discover new profiles. Just press the new{' '} -          </Text> -          <Image -            style={styles.icon} -            source={require('../../assets/navigationIcons/home.png')} -          /> -          <Text> button on the tab bar to check it out!</Text> -        </> -      } -      logoType={'tagg'} -      hideCloseButton={true} -      noPadding={true} -      onClose={() => {}} -    /> -  ) : ( -    <Fragment /> -  ); -    return (      <View style={styles.background}>        <SafeAreaView> @@ -285,12 +264,13 @@ const NotificationsScreen: React.FC = () => {          </View>          <SectionList            contentContainerStyle={styles.container} +          stickySectionHeadersEnabled={false}            sections={sectionedNotifications}            keyExtractor={(_item, index) => index.toString()}            renderItem={renderNotification}            renderSectionHeader={renderSectionHeader}            renderSectionFooter={renderSectionFooter} -          ListHeaderComponent={SPPromptNotification} +          ListHeaderComponent={<InviteFriendsPrompt />}            refreshControl={              <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />            } @@ -358,11 +338,6 @@ const styles = StyleSheet.create({      flex: 1,      justifyContent: 'center',    }, -  icon: { -    width: 20, -    height: 20, -    tintColor: 'grey', -  },  });  export default NotificationsScreen; diff --git a/src/screens/onboarding/InvitationCodeVerification.tsx b/src/screens/onboarding/InvitationCodeVerification.tsx index 41d17f29..7cd4b3bf 100644 --- a/src/screens/onboarding/InvitationCodeVerification.tsx +++ b/src/screens/onboarding/InvitationCodeVerification.tsx @@ -1,3 +1,4 @@ +import AsyncStorage from '@react-native-community/async-storage';  import {RouteProp} from '@react-navigation/native';  import {StackNavigationProp} from '@react-navigation/stack';  import React from 'react'; @@ -9,6 +10,7 @@ import {    useBlurOnFulfill,    useClearByFocusCell,  } from 'react-native-confirmation-code-field'; +import {useDispatch} from 'react-redux';  import {    ArrowButton,    Background, @@ -25,7 +27,7 @@ import {  } from '../../constants/strings';  import {OnboardingStackParams} from '../../routes';  import {BackgroundGradientType} from '../../types'; -import {SCREEN_WIDTH} from '../../utils'; +import {SCREEN_WIDTH, userLogin} from '../../utils';  type InvitationCodeVerificationRouteProp = RouteProp<    OnboardingStackParams, @@ -56,6 +58,7 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({      value,      setValue,    }); +  const dispatch = useDispatch();    const handleInvitationCodeVerification = async () => {      if (value.length === 6) { @@ -71,10 +74,11 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({          );          if (verifyInviteCodeResponse.status === 200) { -          navigation.navigate('Login'); -          setTimeout(() => { -            Alert.alert(SUCCESS_INVITATION_CODE); -          }, 500); +          const userId = route.params.userId; +          const username = route.params.username; +          await AsyncStorage.setItem('userId', userId); +          await AsyncStorage.setItem('username', username); +          userLogin(dispatch, {userId, username});          } else {            Alert.alert(ERROR_INVALID_INVITATION_CODE);          } diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx index cfa39dbd..6d9d3a97 100644 --- a/src/screens/onboarding/Login.tsx +++ b/src/screens/onboarding/Login.tsx @@ -170,6 +170,7 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => {          } else if (statusCode === 200 && !data.isOnboarded) {            navigation.navigate('InvitationCodeVerification', {              userId: data.UserID, +            username: username,            });            setTimeout(() => {              Alert.alert(ERROR_NOT_ONBOARDED); diff --git a/src/screens/onboarding/OnboardingStepThree.tsx b/src/screens/onboarding/OnboardingStepThree.tsx index 64a2a2f7..f22d720f 100644 --- a/src/screens/onboarding/OnboardingStepThree.tsx +++ b/src/screens/onboarding/OnboardingStepThree.tsx @@ -57,7 +57,7 @@ const OnboardingStepThree: React.FC<OnboardingStepThreeProps> = ({    route,    navigation,  }) => { -  const {userId} = route.params; +  const {userId, username} = route.params;    let emptyDate: string | undefined;    const [form, setForm] = React.useState({      smallPic: '', @@ -224,12 +224,12 @@ const OnboardingStepThree: React.FC<OnboardingStepThreeProps> = ({          },          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, +          username: username,          });        } else if (statusCode === 400) {          Alert.alert( diff --git a/src/screens/onboarding/OnboardingStepTwo.tsx b/src/screens/onboarding/OnboardingStepTwo.tsx index 93342c3f..e79e1886 100644 --- a/src/screens/onboarding/OnboardingStepTwo.tsx +++ b/src/screens/onboarding/OnboardingStepTwo.tsx @@ -245,23 +245,6 @@ const OnboardingStepTwo: React.FC<OnboardingStepTwoProps> = ({            <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" @@ -281,6 +264,23 @@ const OnboardingStepTwo: React.FC<OnboardingStepTwoProps> = ({            width={280}          />          <TaggInput +          accessibilityHint="Enter your email." +          accessibilityLabel="Email input field." +          placeholder="School 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 password."            accessibilityLabel="Password input field."            placeholder="Password" diff --git a/src/screens/onboarding/RegistrationTwo.tsx b/src/screens/onboarding/RegistrationTwo.tsx index 707e621a..6d7b2226 100644 --- a/src/screens/onboarding/RegistrationTwo.tsx +++ b/src/screens/onboarding/RegistrationTwo.tsx @@ -214,7 +214,7 @@ const RegistrationTwo: React.FC<RegistrationTwoProps> = ({          <TaggInput            accessibilityHint="Enter your email."            accessibilityLabel="Email input field." -          placeholder="Email" +          placeholder="School Email"            autoCompleteType="email"            textContentType="emailAddress"            autoCapitalize="none" diff --git a/src/screens/profile/CaptionScreen.tsx b/src/screens/profile/CaptionScreen.tsx index 01e859ba..998897e2 100644 --- a/src/screens/profile/CaptionScreen.tsx +++ b/src/screens/profile/CaptionScreen.tsx @@ -152,10 +152,8 @@ const styles = StyleSheet.create({      position: 'relative',      backgroundColor: 'white',      width: '100%', -    paddingLeft: '2%', -    paddingRight: '2%', -    paddingBottom: '1%', -    paddingTop: '1%', +    paddingHorizontal: '2%', +    paddingVertical: '1%',      height: 60,    },  }); diff --git a/src/screens/profile/FriendsListScreen.tsx b/src/screens/profile/FriendsListScreen.tsx index 1cfef058..886ab9c4 100644 --- a/src/screens/profile/FriendsListScreen.tsx +++ b/src/screens/profile/FriendsListScreen.tsx @@ -1,6 +1,12 @@  import {RouteProp} from '@react-navigation/native';  import React from 'react'; -import {SafeAreaView, StyleSheet, View} from 'react-native'; +import { +  SafeAreaView, +  ScrollView, +  StatusBar, +  StyleSheet, +  View, +} from 'react-native';  import {useSelector} from 'react-redux';  import {Friends, TabsGradient} from '../../components';  import {MainStackParams} from '../../routes'; @@ -25,9 +31,10 @@ const FriendsListScreen: React.FC<FriendsListScreenProps> = ({route}) => {    return (      <>        <SafeAreaView> -        <View style={styles.body}> +        <StatusBar barStyle="dark-content" /> +        <ScrollView style={styles.body}>            <Friends result={friends} screenType={screenType} userId={userXId} /> -        </View> +        </ScrollView>        </SafeAreaView>        <TabsGradient />      </> @@ -45,7 +52,7 @@ const styles = StyleSheet.create({    body: {      marginTop: HeaderHeight,      width: SCREEN_WIDTH, -    height: SCREEN_HEIGHT * 0.8, +    height: SCREEN_HEIGHT - HeaderHeight,    },  }); diff --git a/src/screens/profile/InviteFriendsScreen.tsx b/src/screens/profile/InviteFriendsScreen.tsx new file mode 100644 index 00000000..4af52349 --- /dev/null +++ b/src/screens/profile/InviteFriendsScreen.tsx @@ -0,0 +1,337 @@ +import React, {useEffect, useState} from 'react'; +import { +  View, +  Text, +  TouchableOpacity, +  SafeAreaView, +  StyleSheet, +  TextInput, +  FlatList, +  Keyboard, +  Linking, +  StatusBar, +  TouchableWithoutFeedback, +  ScrollView, +} from 'react-native'; +import {useDispatch, useStore} from 'react-redux'; +import {ProfilePreviewType} from '../../types'; +import { +  extractContacts, +  handleAddFriend, +  HeaderHeight, +  isIPhoneX, +  normalize, +  SCREEN_HEIGHT, +  SCREEN_WIDTH, +  StatusBarHeight, +} from '../../utils'; +import {checkPermission} from 'react-native-contacts'; +import {usersFromContactsService} from '../../services/UserFriendsService'; +import {ProfilePreview, TabsGradient} from '../../components'; +import Animated from 'react-native-reanimated'; +import Icon from 'react-native-vector-icons/Feather'; +import {InviteFriendTile} from '../../components/friends'; +import {TAGG_LIGHT_BLUE} from '../../constants'; +import {MainStackParams} from '../../routes'; +import {RouteProp} from '@react-navigation/native'; +const AnimatedIcon = Animated.createAnimatedComponent(Icon); + +type InviteFriendsScreenRouteProp = RouteProp< +  MainStackParams, +  'InviteFriendsScreen' +>; + +interface InviteFriendsScreenProps { +  route: InviteFriendsScreenRouteProp; +} + +const InviteFriendsScreen: React.FC<InviteFriendsScreenProps> = ({route}) => { +  const {screenType} = route.params; +  const dispatch = useDispatch(); +  const state = useStore().getState(); +  const [usersFromContacts, setUsersFromContacts] = useState< +    ProfilePreviewType[] +  >([]); +  const [nonUsersFromContacts, setNonUsersFromContacts] = useState<[]>([]); +  type SearchResultType = { +    usersFromContacts: ProfilePreviewType[]; +    nonUsersFromContacts: []; +  }; +  const [results, setResults] = useState<SearchResultType>({ +    usersFromContacts: usersFromContacts, +    nonUsersFromContacts: nonUsersFromContacts, +  }); +  const [query, setQuery] = useState(''); + +  useEffect(() => { +    const handleFindFriends = () => { +      extractContacts().then(async (retrievedContacts) => { +        const permission = await checkPermission(); +        if (permission === 'authorized') { +          let response = await usersFromContactsService(retrievedContacts); +          await setUsersFromContacts(response.existing_tagg_users); +          await setNonUsersFromContacts(response.invite_from_contacts); +          setResults({ +            usersFromContacts: response.existing_tagg_users, +            nonUsersFromContacts: response.invite_from_contacts, +          }); +        } else { +          Linking.openSettings(); +        } +      }); +    }; +    handleFindFriends(); +  }, []); + +  /* +   * Main handler for changes in query. +   */ +  useEffect(() => { +    const search = async () => { +      if (query.length > 0) { +        const searchResultsUsers = usersFromContacts.filter( +          (item: ProfilePreviewType) => +            (item.first_name + ' ' + item.last_name) +              .toLowerCase() +              .startsWith(query) || +            item.username.toLowerCase().startsWith(query) || +            item.last_name.toLowerCase().startsWith(query), +        ); +        const searchResultsNonUsers = nonUsersFromContacts.filter( +          (item) => +            (item.firstName + ' ' + item.lastName) +              .toLowerCase() +              .startsWith(query) || +            item.lastName.toLowerCase().startsWith(query), +        ); +        const sanitizedResult = { +          usersFromContacts: searchResultsUsers, +          nonUsersFromContacts: searchResultsNonUsers, +        }; +        setResults(sanitizedResult); +      } else { +        setResults({ +          usersFromContacts: usersFromContacts, +          nonUsersFromContacts: nonUsersFromContacts, +        }); +      } +    }; +    search(); +  }, [query]); + +  const UsersFromContacts = () => ( +    <> +      <FlatList +        showsVerticalScrollIndicator={false} +        scrollEnabled={false} +        data={results.usersFromContacts} +        keyExtractor={(item) => item.username} +        renderItem={({item}) => ( +          <View key={item.id} style={styles.ppContainer}> +            <View style={styles.friend}> +              <ProfilePreview +                {...{profilePreview: item}} +                previewType={'Friend'} +                screenType={screenType} +              /> +            </View> +            <TouchableOpacity +              style={styles.addFriendButton} +              onPress={() => { +                handleAddFriend(screenType, item, dispatch, state).then( +                  (success) => { +                    if (success) { +                      let users = usersFromContacts; +                      const filteredUsers = users.filter( +                        (user) => user.username !== item.username, +                      ); +                      setResults({ +                        ...results, +                        usersFromContacts: filteredUsers, +                      }); +                    } +                  }, +                ); +              }}> +              <Text style={styles.addFriendButtonTitle}>Add Friend</Text> +            </TouchableOpacity> +          </View> +        )} +      /> +    </> +  ); + +  return ( +    <View style={styles.mainContainer}> +      <TouchableWithoutFeedback onPress={Keyboard.dismiss}> +        <SafeAreaView style={{marginTop: HeaderHeight + StatusBarHeight}}> +          <StatusBar barStyle="dark-content" /> +          <ScrollView +            style={styles.body} +            contentContainerStyle={{paddingBottom: SCREEN_HEIGHT * 0.1}}> +            <View style={styles.headerContainer}> +              <Text style={styles.headerText}> +                Sharing is caring, invite friends, and create moments together! +              </Text> +            </View> +            <View style={styles.container}> +              <Animated.View style={styles.inputContainer}> +                <AnimatedIcon +                  name="search" +                  color={'#7E7E7E'} +                  size={16} +                  style={styles.searchIcon} +                /> +                <TextInput +                  style={[styles.input]} +                  placeholderTextColor={'#828282'} +                  clearButtonMode="while-editing" +                  autoCapitalize="none" +                  autoCorrect={false} +                  onChangeText={(text) => { +                    setQuery(text.toLowerCase()); +                  }} +                  onBlur={() => { +                    Keyboard.dismiss(); +                  }} +                  onEndEditing={() => { +                    Keyboard.dismiss(); +                  }} +                  value={query} +                  placeholder={'Search'} +                /> +              </Animated.View> +            </View> +            <View style={styles.subheader}> +              <Text style={styles.subheaderText}>Add Friends</Text> +              <UsersFromContacts /> +            </View> +            <View style={styles.subheader}> +              <Text style={styles.subheaderText}>Invite your friends!</Text> +              <FlatList +                contentContainerStyle={styles.nonUsersFlatListContainer} +                showsVerticalScrollIndicator={false} +                scrollEnabled={false} +                data={results.nonUsersFromContacts} +                keyExtractor={(item) => item.phoneNumber} +                renderItem={({item}) => <InviteFriendTile item={item} />} +              /> +            </View> +          </ScrollView> +        </SafeAreaView> +      </TouchableWithoutFeedback> +      <TabsGradient /> +    </View> +  ); +}; + +const styles = StyleSheet.create({ +  mainContainer: {backgroundColor: 'white', height: SCREEN_HEIGHT}, +  body: { +    paddingTop: 10, +    height: SCREEN_HEIGHT, +    backgroundColor: '#fff', +  }, +  headerContainer: { +    width: SCREEN_WIDTH * 0.85, +    height: isIPhoneX() ? SCREEN_HEIGHT * 0.06 : SCREEN_HEIGHT * 0.08, +    alignSelf: 'center', +  }, +  nonUsersFlatListContainer: {paddingBottom: 130}, +  subheader: { +    alignSelf: 'center', +    width: SCREEN_WIDTH * 0.85, +    marginBottom: '5%', +  }, +  subheaderText: { +    color: '#828282', +    fontSize: normalize(12), +    fontWeight: '600', +    lineHeight: normalize(14.32), +    marginBottom: '5%', +  }, +  headerText: { +    textAlign: 'center', +    color: '#828282', +    fontSize: normalize(12), +    fontWeight: '600', +    lineHeight: normalize(14.32), +    marginBottom: '5%', +  }, +  container: { +    alignSelf: 'center', +    flexDirection: 'row', +    justifyContent: 'space-between', +    width: '100%', +    height: normalize(42), +    alignItems: 'center', +    marginBottom: '3%', +    marginHorizontal: 10, +  }, +  ppContainer: { +    alignSelf: 'center', +    flexDirection: 'row', +    justifyContent: 'space-between', +    width: '100%', +    height: normalize(42), +    alignItems: 'center', +    marginBottom: '5%', +    marginHorizontal: 10, +  }, +  inputContainer: { +    flexGrow: 1, +    flexDirection: 'row', +    alignItems: 'center', +    paddingHorizontal: 8, +    marginHorizontal: '3%', +    borderRadius: 20, +    backgroundColor: '#F0F0F0', +    height: 34, +  }, +  searchIcon: { +    marginRight: '5%', +  }, +  input: { +    flex: 1, +    fontSize: normalize(16), +    color: '#000', +    letterSpacing: normalize(0.5), +  }, +  cancelButton: { +    height: '100%', +    position: 'absolute', +    justifyContent: 'center', +    paddingHorizontal: 8, +  }, +  cancelText: { +    color: '#818181', +    fontWeight: '500', +  }, +  friend: { +    alignSelf: 'center', +    height: '100%', +  }, +  addFriendButton: { +    alignSelf: 'center', +    justifyContent: 'center', +    alignItems: 'center', +    width: 82, +    height: 25, +    borderColor: TAGG_LIGHT_BLUE, +    borderWidth: 2, +    borderRadius: 2, +    padding: 0, +    backgroundColor: TAGG_LIGHT_BLUE, +  }, +  addFriendButtonTitle: { +    color: 'white', +    padding: 0, +    fontSize: normalize(11), +    fontWeight: '700', +    lineHeight: normalize(13.13), +    letterSpacing: normalize(0.6), +    paddingHorizontal: '3.8%', +  }, +}); + +export default InviteFriendsScreen; diff --git a/src/screens/profile/ProfileScreen.tsx b/src/screens/profile/ProfileScreen.tsx index 5edc6277..313e2f2c 100644 --- a/src/screens/profile/ProfileScreen.tsx +++ b/src/screens/profile/ProfileScreen.tsx @@ -38,11 +38,11 @@ const ProfileScreen: React.FC<ProfileOnboardingProps> = ({route}) => {     * This is done to reset the users stored in our store for the Search screen.     * Read more about useFocusEffect here : https://reactnavigation.org/docs/function-after-focusing-screen/     */ -  useFocusEffect(() => { -    if (!userXId) { -      dispatch(resetScreenType(screenType)); -    } -  }); +  // useFocusEffect(() => { +  //   if (!userXId) { +  //     dispatch(resetScreenType(screenType)); +  //   } +  // });    return (      <> diff --git a/src/screens/profile/index.ts b/src/screens/profile/index.ts index 9d651729..f74946a6 100644 --- a/src/screens/profile/index.ts +++ b/src/screens/profile/index.ts @@ -6,3 +6,4 @@ export {default as MomentCommentsScreen} from './MomentCommentsScreen';  export {default as FriendsListScreen} from './FriendsListScreen';  export {default as EditProfile} from './EditProfile';  export {default as MomentUploadPromptScreen} from './MomentUploadPromptScreen'; +export {default as InviteFriendsScreen} from './InviteFriendsScreen'; diff --git a/src/screens/suggestedPeople/SPBody.tsx b/src/screens/suggestedPeople/SPBody.tsx index 06d3efb3..8e0801c2 100644 --- a/src/screens/suggestedPeople/SPBody.tsx +++ b/src/screens/suggestedPeople/SPBody.tsx @@ -121,7 +121,7 @@ const SPBody: React.FC<SPBodyProps> = ({        <TouchableOpacity          onPress={() => {            navigation.push('Profile', { -            userXId: user.id, +            userXId: loggedInUserId === user.id ? undefined : user.id,              screenType,            });          }} diff --git a/src/screens/suggestedPeople/SuggestedPeopleScreen.tsx b/src/screens/suggestedPeople/SuggestedPeopleScreen.tsx index 4094b0a3..76889657 100644 --- a/src/screens/suggestedPeople/SuggestedPeopleScreen.tsx +++ b/src/screens/suggestedPeople/SuggestedPeopleScreen.tsx @@ -1,37 +1,31 @@  import {useFocusEffect, useNavigation} from '@react-navigation/native'; -import React, {useCallback, useEffect, useState, useRef} from 'react'; -import { -  FlatList, -  RefreshControl, -  StatusBar, -  StyleSheet, -  ViewToken, -} from 'react-native'; - +import React, {useCallback, useEffect, useRef, useState} from 'react'; +import {FlatList, RefreshControl, StatusBar, ViewToken} from 'react-native';  import {useDispatch, useSelector, useStore} from 'react-redux'; -import {TabsGradient, TaggLoadingIndicator, Background} from '../../components'; +import {Background, TabsGradient, TaggLoadingIndicator} from '../../components';  import {SP_PAGE_SIZE} from '../../constants'; +import {MainStack} from '../../routes'; +import MainStackScreen from '../../routes/main/MainStackScreen';  import SuggestedPeopleOnboardingStackScreen from '../../routes/suggestedPeopleOnboarding/SuggestedPeopleOnboardingStackScreen';  import {getSuggestedPeople} from '../../services/SuggestedPeopleService';  import {cancelFriendRequest, resetScreenType} from '../../store/actions';  import {RootState} from '../../store/rootReducer';  import { +  BackgroundGradientType,    FriendshipStatusType,    ProfilePreviewType,    ScreenType,    SuggestedPeopleDataType, -  BackgroundGradientType,  } from '../../types';  import {    fetchUserX,    getUserAsProfilePreviewType,    handleAddFriend, -  normalize, -  SCREEN_HEIGHT, -  SCREEN_WIDTH,  } from '../../utils'; +import {SuggestedPeopleWelcomeScreen} from '../suggestedPeopleOnboarding';  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] @@ -209,9 +203,13 @@ const SuggestedPeopleScreen: React.FC = () => {      [],    ); -  return suggested_people_linked === 0 ? ( -    <SuggestedPeopleOnboardingStackScreen /> -  ) : loading ? ( +  useFocusEffect(() => { +    if (suggested_people_linked === 0) { +      navigation.navigate('SPWelcomeScreen'); +    } +  }); + +  return loading ? (      <>        <TaggLoadingIndicator fullscreen />        <Background gradientType={BackgroundGradientType.Dark} /> @@ -245,142 +243,4 @@ const SuggestedPeopleScreen: React.FC = () => {    );  }; -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 SuggestedPeopleScreen; diff --git a/src/screens/suggestedPeopleOnboarding/SuggestedPeopleUploadPictureScreen.tsx b/src/screens/suggestedPeopleOnboarding/SuggestedPeopleUploadPictureScreen.tsx index 87e22d9e..0a4e5718 100644 --- a/src/screens/suggestedPeopleOnboarding/SuggestedPeopleUploadPictureScreen.tsx +++ b/src/screens/suggestedPeopleOnboarding/SuggestedPeopleUploadPictureScreen.tsx @@ -98,7 +98,7 @@ const SuggestedPeopleUploadPictureScreen: React.FC<SuggestedPeopleUploadPictureS        if (success) {          dispatch(uploadedSuggestedPeoplePhoto(image));          if (!editing) { -          navigation.push('BadgeSelection', {editing: false}); +          navigation.navigate('BadgeSelection', {editing: false});          }        } else {          Alert.alert(ERROR_UPLOAD); @@ -106,12 +106,12 @@ const SuggestedPeopleUploadPictureScreen: React.FC<SuggestedPeopleUploadPictureS        setLoading(false);        // Navigated back to Profile if user is editing their Suggested People Picture        if (editing) { +        navigation.goBack();          setTimeout(() => {            Alert.alert(success ? SUCCESS_PIC_UPLOAD : ERROR_UPLOAD_SP_PHOTO);          }, 500);        }      } -    navigation.goBack();    };    return ( @@ -161,7 +161,7 @@ const SuggestedPeopleUploadPictureScreen: React.FC<SuggestedPeopleUploadPictureS          {editing && (            <TouchableOpacity              onPress={() => { -              navigation.push('BadgeSelection', {editing: true}); +              navigation.navigate('BadgeSelection', {editing: true});              }}>              <View style={styles.editBadgesMainContainer}>                <View style={styles.editBadgesSubContainer}> diff --git a/src/screens/suggestedPeopleOnboarding/SuggestedPeopleWelcomeScreen.tsx b/src/screens/suggestedPeopleOnboarding/SuggestedPeopleWelcomeScreen.tsx index 10f3b3a5..2f12d909 100644 --- a/src/screens/suggestedPeopleOnboarding/SuggestedPeopleWelcomeScreen.tsx +++ b/src/screens/suggestedPeopleOnboarding/SuggestedPeopleWelcomeScreen.tsx @@ -1,6 +1,6 @@  import {BlurView} from '@react-native-community/blur';  import {useNavigation} from '@react-navigation/native'; -import React from 'react'; +import React, {Fragment, useEffect} from 'react';  import {Image, StatusBar, StyleSheet, TouchableOpacity} from 'react-native';  import {Text} from 'react-native-animatable';  import {SafeAreaView} from 'react-native-safe-area-context'; @@ -9,6 +9,13 @@ import {isIPhoneX, normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils';  const SuggestedPeopleWelcomeScreen: React.FC = () => {    const navigation = useNavigation(); + +  useEffect(() => { +    navigation.setOptions({ +      headerBackImage: () => <Fragment />, +    }); +  }, []); +    return (      <>        <StatusBar barStyle={'light-content'} /> @@ -35,7 +42,9 @@ const SuggestedPeopleWelcomeScreen: React.FC = () => {        </BlurView>        <TouchableOpacity          style={styles.nextButton} -        onPress={() => navigation.push('UploadPicture')}> +        onPress={() => +          navigation.navigate('UpdateSPPicture', {editing: false}) +        }>          <UpArrow color={'white'} />        </TouchableOpacity>      </> diff --git a/src/services/UserFriendsService.ts b/src/services/UserFriendsService.ts index 5ce9df29..da39380f 100644 --- a/src/services/UserFriendsService.ts +++ b/src/services/UserFriendsService.ts @@ -1,9 +1,17 @@  //Abstracted common friends api calls out here +import AsyncStorage from '@react-native-community/async-storage';  import {Alert} from 'react-native'; -import {FriendshipStatusType} from '../types'; -import {FRIENDS_ENDPOINT} from '../constants'; -import {ERROR_SOMETHING_WENT_WRONG_REFRESH} from '../constants/strings'; +import {ContactType, FriendshipStatusType} from '../types'; +import { +  FRIENDS_ENDPOINT, +  INVITE_FRIEND_ENDPOINT, +  USERS_FROM_CONTACTS_ENDPOINT, +} from '../constants'; +import { +  ERROR_SOMETHING_WENT_WRONG, +  ERROR_SOMETHING_WENT_WRONG_REFRESH, +} from '../constants/strings';  export const loadFriends = async (userId: string, token: string) => {    try { @@ -180,3 +188,57 @@ export const deleteFriendshipService = async (      return false;    }  }; + +export const usersFromContactsService = async ( +  contacts: Array<ContactType>, +) => { +  try { +    const token = await AsyncStorage.getItem('token'); +    const response = await fetch(USERS_FROM_CONTACTS_ENDPOINT, { +      method: 'POST', +      headers: { +        Authorization: 'Token ' + token, +      }, +      body: JSON.stringify({ +        contacts: contacts, +      }), +    }); +    const status = response.status; +    if (Math.floor(status / 100) === 2) { +      const users_data = await response.json(); +      return users_data; +    } else { +      console.log( +        'Something went wrong! ðŸ˜', +        'Not able to retrieve tagg users list', +      ); +    } +  } catch (error) { +    console.log(error); +    Alert.alert(ERROR_SOMETHING_WENT_WRONG_REFRESH); +    return false; +  } +}; + +export const inviteFriendService = async ( +  phoneNumber: string, +  inviteeFirstName: string, +  inviteeLastName: string, +) => { +  const token = await AsyncStorage.getItem('token'); +  const response = await fetch(INVITE_FRIEND_ENDPOINT, { +    method: 'POST', +    headers: { +      Authorization: 'Token ' + token, +    }, +    body: JSON.stringify({ +      invitee_phone_number: phoneNumber, +      invitee_first_name: inviteeFirstName, +      invitee_last_name: inviteeLastName, +    }), +  }); +  if (response.status === 201 || response.status === 200) { +    return await response.json(); +  } +  return false; +}; diff --git a/src/store/actions/userFriends.ts b/src/store/actions/userFriends.ts index 4f55acc8..9da3cb4a 100644 --- a/src/store/actions/userFriends.ts +++ b/src/store/actions/userFriends.ts @@ -1,4 +1,4 @@ -import {getTokenOrLogout} from '../../utils'; +import {getTokenOrLogout, userXInStore} from '../../utils';  import {RootState} from '../rootReducer';  import {    FriendshipStatusType, @@ -90,6 +90,7 @@ export const friendUnfriendUser = (  export const addFriend = (    friend: ProfilePreviewType, // userX's profile preview    screenType: ScreenType, //screentype from content +  state: RootState,  ): ThunkAction<    Promise<boolean | undefined>,    RootState, @@ -100,14 +101,16 @@ export const addFriend = (      const token = await getTokenOrLogout(dispatch);      const success = await addFriendService(friend.id, token);      if (success) { -      dispatch({ -        type: userXFriendshipEdited.type, -        payload: { -          userId: friend.id, -          screenType, -          data: 'requested', -        }, -      }); +      if (userXInStore(state, screenType, friend.id)) { +        dispatch({ +          type: userXFriendshipEdited.type, +          payload: { +            userId: friend.id, +            screen: screenType, +            data: 'requested', +          }, +        }); +      }        return true;      }    } catch (error) { diff --git a/src/types/types.ts b/src/types/types.ts index 8937e74c..692da8b4 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -264,3 +264,9 @@ export type SearchCategoryType = {    name: string;    category: string;  }; + +export type ContactType = { +  phone_number: string; +  first_name: string; +  last_name: string; +}; diff --git a/src/utils/common.ts b/src/utils/common.ts index c1049c42..0a76ec08 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -1,8 +1,9 @@ -import {NotificationType} from './../types/types'; +import {ContactType, NotificationType} from './../types/types';  import moment from 'moment';  import {Linking} from 'react-native';  import {BROWSABLE_SOCIAL_URLS, TOGGLE_BUTTON_TYPE} from '../constants';  import AsyncStorage from '@react-native-community/async-storage'; +import {getAll} from 'react-native-contacts';  export const getToggleButtonText: (    buttonType: string, @@ -115,3 +116,19 @@ export const shuffle = (array: any[]) => {    return array;  }; + +export const extractContacts = async () => { +  let retrievedContacts: Array<ContactType> = []; +  await getAll().then((contacts) => { +    contacts.map((contact) => { +      contact.phoneNumbers.map((phoneNumber) => { +        retrievedContacts.push({ +          first_name: contact.givenName, +          last_name: contact.familyName, +          phone_number: phoneNumber.number, +        }); +      }); +    }); +  }); +  return retrievedContacts; +}; diff --git a/src/utils/friends.ts b/src/utils/friends.ts index 6e3b645a..5b0ded8f 100644 --- a/src/utils/friends.ts +++ b/src/utils/friends.ts @@ -60,7 +60,7 @@ export const handleAddFriend = async (    dispatch: AppDispatch,    state: RootState,  ) => { -  const success = await dispatch(addFriend(friend, screenType)); +  const success = await dispatch(addFriend(friend, screenType, state));    await dispatch(updateUserXFriends(friend.id, state));    await dispatch(updateUserXProfileAllScreens(friend.id, state));    return success; diff --git a/src/utils/users.ts b/src/utils/users.ts index f54d461c..d5e44b36 100644 --- a/src/utils/users.ts +++ b/src/utils/users.ts @@ -96,7 +96,7 @@ export const userXInStore = (    userId: string,  ) => {    const userX = state.userX[screen]; -  return userId in userX && userX[userId].user.userId; +  return userX && userId in userX && userX[userId].user.userId;  };  /**  | 
