diff --git a/.gitignore b/.gitignore
index 849b401..7c77f69 100644
--- a/.gitignore
+++ b/.gitignore
@@ -62,3 +62,4 @@ storybook
test.js
ios/Tagfer.xcodeproj/project.pbxproj
package-lock.json
+src/screens/contacts/data.js
diff --git a/ios/Tagfer/Info.plist b/ios/Tagfer/Info.plist
index 4d9b63d..67049b8 100644
--- a/ios/Tagfer/Info.plist
+++ b/ios/Tagfer/Info.plist
@@ -21,7 +21,7 @@
CFBundleSignature
????
CFBundleVersion
- 2
+ 3
LSRequiresIPhoneOS
NSAppTransportSecurity
diff --git a/src/components/navigators/ContactsTabNavigator.js b/src/components/navigators/ContactsTabNavigator.js
index 304eee5..4b6d69c 100644
--- a/src/components/navigators/ContactsTabNavigator.js
+++ b/src/components/navigators/ContactsTabNavigator.js
@@ -8,6 +8,7 @@ const contactsTabBarOptions = {
upperCaseLabel: false,
activeTintColor: 'white',
inactiveTintColor: '#53ACF0',
+ lazy: true,
labelStyle: {
margin: 1,
fontSize: 13,
diff --git a/src/components/new/TagferAvatar.js b/src/components/new/TagferAvatar.js
index a11e3b9..b2c6e02 100644
--- a/src/components/new/TagferAvatar.js
+++ b/src/components/new/TagferAvatar.js
@@ -1,11 +1,9 @@
import React from 'react';
import { Avatar } from 'react-native-elements';
-import { isEmpty } from '../../utils/aux';
-
const TagferAvatar = ({ size, photoURL, style, color }) => {
- const uri = !isEmpty(photoURL) ? { uri: photoURL } : null;
- const icon = isEmpty(photoURL) ? { name: 'user', type: 'entypo' } : null;
+ const uri = photoURL ? { uri: photoURL } : null;
+ const icon = !photoURL ? { name: 'user', type: 'entypo' } : null;
return (
{
return (
}
+ avatar={}
title={fullName}
subtitle={subtitle}
subtitleNumberOfLines={3}
@@ -24,5 +24,4 @@ const UserListItem = ({ profile, selected, onPress }) => {
/>);
};
-export default UserListItem
-;
+export default UserListItem;
diff --git a/src/config/apiEndpoints.js b/src/config/apiEndpoints.js
index 66d0455..98a84e5 100644
--- a/src/config/apiEndpoints.js
+++ b/src/config/apiEndpoints.js
@@ -1,4 +1,4 @@
-const baseurl = 'https://us-central1-tagfer-inc.cloudfunctions.net/api';
+const baseurl = 'http://localhost:3000';
const endpoints = {
login: {
method: 'POST',
@@ -39,6 +39,22 @@ const endpoints = {
signup: {
method: 'PUT',
url: `${baseurl}/auth/signup`
+ },
+ getConnectionRequests: {
+ method: 'GET',
+ url: `${baseurl}/connections/me/requests`
+ },
+ sendConnectionRequest: (profileN) => ({
+ method: 'PUT',
+ url: `${baseurl}/connections/me/${profileN}`
+ }),
+ acceptConnectionRequest: (profileN) => ({
+ method: 'POST',
+ url: `${baseurl}/connections/me/${profileN}`
+ }),
+ removeConnectionRequest: {
+ method: 'DELETE',
+ url: `${baseurl}/connections/me`
}
};
diff --git a/src/config/router.js b/src/config/router.js
deleted file mode 100644
index eb191ea..0000000
--- a/src/config/router.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { createStackNavigator, createAppContainer } from 'react-navigation';
-import WelcomeScreen from '../screens/onboaring/WelcomeScreen';
-// AUTH IMPORTS
-import LoginScreen from '../screens/auth/LoginScreen';
-import SignupScreenOne from '../screens/auth/SignupScreen1';
-import SignupScreenTwo from '../screens/auth/SignupScreen2';
-import SignupScreenThreeA from '../screens/auth/SignupScreen3a';
-import SignupScreenThreeB from '../screens/auth/SignupScreen3b';
-import SignupScreenFour from '../screens/auth/SignupScreen4';
-import SignupScreenFive from '../screens/auth/SignupScreen5';
-import SignupScreenSix from '../screens/auth/SignupScreen6';
-import TwitterWebView from '../screens/auth/TwitterWebView';
-import LinkedInWebView from '../screens/auth/LinkedInWebView';
-import ForgotPasswordScreen from '../screens/auth/ForgotPasswordScreen';
-// PROFILE IMPORTS
-import ProfileQRCodeScanner from '../screens/profile/ProfileQRCodeScanner';
-import ProfileSearchScreen from '../screens/profile/ProfileSearchScreen';
-
-const MainNavigator = createStackNavigator({
- welcome: WelcomeScreen,
- login: LoginScreen,
- forgotPassword: ForgotPasswordScreen,
- signup1: SignupScreenOne,
- signup2: SignupScreenTwo,
- signup3a: SignupScreenThreeA,
- signup3b: SignupScreenThreeB,
- signup4: SignupScreenFour,
- signup5: SignupScreenFive,
- signup6: SignupScreenSix,
- profileQRScan: ProfileQRCodeScanner,
- profileSearch: ProfileSearchScreen
-});
-
-const RootNavigator = createStackNavigator(
-{
- Main: {
- screen: MainNavigator,
- navigationOptions: { header: null }
- },
- twitterWebView: TwitterWebView,
- linkedInWebView: LinkedInWebView
-},
-{
- mode: 'modal'
-});
-
-const AppNavigator = createAppContainer(RootNavigator);
-
-export default AppNavigator;
diff --git a/src/realm/actions/AuthActions.js b/src/realm/actions/AuthActions.js
index 1a34a02..9989e40 100644
--- a/src/realm/actions/AuthActions.js
+++ b/src/realm/actions/AuthActions.js
@@ -6,6 +6,7 @@ import { UserSchema, ProfileSchema, InvitesSchema, ContactsSchema, SignupConfig,
var authRealm;
var signupRealm;
+var sessionId;
// Loads the signup state; a `then` should be chained at the component level to save the state
export async function loadSignupState(screen) {
@@ -37,9 +38,15 @@ export async function saveAuthState(sessionId) {
}
export async function getAuthState() {
+ if (!!sessionId) {
+ return sessionId;
+ }
+
try {
const realm = await getRealm(0);
- return realm.objectForPrimaryKey(SessionSchema.name, primaryKey);
+ const auth = realm.objectForPrimaryKey(SessionSchema.name, primaryKey);
+ sessionId = !!auth ? auth.sessionId : undefined;
+ return sessionId;
} catch (error) {
console.log(error);
}
diff --git a/src/screens/contacts/RequestsScreen.js b/src/screens/contacts/RequestsScreen.js
index a7a0156..c21af3d 100644
--- a/src/screens/contacts/RequestsScreen.js
+++ b/src/screens/contacts/RequestsScreen.js
@@ -1,21 +1,265 @@
import React from 'react';
-import { Text, View } from 'react-native';
+import { View, Text, TouchableOpacity, FlatList } from 'react-native';
+import { ListItem, Icon } from 'react-native-elements';
+
+import { fetchTagferApi } from '../../utils/fetch';
+
+import TagferAvatar from '../../components/new/TagferAvatar';
+import colors from '../../config/colors.json';
+import endpoints from '../../config/apiEndpoints';
+import { showFlashErrorMessage } from '../../utils/errorHandler';
+
+export default class RequestsScreen extends React.Component {
+ static navigationOptions = ({ navigation }) => {
+ const title = navigation.getParam('title', 'Unconfirmed');
+ return { title };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ filter: 'rcvd',
+ currRequests: [],
+ loading: false
+ };
+
+ this.rcvdRequests = [];
+ this.sentRequests = [];
+ this.onRcvdPress = this.onRcvdPress.bind(this);
+ this.onSentPress = this.onSentPress.bind(this);
+ }
+
+ componentDidMount() {
+ this.onLoad();
+ }
+
+ onRcvdPress() {
+ this.setState({ filter: 'rcvd', currRequests: this.rcvdRequests });
+ }
+
+ onSentPress() {
+ this.setState({ filter: 'sent', currRequests: this.sentRequests });
+ }
+
+ onAccept(index) {
+ const fromTagferId = this.rcvdRequests[index].tagferId;
+ const fromProfileN = this.rcvdRequests[index].profileN;
+ const response = fetchTagferApi(endpoints.acceptConnectionRequest(1), { fromTagferId, fromProfileN });
+
+ if (response.error) {
+ return showFlashErrorMessage(response.error);
+ }
+
+ this.rcvdRequests.splice(index, 1);
+ this.setState({ currRequests: this.rcvdRequests });
+ this.updateTitle();
+ }
+
+ async onReject(index) {
+ const { filter } = this.state;
+ const fromTagferId = filter === 'rcvd' ? this.rcvdRequests[index].tagferId : undefined;
+ const toTagferId = filter === 'sent' ? this.sentRequests[index].tagferId : undefined;
+ const response = await fetchTagferApi(endpoints.removeConnectionRequest, { fromTagferId, toTagferId });
+
+ if (response.error) {
+ return showFlashErrorMessage(response.error);
+ }
+
+ if (this.state.filter === 'rcvd') {
+ this.rcvdRequests.splice(index, 1);
+ this.setState({ currRequests: this.rcvdRequests });
+ } else {
+ this.sentRequests.splice(index, 1);
+ this.setState({ currRequests: this.sentRequests });
+ }
+
+ this.updateTitle();
+ }
+
+ async onLoad() {
+ this.setState({ loading: true });
+ const response = await fetchTagferApi(endpoints.getConnectionRequests);
+
+ if (response.error) {
+ this.setState({ loading: false });
+ return showFlashErrorMessage(response.error);
+ }
+
+ this.rcvdRequests = response.received;
+ this.sentRequests = response.sent;
+ this.updateTitle();
+
+ const currRequests = this.state.filter === 'rcvd' ? this.rcvdRequests : this.sentRequests;
+ this.setState({ currRequests, loading: false });
+ }
+
+ updateTitle() {
+ let numOfRequests = this.rcvdRequests.length + this.sentRequests.length;
+ if (numOfRequests > 100) { numOfRequests = '99+'; }
+
+ this.props.navigation.setParams({ title: `Unconfirmed(${numOfRequests})` });
+ }
+
+ // Render a single entry in the list
+ renderItem(profile, index) {
+ return (
+ this.onAccept(index)}
+ onReject={() => this.onReject(index)}
+ />
+ );
+ }
-export default class UnconfirmedConnetcionsScreen extends React.Component {
-
render() {
return (
- Unconfirmed Connections
+
+ this.renderItem(item, index)}
+ keyExtractor={(item) => item.tagferId}
+ style={{ flex: 1, marginHorizontal: 10, marginTop: 10, borderRadius: 5 }}
+ refreshing={this.state.loading}
+ onRefresh={() => this.onLoad()}
+ />
);
}
}
+const FilterButtons = ({ filter, rcvdCount = 0, sentCount = 0, onRcvdPress, onSentPress }) => {
+ let rcvdButtonStyle;
+ let rcvdButtonText;
+ let sentButtonStyle;
+ let sentButtonText;
+
+ if (filter === 'sent') {
+ rcvdButtonStyle = styles.rcvdButtonNotSelected;
+ sentButtonStyle = styles.sentButtonSelected;
+ rcvdButtonText = styles.notSelectedText;
+ sentButtonText = styles.selectedText;
+ } else {
+ rcvdButtonStyle = styles.rcvdButtonSelected;
+ sentButtonStyle = styles.sentButtonNotSelected;
+ rcvdButtonText = styles.selectedText;
+ sentButtonText = styles.notSelectedText;
+ }
+
+ return (
+
+
+ Requests received ({rcvdCount})
+
+
+
+ Requests sent ({sentCount})
+
+
+ );
+};
+
+const RequestListItem = ({ type, profile, onAccept, onReject }) => {
+ const { tagferId, fullName, jobTitle, companyName, photoURL } = profile;
+ const subtitle = `@${tagferId}\n${jobTitle} at ${companyName}`;
+
+ return (
+ }
+ title={fullName}
+ subtitle={subtitle}
+ subtitleNumberOfLines={3}
+ subtitleStyle={{ fontSize: 12, fontWeight: 'normal' }}
+ titleStyle={{ fontSize: 16, color: 'black' }}
+ rightIcon={}
+ containerStyle={{ backgroundColor: 'white' }}
+ />
+ );
+};
+
+const ActionButtons = ({ type, onAccept, onReject }) => {
+ const sentActionButtons = (
+
+ );
+
+ const rcvdActionButtons = (
+
+
+
+
+ );
+
+ return type === 'sent' ? sentActionButtons : rcvdActionButtons;
+};
+
const styles = {
container: {
flex: 1,
+ backgroundColor: colors.offWhite
+ },
+ rcvdButtonSelected: {
+ marginLeft: 40,
+ height: 30,
+ paddingHorizontal: 15,
justifyContent: 'center',
- alignItems: 'center'
+ borderRadius: 15,
+ backgroundColor: '#AAAFC4'
+ },
+ rcvdButtonNotSelected: {
+ marginLeft: 40,
+ height: 30,
+ paddingHorizontal: 15,
+ justifyContent: 'center',
+ borderRadius: 15
+ },
+ sentButtonSelected: {
+ marginRight: 40,
+ height: 30,
+ paddingHorizontal: 15,
+ justifyContent: 'center',
+ borderRadius: 15,
+ backgroundColor: '#AAAFC4'
+ },
+ sentButtonNotSelected: {
+ marginRight: 40,
+ height: 30,
+ paddingHorizontal: 15,
+ justifyContent: 'center',
+ borderRadius: 15
+ },
+ selectedText: {
+ fontSize: 13,
+ fontWeight: 'bold',
+ color: 'white'
+ },
+ notSelectedText: {
+ fontSize: 14,
+ fontWeight: 'normal',
+ color: '#6A6A6A'
}
};
diff --git a/src/screens/contacts/SuggestedContactsScreen.js b/src/screens/contacts/SuggestedContactsScreen.js
index 5b73c48..cf5fa64 100644
--- a/src/screens/contacts/SuggestedContactsScreen.js
+++ b/src/screens/contacts/SuggestedContactsScreen.js
@@ -1,21 +1,85 @@
import React from 'react';
-import { Text, View } from 'react-native';
+import { View, FlatList } from 'react-native';
+
+import { fetchAuth, fetchTagferApi } from '../../utils/fetch';
+import { showFlashErrorMessage } from '../../utils/errorHandler';
+
+import colors from '../../config/colors.json';
+import endpoints from '../../config/apiEndpoints';
+import UserListItem from '../../components/new/UserListItem';
+
+export default class SuggestedContactsScreen extends React.Component {
+ static navigationOptions = ({ navigation }) => {
+ const title = navigation.getParam('title', 'Suggested');
+ return { title };
+ }
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ users: [],
+ loading: true
+ };
+ }
+
+ componentDidMount() {
+ this.onLoad();
+ }
+
+ // Load suggested contacts
+ async onLoad() {
+ const resp = await fetchAuth(endpoints.suggestProfiles);
+ this.setState({ loading: false, users: resp.profiles || [] });
+
+ if (resp.error) {
+ return showFlashErrorMessage(resp.error);
+ }
+
+ this.updateTitle();
+ }
+
+ // WARN: this might break as we are mutating state
+ async onConnect(index) {
+ const response = await fetchTagferApi(endpoints.sendConnectionRequest(1), { toTagferId: this.state.users[index].tagferId });
+ if (response.error) {
+ return showFlashErrorMessage(response.error);
+ }
+
+ this.state.users.splice(index, 1);
+ this.updateTitle();
+
+ if (this.state.users.length === 0) {
+ this.onLoad();
+ }
+ }
+
+ // Update count in title
+ updateTitle() {
+ let count = this.state.users.length;
+ if (count > 100) { count = '99+'; }
+
+ this.props.navigation.setParams({ title: `Suggested(${count})` });
+ }
+
+ // Render a single entry in the list
+ renderItem(profile, index) {
+ return this.onConnect(index)} />;
+ }
-export default class SuggestedConnectionsScreen extends React.Component {
-
render() {
return (
-
- Suggested Connections
+
+ {/* SUGGESTED CONTACTS */}
+ this.renderItem(item, index)}
+ keyExtractor={(item) => item.tagferId}
+ style={{ flex: 1, marginHorizontal: 10, marginTop: 10, borderRadius: 2 }}
+ refreshing={this.state.loading}
+ onRefresh={() => this.onLoad()}
+ />
);
}
}
-
-const styles = {
- container: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center'
- }
-};
diff --git a/src/utils/fetch.js b/src/utils/fetch.js
index f70b57c..5b49320 100644
--- a/src/utils/fetch.js
+++ b/src/utils/fetch.js
@@ -1,6 +1,8 @@
import appConfig from '../config/appConfig.json';
import errors from '../config/errors.js';
+import { getAuthState } from '../realm/actions/AuthActions';
+
export function fetchAuth(endpoint, body) {
const request = {
method: endpoint.method,
@@ -13,3 +15,20 @@ export function fetchAuth(endpoint, body) {
return fetch(endpoint.url, request).then(res => res.json()).catch(() => ({ error: errors.APP_NETWORK_ERROR }));
}
+
+
+// Fetch wrapper for authorized calls with valid session id. Returns either a JSON object documented in the wiki or an error.
+export async function fetchTagferApi(endpoint, body) {
+ const sessionId = await getAuthState();
+
+ const request = {
+ method: endpoint.method,
+ headers: {
+ Authorization: sessionId,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify(body)
+ };
+
+ return fetch(endpoint.url, request).then(res => res.json()).catch(() => ({ error: errors.APP_NETWORK_ERROR }));
+}