UPS1 - Signup with Realm

This commit is contained in:
Omar 2019-01-09 19:40:19 -08:00
parent a01d5345cf
commit 8ad99768f0
75 changed files with 1041 additions and 29838 deletions

View File

@ -1,7 +1,8 @@
{
module.exports = {
"extends": "rallycoding",
"rules": {
"max-len": 0
"max-len": 0,
"react/require-extension": "off"
},
"globals": {
"fetch": true

3
.gitignore vendored
View File

@ -59,3 +59,6 @@ buck-out/
#Storybook
storybook
test.js
ios/Tagfer.xcodeproj/project.pbxproj
package-lock.json

View File

@ -137,14 +137,15 @@ android {
}
dependencies {
compile project(':react-native-app-settings')
compile project(':react-native-image-picker')
compile project(':react-native-camera')
compile project(':react-native-contacts')
compile project(':react-native-sms')
compile project(':realm')
compile project(':react-native-webview')
compile project(':react-native-vector-icons')
compile project(':react-native-image-picker')
compile project(':react-native-gesture-handler')
compile project(':realm')
compile project(':react-native-contacts')
compile project(':react-native-camera')
compile project(':react-native-app-settings')
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
implementation "com.facebook.react:react-native:+" // From node_modules

View File

@ -3,14 +3,15 @@ package com.tagfer;
import android.app.Application;
import com.facebook.react.ReactApplication;
import com.krazylabs.OpenAppSettingsPackage;
import com.imagepicker.ImagePickerPackage;
import org.reactnative.camera.RNCameraPackage;
import com.rt2zz.reactnativecontacts.ReactNativeContacts;
import com.tkporter.sendsms.SendSMSPackage;
import io.realm.react.RealmReactPackage;
import com.reactnativecommunity.webview.RNCWebViewPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.imagepicker.ImagePickerPackage;
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage;
import io.realm.react.RealmReactPackage;
import com.rt2zz.reactnativecontacts.ReactNativeContacts;
import org.reactnative.camera.RNCameraPackage;
import com.krazylabs.OpenAppSettingsPackage;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
@ -31,14 +32,15 @@ public class MainApplication extends Application implements ReactApplication {
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new OpenAppSettingsPackage(),
new ImagePickerPackage(),
new RNCameraPackage(),
new ReactNativeContacts(),
SendSMSPackage.getInstance(),
new RealmReactPackage(),
new RNCWebViewPackage(),
new VectorIconsPackage(),
new ImagePickerPackage(),
new RNGestureHandlerPackage(),
new RealmReactPackage()
new ReactNativeContacts(),
new RNCameraPackage(),
new OpenAppSettingsPackage()
);
}

View File

@ -1,19 +1,21 @@
rootProject.name = 'Tagfer'
include ':react-native-app-settings'
project(':react-native-app-settings').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-app-settings/android')
include ':react-native-image-picker'
project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android')
include ':react-native-camera'
project(':react-native-camera').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-camera/android')
include ':react-native-contacts'
project(':react-native-contacts').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-contacts/android')
include ':react-native-sms'
project(':react-native-sms').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sms/android')
include ':realm'
project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android')
include ':react-native-webview'
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
include ':react-native-image-picker'
project(':react-native-image-picker').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-image-picker/android')
include ':react-native-gesture-handler'
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
include ':realm'
project(':realm').projectDir = new File(rootProject.projectDir, '../node_modules/realm/android')
include ':react-native-contacts'
project(':react-native-contacts').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-contacts/android')
include ':react-native-camera'
project(':react-native-camera').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-camera/android')
include ':react-native-app-settings'
project(':react-native-app-settings').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-app-settings/android')
include ':app'

View File

@ -7,7 +7,7 @@
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
@ -22,6 +22,19 @@
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
@ -36,19 +49,5 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<key>NSAppTransportSecurity</key>
<!--See http://ste.vn/2015/06/10/configuring-app-transport-security-ios-9-osx-10-11/ -->
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
</dict>
</plist>

File diff suppressed because it is too large Load Diff

View File

@ -80,7 +80,7 @@
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"

View File

@ -1,34 +1,58 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-Spotlight-40.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-60.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-Small@2x.png",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-Small@3x.png",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-Spotlight-40@2x.png",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-Spotlight-40@3x.png",
"scale" : "3x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-60@2x.png",
"scale" : "2x"
},
{
"idiom" : "iphone",
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-60@3x.png",
"scale" : "3x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Logo2.png",
"scale" : "1x"
}
],
"info" : {

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -21,25 +21,9 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1</string>
<string>2</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSLocationWhenInUseUsageDescription</key>
<string/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
@ -53,12 +37,26 @@
</dict>
</dict>
</dict>
<key>NSAppleMusicUsageDescription</key>
<string>Listen to tunes</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Send photos using bluetooth</string>
<key>NSCalendarsUsageDescription</key>
<string>Need to add events to your calendar</string>
<key>NSCameraUsageDescription</key>
<string>Upload photos and scan QR codes</string>
<key>NSContactsUsageDescription</key>
<string>Allow the user to search for his phone contacts in network</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string></string>
<string>Find your location</string>
<key>NSMicrophoneUsageDescription</key>
<string>Record audio clips</string>
<key>NSMotionUsageDescription</key>
<string>Orientation of device</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Add images to profile</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Speech to text</string>
<key>UIAppFonts</key>
<array>
<string>Entypo.ttf</string>

18525
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"name": "tagfer",
"name": "Tagfer",
"version": "0.0.1",
"private": true,
"scripts": {
@ -14,23 +14,25 @@
"react-native-app-settings": "^2.0.1",
"react-native-camera": "^1.8.0",
"react-native-contacts": "^2.2.5",
"react-native-country-picker-modal": "^0.6.2",
"react-native-country-picker-modal": "^0.7.1",
"react-native-elements": "^0.19.1",
"react-native-flash-message": "^0.1.10",
"react-native-gesture-handler": "^1.0.12",
"react-native-image-picker": "^0.28.0",
"react-native-masked-text": "^1.9.2",
"react-native-permissions": "^1.1.1",
"react-native-sms": "^1.8.0",
"react-native-vector-icons": "^4.6.0",
"react-native-webview": "^2.14.3",
"react-navigation": "^3.0.9",
"react-native-image-picker": "^0.28.0",
"react-native-permissions": "^1.1.1",
"realm": "^2.21.1"
"realm": "^2.23.0"
},
"devDependencies": {
"@storybook/react-native": "^4.1.6",
"babel-jest": "23.6.0",
"eslint": "^3.19.0",
"eslint-config-rallycoding": "^3.2.0",
"eslint-plugin-react": "^7.12.4",
"jest": "23.6.0",
"metro-react-native-babel-preset": "0.51.1",
"react-test-renderer": "16.6.3"

View File

@ -1,7 +0,0 @@
/** Contains all string key/value pairs to prevent typos **/
import { strings } from '../locales/i18n';
export const GOING = strings('event.attendeeStatus_going');
export const INTERESTED = strings('event.attendeeStatus_interested');
export const INVITED = strings('event.attendeeStatus_invited');
export const NOT_GOING = strings('event.attendeeStatus_notGoing');

View File

@ -1,892 +0,0 @@
/** Contains the base fields for a contact reuse **/
import React, { Component } from 'react';
import { Alert, Dimensions, Linking, View } from 'react-native';
import { Button, Tile } from 'react-native-elements';
import { connect } from 'react-redux';
import { ImagePicker, Permissions } from 'expo';
import { contactUpdate, profileUpdate } from '../actions';
import { PROFILE_FORM, CONTACT_FORM, SOCIAL_FORM } from './FormTypes';
import { Spacer } from './common';
import privateKeys from '../../private_keys.json';
import colors from '../config/colors.json';
import { strings } from '../locales/i18n';
const width = Dimensions.get('window').width * 0.9;
const height = width * 0.57; // ( 2 / 3.5 = 0.57) Business card dimensions
const regexObj = {};
const visionObj = {};
// Since we have duplicate keys between contact and profile we can't use this.props
// so we track what has been populated here
const propsAlreadySet = {};
class BizCard extends Component {
constructor(props) {
super(props);
this.state = {
cameraRollStatus: '',
text: null,
company: null,
jobTitle: null,
city: null,
region: null
};
this.setCameraRollStatus();
}
/**
* Component Will Mount
**/
componentWillMount() {
if (this.props.isScanBizCardScreen) {
this.onTakePhotoButtonPress();
}
}
/**
* Sends user to the gallery to upload image from device
**/
onPickImageButtonPress = async () => {
if (this.state.cameraRollStatus !== 'granted') {
await Permissions.askAsync(Permissions.CAMERA_ROLL)
.then((data) => {
if (data.status !== 'granted') {
Alert.alert(strings('common.error.permissionDenied_cameraRoll'));
Linking.openURL('app-settings:');
return;
}
}).catch(err => console.error('askAsync camera roll error ', err));
this.setCameraRollStatus();
}
const result = await ImagePicker.launchImageLibraryAsync({
allowsEditing: true,
aspect: [3.5, 2],
base64: true,
mediaTypes: ImagePicker.MediaTypeOptions.Images
});
if (!result.cancelled) {
this.processImage(result);
}
};
/**
* Opens the camera for the user to take a new photo
**/
onTakePhotoButtonPress = async () => {
const result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [4, 3],
base64: true
});
if (!result.cancelled) {
this.processImage(result);
}
};
/**
* Gets the current permission state of the app for camera roll/photos
**/
setCameraRollStatus = async () => {
const { status } = await Permissions.getAsync(Permissions.CAMERA_ROLL);
this.setState({ cameraRollStatus: status });
}
/**
* Takes the chosen image in base64 and saves the base64 and submits it for text recognition
**/
processImage = async (response) => {
this.setState({ text: strings('bizCard.loading') });
propsAlreadySet.base64 = true;
if (this.props.formType === CONTACT_FORM) {
this.props.contactUpdate({ prop: 'contactBizCardImg', value: response.uri });
} else if (this.props.formType === PROFILE_FORM) {
this.props.profileUpdate({ prop: 'profileBizCardImg', value: response.uri });
}
const body = {
requests: [
{
image: {
content: response.base64,
},
features: [
{
type: 'DOCUMENT_TEXT_DETECTION',
}
]
},
],
};
const visionResponse = await fetch(`https://vision.googleapis.com/v1/images:annotate?key=${privateKeys.googleapis.GOOGLE_API_KEY}`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(body)
});
const visionParsed = await visionResponse.json();
// Checks that some text was found before trying to read it
if (visionParsed.responses[0].fullTextAnnotation && visionParsed.responses[0].fullTextAnnotation.text) {
this.setState({
text: visionParsed.responses[0].fullTextAnnotation.text,
});
console.log('STATE.TEXT\n', this.state.text);
this.extractImageContext(this.state.text);
}
}
/**
* Extracts information from the OCR text and separates it based on context
**/
extractImageContext = async (imageText) => {
const body = {
encodingType: 'UTF8',
document: {
type: 'PLAIN_TEXT',
content: imageText
}
};
const languageResponse = await fetch(`https://language.googleapis.com/v1/documents:analyzeEntities?key=${privateKeys.googleapis.GOOGLE_API_KEY}`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(body)
});
const languageParsed = await languageResponse.json();
// console.log('languageParsed,entities is\n', languageParsed.entities);
this.populatePhoneField(imageText);
this.extractFullAddress(imageText);
// Try fullAddress first
if (regexObj.fullAddress) {
this.populateFieldsFromFullAddress();
}
this.performVisionParsing(languageParsed.entities);
if (!propsAlreadySet.street || !propsAlreadySet.city || !propsAlreadySet.region || !propsAlreadySet.postalCode) {
this.extractSecondLine(imageText);
this.populateAddressByLine(imageText);
}
if (!propsAlreadySet.email) {
this.extractEmail(imageText);
}
if (!propsAlreadySet.website) {
this.populateWebsiteField(imageText);
}
if (!propsAlreadySet.company) {
this.populateCompanyField();
}
}
/**
* Populates fields based on the common data assignment based on the data below
* 0 - PROPER 1 - PERSON === NAME
* 0 - PROPER 1 - ORGANIZATION === ADDRESS OR COMPANY
* 0 - PROPER 1 - LOCATION === ADDRESS
* 0 - PROPER 1 - OTHER === EMAIL OR WEBSITE
* 0 - PROPER 1 - WORKOFART === ADDRESS
* 0 - COMMON 1 - PERSON === TITLE
**/
performVisionParsing = (languageEntities) => {
languageEntities.forEach((entity) => {
for (let i = 0; i < entity.mentions.length; i++) {
switch (entity.mentions[i].type) {
case 'PROPER':
// PROPER - PERSON is the name
// We don't get the name from regex so just set it here
if (entity.type === 'PERSON' && !propsAlreadySet.firstName) {
this.populateNameField(entity.name);
// PROPER - LOCATION is the address
} else if (entity.type === 'LOCATION') {
this.extractAddressFromVisionFields(entity.name);
// PROPER - ORGANIZATION is the address or company
} else if (entity.type === 'ORGANIZATION') {
if (this.isAddressField(entity.name)) {
this.extractAddressFromVisionFields(entity.name);
} else {
this.extractAddressOrCompany(entity.name);
}
// PROPER - WORKOFART is the address
} else if (entity.type === 'WORKOFART') {
this.extractAddressFromVisionFields(entity.name);
// PROPER - OTHER is the email or website
} else if (entity.type === 'OTHER') {
this.extractEmailOrWebsite(entity.name);
}
break;
case 'COMMON':
// COMMON - PERSON is the jobTitle
// We don't get the jobTitle from regex so just set it here
if (entity.type === 'PERSON' && !propsAlreadySet.jobTitle) {
propsAlreadySet.jobTitle = true;
this.setState({ jobTitle: entity.name });
}
break;
default:
break;
}
}
});
this.combineTitleAndCompany();
this.combineLocation();
}
/**
* Uses regex to find phone number from the imageText.
* Since phone isn't typically extracted from the
* vision process we set the phone now
**/
populatePhoneField(imageText) {
try {
const phoneRegex = /\(?\d{3}\)?-?\.?\s?\d{3}-?.?\s?\d{4}/;
let extractedPhone = '';
if (Array.isArray(imageText)) {
extractedPhone = imageText[0].match(phoneRegex);
} else {
extractedPhone = imageText.match(phoneRegex);
}
if (extractedPhone) {
propsAlreadySet.primaryPhone = true;
if (this.props.formType === CONTACT_FORM) {
this.props.contactUpdate({ prop: 'primaryPhone', value: extractedPhone[0] });
} else if (this.props.formType === PROFILE_FORM) {
this.props.profileUpdate({ prop: 'primaryPhone', value: extractedPhone[0] });
}
}
} catch (error) {
console.log(`Could not populate phone from '${imageText}': ${error}`);
}
}
/**
* Sets the firstName and lastName fields
**/
populateNameField(proposedName) {
const nameArray = proposedName.split(' ');
let lastNameStr = '';
switch (true) {
case (nameArray.length > 3):
return;
case (nameArray.length === 3):
lastNameStr = `${nameArray[1]} ${nameArray[2]}`;
break;
case (nameArray.length === 2):
lastNameStr = nameArray[1];
break;
default:
break;
}
propsAlreadySet.firstName = true;
let fullName = '';
if (lastNameStr) {
fullName = `${nameArray[0]} ${lastNameStr}`;
} else {
fullName = nameArray[0];
}
if (this.props.formType === CONTACT_FORM) {
this.props.contactUpdate({ prop: 'firstName', value: nameArray[0] });
this.props.contactUpdate({ prop: 'fullName', value: fullName });
if (lastNameStr) {
this.props.contactUpdate({ prop: 'lastName', value: lastNameStr });
}
} else if (this.props.formType === PROFILE_FORM) {
this.props.profileUpdate({ prop: 'firstName', value: nameArray[0] });
this.props.profileUpdate({ prop: 'fullName', value: fullName });
if (lastNameStr) {
this.props.profileUpdate({ prop: 'lastName', value: lastNameStr });
}
}
}
/**
* Performs regex and populates fields if found
**/
extractEmail(textToParse) {
try {
const emailRegex = /[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+/i;
let extractedEmail = '';
if (Array.isArray(textToParse)) {
extractedEmail = textToParse[0].match(emailRegex);
} else {
extractedEmail = textToParse.match(emailRegex);
}
if (extractedEmail) {
propsAlreadySet.email = true;
regexObj.email = extractedEmail;
if (this.props.formType === CONTACT_FORM) {
this.props.contactUpdate({ prop: 'contactEmail', value: extractedEmail[0] });
} else if (this.props.formType === PROFILE_FORM) {
this.props.profileUpdate({ prop: 'profileEmail', value: extractedEmail[0] });
}
}
} catch (error) {
console.log(`Could not extract email from '${textToParse}': ${error}`);
}
}
/**
* Vision extraction has already been attempted
* Extracts the website from image text first and if unsuccessful
* then just get it from the email domain
**/
populateWebsiteField(imageText) {
try {
if (!propsAlreadySet.website) {
this.extractWebsite(imageText);
}
if (!propsAlreadySet.website && propsAlreadySet.email) {
this.extractWebsite(regexObj.email);
}
} catch (error) {
console.log('Could not populate website', error);
}
}
/**
* Performs regex and populates fields if found
**/
extractWebsite(textToParse) {
try {
const websiteRegex = /w?w?w?\.?[a-z-]{2,}\.[a-z]{2,}\.?[a-z]*/i;
let extractedWebsite = '';
if (Array.isArray(textToParse)) {
extractedWebsite = textToParse[0].match(websiteRegex);
} else {
extractedWebsite = textToParse.match(websiteRegex);
}
if (extractedWebsite) {
propsAlreadySet.website = true;
if (this.props.formType === CONTACT_FORM) {
this.props.contactUpdate({ prop: 'website', value: extractedWebsite[0] });
} else if (this.props.formType === PROFILE_FORM) {
this.props.profileUpdate({ prop: 'website', value: extractedWebsite[0] });
}
}
} catch (error) {
console.log(`Could not extract website from '${textToParse}': ${error}`);
}
}
/**
* Extracts the company from vision first and from image text second
* If both of those fail then just get it from the email domain
**/
populateCompanyField() {
try {
if (propsAlreadySet.company) {
return;
}
if (visionObj.company) {
this.extractCompanyFromVision();
} else if (propsAlreadySet.email) {
this.extractCompanyFromEmail();
}
} catch (error) {
console.log('Could not populate company', error);
}
}
/**
* Populates the company from the vision response
**/
extractCompanyFromVision() {
propsAlreadySet.company = true;
this.setState({ company: visionObj.company });
}
/**
* Extracts the company from the email
**/
extractCompanyFromEmail() {
const { email } = regexObj;
// Sets the company to be anything between the '@' and '.'
let atIndex;
let dotIndex;
let foundAt = false;
for (let i = 0; i < email.length; i++) {
if (email[i] === '@') {
atIndex = i + 1;
foundAt = true;
} else if (foundAt && email[i] === '.') {
dotIndex = i;
}
}
const company = email.slice(atIndex, dotIndex);
propsAlreadySet.company = true;
this.setState({ company });
}
/**
* Combines the jobTitle and Company fields into one field for the details section
**/
combineTitleAndCompany() {
const company = this.state.company || '';
const jobTitle = this.state.jobTitle || '';
if (!company && !jobTitle) {
return;
}
const titleAndCompany = `${jobTitle} ${company}`;
if (this.props.formType === CONTACT_FORM) {
this.props.contactUpdate({ prop: 'titleAndCompany', value: titleAndCompany });
} else if (this.props.formType === PROFILE_FORM) {
this.props.profileUpdate({ prop: 'titleAndCompany', value: titleAndCompany });
}
}
/**
* Combines the region and state fields into one field for the details section
**/
combineLocation() {
const city = this.state.city || '';
const region = this.state.region || '';
if (!city && !region) {
return;
}
const location = region ? `${city}, ${region}` : city;
if (this.props.formType === CONTACT_FORM) {
this.props.contactUpdate({ prop: 'location', value: location });
} else if (this.props.formType === PROFILE_FORM) {
this.props.profileUpdate({ prop: 'location', value: location });
}
}
/**
* Uses regex to find the full address for comparison to vision response
**/
extractFullAddress(imageText) {
try {
const fullAddressRegex = /\d+ [a-zA-Z,.# 0-9]+[\s |][a-zA-Z ]+, [A-Z]{2}\s\d{5}/;
const extractedFullAddress = imageText.match(fullAddressRegex);
if (extractedFullAddress) {
regexObj.fullAddress = extractedFullAddress;
}
} catch (error) {
console.log(`Could not extract full address from '${imageText}': ${error}`);
}
}
/**
* Uses regex to find the first line of address for comparison to vision response
**/
extractFirstLine(textToParse) {
try {
regexObj.firstLine = '';
const firstLineRegex = /\d+ [a-zA-Z.,# 0-9]+\s/;
let extractedFirstLine = '';
if (Array.isArray(textToParse)) {
extractedFirstLine = textToParse[0].match(firstLineRegex);
} else {
extractedFirstLine = textToParse.match(firstLineRegex);
}
if (extractedFirstLine && !propsAlreadySet.street) {
propsAlreadySet.street = true;
if (this.props.formType === CONTACT_FORM) {
this.props.contactUpdate({ prop: 'street', value: extractedFirstLine[0] });
} else if (this.props.formType === PROFILE_FORM) {
this.props.profileUpdate({ prop: 'street', value: extractedFirstLine[0] });
}
}
} catch (error) {
console.log(`Could not extract first line of address from'${textToParse}': ${error}`);
}
}
/**
* Uses regex to find the second line of address for comparison to vision response
**/
extractSecondLine(textToParse) {
try {
regexObj.secondLine = '';
const secondLineRegex = /[a-zA-Z ]+, [A-Z]{2}\s\d{5}/;
let extractedSecondLine = '';
if (Array.isArray(textToParse)) {
extractedSecondLine = textToParse[0].match(secondLineRegex);
} else {
extractedSecondLine = textToParse.match(secondLineRegex);
}
if (extractedSecondLine) {
regexObj.secondLine = extractedSecondLine[0];
return true;
}
return false;
} catch (error) {
console.log(`Could not extract second line of address from '${textToParse}': ${error}`);
}
}
/**
* Uses regex to find the city
**/
extractCity(textToParse) {
try {
regexObj.city = '';
const cityRegex = /[a-zA-Z ]+,/;
let extractedCity = '';
if (Array.isArray(textToParse)) {
extractedCity = textToParse[0].match(cityRegex);
} else {
extractedCity = textToParse.match(cityRegex);
}
if (extractedCity) {
extractedCity = extractedCity[0].replace(/,/, '');
propsAlreadySet.city = true;
this.setState({ city: extractedCity });
if (this.props.formType === CONTACT_FORM) {
this.props.contactUpdate({ prop: 'city', value: extractedCity });
} else if (this.props.formType === PROFILE_FORM) {
this.props.profileUpdate({ prop: 'city', value: extractedCity });
}
}
} catch (error) {
console.log(`Could not extract city from '${textToParse}': ${error}`);
}
}
/**
* Uses regex to find the region (state)
**/
extractRegion(textToParse) {
try {
regexObj.region = '';
const regionRegex = /, [A-Z]{2}/;
let extractedRegion = '';
if (Array.isArray(textToParse)) {
extractedRegion = textToParse[0].match(regionRegex);
} else {
extractedRegion = textToParse.match(regionRegex);
}
if (extractedRegion) {
extractedRegion = extractedRegion[0].replace(/, /, '');
propsAlreadySet.region = true;
this.setState({ region: extractedRegion });
if (this.props.formType === CONTACT_FORM) {
this.props.contactUpdate({ prop: 'region', value: extractedRegion });
} else if (this.props.formType === PROFILE_FORM) {
this.props.profileUpdate({ prop: 'region', value: extractedRegion });
}
}
} catch (error) {
console.log(`Could not extract region from ${textToParse}' ${error}`);
}
}
/**
* Uses regex to find the postalCode
**/
extractPostalCode(textToParse) {
try {
regexObj.postalCode = '';
const zipRegex = /\d{5}/;
let extractedPostalCode = '';
if (Array.isArray(textToParse)) {
extractedPostalCode = textToParse[0].match(zipRegex);
} else {
extractedPostalCode = textToParse.match(zipRegex);
}
if (extractedPostalCode) {
propsAlreadySet.postalCode = true;
if (this.props.formType === CONTACT_FORM) {
this.props.contactUpdate({ prop: 'postalCode', value: extractedPostalCode[0] });
} else if (this.props.formType === PROFILE_FORM) {
this.props.profileUpdate({ prop: 'postalCode', value: extractedPostalCode[0] });
}
}
} catch (error) {
console.log(`Could not extract postalCode from '${textToParse}': ${error}`);
}
}
/**
* Checks if a field is an address field of some form
**/
isAddressField(textToParse) {
const fullAddressRegex = /\d+ [a-zA-Z,.# 0-9]+[\s |][a-zA-Z ]+, [A-Z]{2}\s\d{5}/;
const foundFullAddress = textToParse.match(fullAddressRegex);
const firstLineRegex = /\d+ [a-zA-Z.,# 0-9]+\s/;
const foundFirstLine = textToParse.toString().match(firstLineRegex);
const secondLineRegex = /[a-zA-Z ]+, [A-Z]{2}\s\d{5}/;
const foundSecondLine = textToParse.match(secondLineRegex);
const cityRegex = /[a-zA-Z ]+,/;
const foundCity = textToParse.match(cityRegex);
const regionRegex = /, [A-Z]{2}/;
const foundRegion = textToParse.match(regionRegex);
const zipRegex = /\d{5}/;
const foundPostalCode = textToParse.match(zipRegex);
return foundFullAddress || foundFirstLine || foundSecondLine || foundCity || foundRegion || foundPostalCode;
}
/**
* Since the regex for full address is so strict if we found something that
* likely means we have the full address so we'll just use that and not
* look at the vision response since address isn't that reliable
**/
populateFieldsFromFullAddress() {
// Populate the street
this.extractFirstLine(regexObj.fullAddress[0]);
// Get the second line then split into city, region, and zip
this.extractSecondLine(regexObj.fullAddress[0]);
if (regexObj.secondLine) {
// Populate the city, region, and postalCode fields
this.extractCity(regexObj.secondLine);
this.extractRegion(regexObj.secondLine);
this.extractPostalCode(regexObj.secondLine);
}
}
/**
* Uses the vision response to find remaining address fields
**/
extractAddressFromVisionFields(textToParse) {
// Check for Street if not found
if (!propsAlreadySet.street) {
this.extractFirstLine(textToParse);
}
// Check for city if not found
if (!propsAlreadySet.city) {
this.extractCity(textToParse);
}
// Check for region if not found
if (!propsAlreadySet.region) {
this.extractRegion(textToParse);
}
// Check for postalCode if not found
if (!propsAlreadySet.postalCode) {
this.extractPostalCode(textToParse);
}
}
/**
* PROPER - ORGANIZATION is address or company
* This tries to determine which one it is
**/
extractAddressOrCompany(textToParse) {
const firstLineRegex = /\d+ [a-zA-Z.,# 0-9]+\s/;
const extractedFirstLine = textToParse.toString().match(firstLineRegex);
const secondLineRegex = /[a-zA-Z ]+, [A-Z]{2}\s\d{5}/;
const extractedSecondLine = textToParse.match(secondLineRegex);
// If we didn't find either address line then assume it's company
if (!extractedFirstLine && !extractedSecondLine && !propsAlreadySet.company) {
visionObj.company = textToParse;
this.populateCompanyField();
}
// If we found either line then extract the info from it
if (extractedFirstLine || extractedSecondLine) {
this.populateAddressByLine(textToParse);
}
}
/**
* PROPER - OTHER is email or website
* This tries to determine which one it is
**/
extractEmailOrWebsite(textToParse) {
if (textToParse.indexOf('@') > 1) {
if (!propsAlreadySet.email) {
this.extractEmail(textToParse);
}
} else if (!propsAlreadySet.website) {
this.extractWebsite(textToParse);
}
}
/**
* Looks at the remaining fields and try to populate them using regex
**/
populateAddressByLine(textToParse) {
const { secondLine } = regexObj;
if (!propsAlreadySet.street) {
this.extractFirstLine(textToParse);
}
if (secondLine) {
if (!propsAlreadySet.city) {
this.extractCity(secondLine);
}
if (!propsAlreadySet.region) {
this.extractRegion(secondLine);
}
if (!propsAlreadySet.postalCode) {
this.extractPostalCode(secondLine);
}
}
}
/**
* Renders the business card tile
**/
renderBusinessCardTile() {
let base64 = '';
if (this.props.formType === CONTACT_FORM) {
base64 = this.props.contactBizCardImg;
} else if (this.props.formType === PROFILE_FORM) {
base64 = this.props.profileBizCardImg;
} else if (this.props.formType === SOCIAL_FORM) {
base64 = this.props.socialBizCardImg;
}
if (base64) {
return (
<Tile
imageSrc={{ uri: base64 }}
featured
containerStyle={styles.tileContainerStyle}
imageContainerStyle={styles.tileStyle}
/>
);
}
return (
<Tile
featured
title={strings('bizCard.photoTile_noCard')}
containerStyle={styles.tileContainerStyle}
imageContainerStyle={styles.tileStyle}
/>
);
}
/**
* Renders the photo buttons
**/
renderPhotoButtons() {
if (this.props.formType === SOCIAL_FORM || !this.props.isEditMode) {
return <View style={styles.buttonContainerStyle} />;
}
return (
<View style={styles.buttonContainerStyle}>
<Button
onPress={this.onPickImageButtonPress.bind(this)}
title={strings('common.buttons.uploadImage')}
buttonStyle={styles.halfButtonStyle}
disabled={this.props.loading}
icon={{ name: 'folder' }}
/>
<Button
onPress={this.onTakePhotoButtonPress.bind(this)}
title={strings('common.buttons.takePhoto')}
buttonStyle={styles.halfButtonStyle}
disabled={this.props.loading}
icon={{ name: 'photo-camera' }}
/>
</View>
);
}
/**
* Render method
**/
render() {
if (this.props.isEditMode) {
return (
<View style={styles.viewStyle}>
{this.renderBusinessCardTile()}
{this.renderPhotoButtons()}
</View>
);
}
return (
<View style={styles.viewStyle}>
<Spacer space={45} />
{this.renderBusinessCardTile()}
{this.renderPhotoButtons()}
</View>
);
}
}
const styles = {
tileContainerStyle: {
height,
width,
marginTop: 15,
marginBottom: 15
},
tileStyle: {
height,
width
},
buttonContainerStyle: {
flex: 2,
marginTop: 15,
flexDirection: 'row',
justifyContent: 'center'
},
halfButtonStyle: {
backgroundColor: colors.buttonBlue,
flex: 1,
borderRadius: 5
},
viewStyle: {
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center'
}
};
/**
* Map State To Props
**/
const mapStateToProps = (state) => {
const {
profileBizCardImg,
} = state.profileForm;
const {
contactBizCardImg,
} = state.contactForm;
const {
socialBizCardImg,
} = state.socialContacts;
return { profileBizCardImg, contactBizCardImg, socialBizCardImg };
};
export default connect(mapStateToProps, {
contactUpdate, profileUpdate
})(BizCard);

View File

@ -1,23 +0,0 @@
/** Used as a placeholder with an empty title for the drawerNavigator reset action from the welcomScreen **/
import React, { Component } from 'react';
import { Text } from 'react-native';
import { strings } from '../locales/i18n';
class EmptyNavigator extends Component {
static navigationOptions = {
title: '',
}
/**
* Render method
**/
render() {
return (
<Text>
{strings('common.error.navigatedToEmpty')}
</Text>
);
}
}
export default EmptyNavigator;

View File

@ -1,5 +0,0 @@
/** Contains all string key/value pairs to prevent typos **/
export const NEAR_YOU = 'near_you';
export const THIS_WEEK = 'this_week';
export const TODAY = 'today';
export const TOMORROW = 'tomorrow';

View File

@ -1,4 +0,0 @@
/** Contains all string key/value pairs to prevent typos **/
export const THIS_WEEK = 'this_week';
export const TODAY = 'today';
export const TOMORROW = 'tomorrow';

View File

@ -1,6 +0,0 @@
/** Contains all string key/value pairs to prevent typos **/
export const PROFILE_FORM = 'profile';
export const CONTACT_FORM = 'contact';
export const EVENT_FORM = 'event';
export const SOCIAL_FORM = 'social';
export const TASK_FORM = 'task';

View File

@ -1,5 +0,0 @@
/** Contains all string key/value pairs to prevent typos **/
export const REFERRAL_RECEIVED = 'referralReceived';
export const REQUEST_ACCEPTED = 'requestAccepted';
export const REQUEST_RECEIVED = 'requestReceived';
export const REQUEST_SENT = 'requestSent';

View File

@ -1,866 +0,0 @@
/** Contains the base fields for a contact reuse **/
import _ from 'lodash';
import { ImagePicker, MailComposer, Permissions } from 'expo';
import React, { Component } from 'react';
import { Alert, Dimensions, Image, Linking, Modal, ScrollView, Share, Text, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native';
import { Button, Icon } from 'react-native-elements';
import ModalDropdown from 'react-native-modal-dropdown';
import { connect } from 'react-redux';
import { contactUpdate, profileToggleDisabled, profileSave, profileUpdate, socialRemoveFriend } from '../actions';
import { PROFILE_FORM, CONTACT_FORM, SOCIAL_FORM } from './FormTypes';
import { Confirm, Spacer } from './common';
import colors from '../config/colors.json';
import privateKeys from '../../private_keys.json';
import { strings } from '../locales/i18n';
const GREEN_CHECK_IMAGE = require('../../assets/GreenCheck.png');
const SCREEN_WIDTH = Dimensions.get('window').width;
const DEV_LINKING_BASE_URL = privateKeys.deepLinking.devBaseUrl;
const TAGFER_BASE_URL = privateKeys.deepLinking.tagferBaseUrl;
const profileKebabOptions = [
strings('profile.kebabOptions_shareQR'),
strings('profile.kebabOptions_shareLink'),
strings('profile.kebabOptions_edit'),
strings('profile.kebabOptions_duplicate'),
strings('profile.kebabOptions_disable')
];
const friendKebabOptions = [
strings('profile.kebabOptions_shareQR'),
strings('profile.kebabOptions_shareLink'),
// strings('profile.kebabOptions_unfollow'),
strings('profile.kebabOptions_merge'),
strings('profile.kebabOptions_remove'),
strings('profile.kebabOptions_report')
];
let profileNames = [];
let copyFromIndex = 0;
let copyToIndex = 0;
class ProfileAvatar extends Component {
constructor(props) {
super(props);
this.state = {
cameraRollStatus: '',
isDuplicateModalVisible: false,
isDisableModaleVisible: false,
isRemoveModalVisible: false
};
this.setCameraRollStatus();
}
/**
* Component Will Mount
**/
componentWillMount() {
this.populateKebabLastOption();
profileNames = [];
for (let i = 0; i < this.props.profiles.length; i++) {
profileNames.push(this.props.profiles[i].profileName);
}
}
/**
* Copies the profile data from the copyFromIndex to the copyToIndex profile
**/
onDuplicateButtonPress() {
if (copyFromIndex === copyToIndex) {
return;
}
const {
profileBizCardImg,
profileAvatarImg,
firstName,
lastName,
fullName,
headline,
location,
primaryPhone,
secondaryPhone,
extension,
titleAndCompany,
about,
howIHelp,
whatINeed,
experience,
education,
workPhone,
fax,
profileEmail,
website,
street,
address2,
city,
region,
postalCode,
country,
keywords,
referralId,
isDisabled,
} = this.props.profiles[copyFromIndex];
const { profileName, uid } = this.props.profiles[copyToIndex];
this.props.profileSave({
profileBizCardImg,
profileAvatarImg,
firstName,
lastName,
fullName,
headline,
location,
primaryPhone,
secondaryPhone,
extension,
titleAndCompany,
about,
howIHelp,
whatINeed,
experience,
education,
workPhone,
fax,
profileEmail,
website,
street,
address2,
city,
region,
postalCode,
country,
keywords,
referralId,
profileName,
isDisabled,
uid // DON'T DELETE THIS PROPERTY WHEN UPDATING LIST
});
this.setState({ isDuplicateModalVisible: false });
}
/**
* Sends user to the gallery to upload image from device
**/
onPickImageButtonPress = async () => {
if (this.state.cameraRollStatus !== 'granted') {
await Permissions.askAsync(Permissions.CAMERA_ROLL)
.then((data) => {
if (data.status !== 'granted') {
Alert.alert(strings('common.error.permissionDenied_cameraRoll'));
Linking.openURL('app-settings:');
return;
}
}).catch(err => console.error('askAsync camera roll error ', err));
this.setCameraRollStatus();
}
const result = await ImagePicker.launchImageLibraryAsync({
allowsEditing: true,
aspect: [3.5, 2],
base64: true,
mediaTypes: ImagePicker.MediaTypeOptions.Images
});
if (!result.cancelled) {
this.processImage(result.uri);
}
};
/**
* Opens the camera for the user to take a new photo
**/
onTakePhotoButtonPress = async () => {
const result = await ImagePicker.launchCameraAsync({
allowsEditing: true,
aspect: [4, 3],
base64: true
});
if (!result.cancelled) {
this.processImage(result.uri);
}
};
/**
* Disables the profile
**/
onDisableAccept() {
this.toggleProfileEnabled();
this.setState({ isDisableModaleVisible: false });
}
/**
* Closes the modal
**/
onDisableDecline() {
this.setState({ isDisableModaleVisible: false });
}
/**
* Removes the friend upon button press
**/
onRemoveAccept() {
const { myFriendId, userId, otherFriendId } = this.props.navigation.state.params;
this.props.socialRemoveFriend({ myFriendId, userId, otherFriendId });
this.setState({ isRemoveModalVisible: false });
this.props.navigation.goBack();
this.props.friendsFetch();
}
/**
* Prevents friend removal and closes modal upon button press
**/
onRemoveDecline() {
this.setState({ isRemoveModalVisible: false });
}
/**
* Gets the current permission state of the app for camera roll/photos
**/
setCameraRollStatus = async () => {
const { status } = await Permissions.getAsync(Permissions.CAMERA_ROLL);
this.setState({ cameraRollStatus: status });
}
/**
* Toggles whether or not the profile is enabled
**/
async toggleProfileEnabled() {
this.props.profileToggleDisabled({ isDisabled: !this.props.isDisabled, uid: this.props.uid });
await this.props.profileUpdate({ prop: 'isDisabled', value: !this.props.isDisabled });
this.populateKebabLastOption();
}
/**
* Takes the chosen image in base64 and saves the base64
**/
processImage = async (uri) => {
if (this.props.formType === CONTACT_FORM) {
this.props.contactUpdate({ prop: 'contactAvatarImg', value: uri });
} else if (this.props.formType === PROFILE_FORM) {
this.props.profileUpdate({ prop: 'profileAvatarImg', value: uri });
} else if (this.props.formType === SOCIAL_FORM) {
this.props.profileUpdate({ prop: 'socialAvatarImg', value: uri });
}
}
/**
* Populates last option of the kebab menu based on disabled or not
**/
populateKebabLastOption() {
profileKebabOptions.pop();
profileKebabOptions.push(this.props.isDisabled ? strings('profile.kebabOptions_enable') : strings('profile.kebabOptions_disable'));
}
/**
* Performs an action based on the option selected from the kebab menu
**/
async processProfileKebabSelection(idx) {
const { profileAvatarImg, fullName, titleAndCompany, tagferId, uid } = this.props;
switch (idx) {
case '0':
this.props.profileUpdate({ prop: 'success', value: false });
this.props.profileUpdate({ prop: 'loading', value: false });
this.props.navigation.navigate('shareProfileQRCodeScreen', { profileAvatarImg, fullName, titleAndCompany, tagferId, uid });
break;
case '1':
this.props.profileUpdate({ prop: 'success', value: false });
this.props.profileUpdate({ prop: 'loading', value: false });
this.shareLink(tagferId, uid);
break;
case '2':
this.props.profileUpdate({ prop: 'success', value: false });
this.props.profileUpdate({ prop: 'loading', value: false });
this.props.navigation.navigate('profileEdit');
break;
case '3':
this.props.profileUpdate({ prop: 'success', value: false });
this.props.profileUpdate({ prop: 'loading', value: false });
this.setState({ isDuplicateModalVisible: true });
break;
case '4':
if (this.props.isDisabled) {
this.toggleProfileEnabled();
} else {
this.setState({ isDisableModaleVisible: true });
}
break;
default:
break;
}
}
/**
* Performs an action based on the option selected from the kebab menu
**/
async processSocialKebabSelection(idx) {
const { profileAvatarImg, fullName, titleAndCompany, otherTagferId, otherUserId, otherProfileId, myFriendId } = this.props.navigation.state.params;
switch (idx) {
case '0':
this.props.profileUpdate({ prop: 'success', value: false });
this.props.profileUpdate({ prop: 'loading', value: false });
this.props.navigation.navigate('shareProfileQRCodeScreen', { profileAvatarImg, fullName, titleAndCompany, otherTagferId, otherUserId });
break;
case '1':
this.props.profileUpdate({ prop: 'success', value: false });
this.props.profileUpdate({ prop: 'loading', value: false });
this.shareLink(otherTagferId, otherUserId);
break;
case '2':
this.props.navigation.navigate('socialMergeScreen', { fullName, titleAndCompany, otherTagferId, otherUserId, myFriendId });
break;
case '3':
this.setState({ isRemoveModalVisible: !this.state.isRemoveModalVisible });
break;
case '4':
MailComposer.composeAsync({
recipients: ['abuse@tagfer.com'],
subject: strings('common.forms.reportContact_emailHeader'),
body: strings('common.forms.reportContact_emailMessage', { otherTagferId, otherProfileId })
});
break;
default:
break;
}
}
/**
* Opens the normal hardware process for sharing something via social, sms, etc
* uid - the uid for the profile to be shared
**/
shareLink(tagferId, uid) {
Share.share(
{
message: strings('common.linking.shareProfile_body', { tagferId, DEV_LINKING_BASE_URL, uid }),
title: strings('common.linking.shareProfile_title'),
url: `${TAGFER_BASE_URL}${tagferId}/${uid}`
},
{
tintColor: 0x056ecf,
excludedActivityTypes: [
'com.apple.UIKit.activity.PostToWeibo',
'com.apple.UIKit.activity.Print',
'com.apple.UIKit.activity.CopyToPasteboard',
'com.apple.UIKit.activity.AssignToContact',
'com.apple.UIKit.activity.SaveToCameraRoll',
'com.apple.UIKit.activity.AddToReadingList',
'com.apple.UIKit.activity.PostToFlickr',
'com.apple.UIKit.activity.PostToVimeo',
'com.apple.UIKit.activity.PostToTencentWeibo',
'com.apple.UIKit.activity.AirDrop',
'com.apple.UIKit.activity.OpenInIBooks',
'com.apple.UIKit.activity.MarkupAsPDF',
'com.apple.reminders.RemindersEditorExtension', //Reminders
'com.apple.mobilenotes.SharingExtension', // Notes
'com.apple.mobileslideshow.StreamShareService', // iCloud Photo Sharing - This also does nothing :{
// Not supported
'com.linkedin.LinkedIn.ShareExtension', //LinkedIn
'pinterest.ShareExtension', //Pinterest
'com.google.GooglePlus.ShareExtension', //Google +
'com.tumblr.tumblr.Share-With-Tumblr', //Tumblr
'wefwef.YammerShare', //Yammer
'com.hootsuite.hootsuite.HootsuiteShareExt', //HootSuite
'net.naan.TwitterFonPro.ShareExtension-Pro', //Echofon
'net.whatsapp.WhatsApp.ShareExtension' //WhatsApp
]
}
);
}
/**
* Renders the avatar cirle on profiles
**/
renderAvatarPhoto() {
let base64 = '';
if (this.props.formType === CONTACT_FORM) {
base64 = this.props.contactAvatarImg;
} else if (this.props.formType === PROFILE_FORM) {
base64 = this.props.profileAvatarImg;
} else if (this.props.formType === SOCIAL_FORM) {
base64 = this.props.socialAvatarImg;
}
if (base64) {
if (this.props.isEditMode) {
return (
<View style={styles.avatarEditStyle}>
<Image
source={{ uri: base64 }}
style={styles.avatarStyle}
/>
{this.renderPhotoButtons()}
</View>
);
}
// Not edit mode but has avatar image
return (
<View>
<Image
source={{ uri: base64 }}
style={styles.avatarStyle}
/>
</View>
);
}
// No profile Image
if (this.props.isEditMode) {
return (
<View style={styles.noAvatarEditStyle}>
<Icon
name='account-circle'
size={150}
color={colors.lightTeal}
/>
{this.renderPhotoButtons()}
</View>
);
}
return (
<View>
<Icon
name='account-circle'
size={150}
color={colors.lightTeal}
/>
</View>
);
}
/**
* Renders the photo buttons
**/
renderPhotoButtons() {
if (this.props.formType === SOCIAL_FORM) {
return <View style={styles.buttonContainerStyle} />;
}
return (
<View style={styles.buttonContainerStyle}>
<TouchableOpacity
onPress={this.onPickImageButtonPress.bind(this)}
disabled={this.props.loading}
>
<Text style={styles.textButtonStyle}>{strings('common.buttons.uploadImage')}</Text>
</TouchableOpacity>
<Text style={styles.photoButtonsOrTextStyle}>{strings('profile.or')}</Text>
<TouchableOpacity
onPress={this.onTakePhotoButtonPress.bind(this)}
disabled={this.props.loading}
>
<Text style={styles.textButtonStyle}>{strings('common.buttons.takePhoto')}</Text>
</TouchableOpacity>
</View>
);
}
/**
* Adds an icon on the right of the profile row if it is the selected profile
**/
renderHightlightedIcon(highlighted) {
return (
<Image
style={styles.highlightedImageStyle}
mode='stretch'
source={highlighted ? GREEN_CHECK_IMAGE : null}
/>
);
}
/**
* Adds an icon to the left of the profile name in the profile row
**/
renderLeftIcon(rowID) {
const base64 = this.props.profiles[rowID].profileAvatarImg;
if (base64) {
return (
<View style={styles.leftIconStyle}>
<Image
source={{ uri: base64 }}
style={styles.avatarSmallImageStyle}
/>
<Text style={[styles.modalDropdownDropdownTextStyle, { color: this.props.profiles[rowID].isDisabled ? colors.lightGrey : colors.black }]}>
{profileNames[rowID]}
</Text>
</View>
);
}
return (
<View style={styles.leftIconStyle}>
<Icon
name={'account-circle'}
size={30}
color={colors.middleGrey}
/>
<Text style={[styles.modalDropdownDropdownTextStyle, { color: this.props.profiles[rowID].isDisabled ? colors.lightGrey : colors.black }]}>
{profileNames[rowID]}
</Text>
</View>
);
}
/**
* Used for custom styling of the modal dropdown
**/
renderDuplicateModalDropdownRow(rowData, rowID, highlighted) {
return (
<View style={{ backgroundColor: this.props.profiles[rowID].isDisabled ? colors.offWhite : colors.white }}>
<View style={styles.modalDropdownDropdownViewStyle}>
{this.renderLeftIcon(rowID)}
{this.renderHightlightedIcon(highlighted)}
</View>
</View>
);
}
/**
* Renders the kebab menu
**/
renderKebab() {
if (!this.props.isEditMode) {
if (this.props.formType === PROFILE_FORM) {
return (
<View style={styles.kebabViewStyle}>
<ModalDropdown
style={styles.modalDropdownStyle}
dropdownStyle={styles.profileModalDropsdownDropdownStyle}
dropdownTextStyle={styles.modalDropsdownDropdownTextStyle}
options={profileKebabOptions}
onSelect={(idx) => this.processProfileKebabSelection(idx)}
>
<Icon
name={'more-vert'}
size={40}
containerStyle={{ paddingLeft: 10, paddingTop: 10 }}
/>
</ModalDropdown>
</View>
);
} else if (this.props.formType === SOCIAL_FORM) {
return (
<View style={styles.kebabViewStyle}>
<ModalDropdown
style={styles.modalDropdownStyle}
dropdownStyle={styles.socialModalDropdownDropdownStyle}
dropdownTextStyle={styles.modalDropsdownDropdownTextStyle}
options={friendKebabOptions}
onSelect={(idx) => this.processSocialKebabSelection(idx)}
>
<Icon
name={'more-vert'}
size={40}
containerStyle={{ paddingLeft: 10, paddingTop: 10 }}
/>
</ModalDropdown>
</View>
);
}
}
}
/**
* Render method
**/
render() {
const name = `${this.props.fullName}`;
return (
<View>
<View style={styles.avatarViewStyle}>
{this.renderAvatarPhoto()}
</View>
{this.renderKebab()}
<Modal
transparent
animationType="slide"
visible={this.state.isDuplicateModalVisible}
onRequestClose={() => {}}
>
<TouchableOpacity
style={styles.modalOpacityStyle}
activeOpacity={1}
onPressOut={() => this.setState({ isDuplicateModalVisible: false })}
>
<ScrollView
directionalLockEnabled
centerContent
contentContainerStyle={styles.modalScrollViewStyle}
>
<TouchableWithoutFeedback>
<View>
<View style={styles.duplicateMainViewStyle}>
<View />
<View>
<Spacer />
<Text style={styles.leftAlignedTextStyle}>{strings('profile.duplicateScreen_duplicateBasicInfo')}</Text>
<Spacer />
<Text style={styles.leftAlignedTextStyle}>{strings('profile.duplicateScreen_duplicateFrom')}</Text>
<Spacer />
<ModalDropdown
style={styles.duplicateModalDropdownStyle}
dropdownStyle={styles.modalDropdownDropdownStyle}
options={profileNames}
renderRow={this.renderDuplicateModalDropdownRow.bind(this)}
onSelect={(idx) => {
copyFromIndex = idx;
}}
/>
</View>
<View>
<Spacer />
<Text style={styles.leftAlignedTextStyle}>{strings('profile.duplicateScreen_duplicateTo')}</Text>
<Spacer />
<ModalDropdown
style={styles.duplicateModalDropdownStyle}
dropdownStyle={styles.modalDropdownDropdownStyle}
options={profileNames}
renderRow={this.renderDuplicateModalDropdownRow.bind(this)}
onSelect={(idx) => {
copyToIndex = idx;
}}
/>
</View>
<Button
onPress={() => this.onDuplicateButtonPress()}
title={strings('common.buttons.duplicate')}
buttonStyle={styles.buttonStyle}
fontWeight="bold"
fontSize={25}
/>
<Spacer />
</View>
</View>
</TouchableWithoutFeedback>
</ScrollView>
</TouchableOpacity>
</Modal>
<Confirm
visible={this.state.isDisableModaleVisible}
onAccept={this.onDisableAccept.bind(this)}
onDecline={this.onDisableDecline.bind(this)}
>
{strings('profile.kebabOptions_disableInfo')}
</Confirm>
<Confirm
visible={this.state.isRemoveModalVisible}
onAccept={this.onRemoveAccept.bind(this)}
onDecline={this.onRemoveDecline.bind(this)}
>
{strings('common.modal.confirm_delete', { name })}
</Confirm>
</View>
);
}
}
const styles = {
buttonContainerStyle: {
flex: 2,
marginTop: 15,
flexDirection: 'column',
justifyContent: 'center'
},
textButtonStyle: {
fontSize: 20,
fontWeight: 'bold',
color: colors.labelBlue,
marginTop: 5,
marginBottom: 5,
marginLeft: 15
},
photoButtonsOrTextStyle: {
fontSize: 20,
fontWeight: 'bold',
color: colors.lightGrey,
marginTop: 5,
marginBottom: 5,
marginLeft: 15
},
kebabViewStyle: {
position: 'absolute'
},
modalDropdownStyle: {
alignSelf: 'center',
},
profileModalDropsdownDropdownStyle: {
width: SCREEN_WIDTH * 0.6,
height: 215,
borderColor: colors.lightgrey,
borderWidth: 1,
marginLeft: 40,
marginTop: -30
},
socialModalDropdownDropdownStyle: {
width: SCREEN_WIDTH * 0.6,
height: 210,
borderColor: colors.lightgrey,
borderWidth: 1,
marginLeft: 40,
marginTop: -30
},
modalDropsdownDropdownTextStyle: {
fontSize: 18
},
modalTextStyle: {
flex: 1,
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
lineHeight: 40
},
avatarViewStyle: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginTop: 10
},
avatarEditStyle: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginLeft: 25
},
noAvatarEditStyle: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginLeft: 10
},
avatarStyle: {
width: 125,
height: 125,
borderWidth: 1,
borderRadius: 60,
marginTop: 15
},
modalOpacityStyle: {
backgroundColor: colors.modalTransparency,
position: 'relative',
flex: 1,
justifyContent: 'center'
},
modalScrollViewStyle: {
backgroundColor: colors.white,
},
modalCardStyle: {
justifyContent: 'center'
},
duplicateMainViewStyle: {
backgroundColor: colors.white,
flex: 1,
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'flex-start',
paddingRight: 10
},
leftAlignedTextStyle: {
textAlign: 'left',
marginLeft: 20,
marginRight: 20,
fontSize: 15
},
duplicateModalDropdownStyle: {
marginLeft: 20
},
modalDropdownDropdownViewStyle: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
height: 40,
paddingLeft: 10
},
leftIconStyle: {
flex: 1,
flexDirection: 'row',
alignItems: 'flex-start'
},
modalDropdownDropdownStyle: {
width: SCREEN_WIDTH,
borderColor: colors.lightGrey,
borderWidth: 1,
marginLeft: -10
},
modalDropdownDropdownTextStyle: {
fontSize: 18,
marginLeft: 10
},
highlightedImageStyle: {
width: 30,
height: 30,
borderRadius: 15,
marginRight: 10
},
avatarSmallImageStyle: {
width: 30,
height: 30,
borderWidth: 1,
borderRadius: 15
},
buttonStyle: {
width: SCREEN_WIDTH * 0.9,
marginTop: 15,
marginBottom: 30,
borderRadius: 5,
backgroundColor: colors.buttonBlue
}
};
/**
* Map State To Props
**/
const mapStateToProps = (state) => {
// converts Profile objects into profile arrays
const profiles = _.map(state.profiles, (val, uid) => {
return { ...val, uid };
});
const {
contactAvatarImg,
} = state.contactForm;
const {
socialAvatarImg
} = state.socialContacts;
const {
isDisabled,
profileAvatarImg,
uid
} = state.profileForm;
return {
profiles,
contactAvatarImg,
socialAvatarImg,
isDisabled,
profileAvatarImg,
uid
};
};
export default connect(mapStateToProps, {
contactUpdate, profileToggleDisabled, profileSave, profileUpdate, socialRemoveFriend
})(ProfileAvatar);

View File

@ -1,6 +0,0 @@
/** Contains all string key/value pairs to prevent typos **/
export const EVENT_TRANSACTION = 'event';
export const EARNED_TRANSACTION = 'earned';
export const RECEIVED_TRANSACTION = 'received';
export const REQUESTED_TRANSACTION = 'requested';
export const SENT_TRANSACTION = 'sent';

View File

@ -1,57 +0,0 @@
/** Used for handling logging out a user **/
import React, { Component } from 'react';
import { AsyncStorage } from 'react-native';
import { NavigationActions } from 'react-navigation';
import { purgeStoredState } from 'redux-persist';
import firebase from 'firebase';
import { connect } from 'react-redux';
import { Spinner } from '../components/common';
import { profileListReset, propertiesReset, userFormReset } from '../actions';
import { strings } from '../locales/i18n';
class UserLogout extends Component {
static navigationOptions = {
title: strings('logout.title'),
}
/**
* Component Will Mount
**/
async componentWillMount() {
firebase.auth().signOut();
this.props.userFormReset();
this.props.propertiesReset();
this.props.profileListReset();
purgeStoredState({ storage: AsyncStorage });
this.navigateToLoginScreen();
}
/**
* Navigates to login screen and resets the navigation so no back option
**/
navigateToLoginScreen() {
const resetAction = NavigationActions.reset({
index: 0,
actions: [
NavigationActions.navigate({ routeName: 'userLogin' })
]
});
this.props.navigation.dispatch(resetAction);
}
/**
* Render method
**/
render() {
return (
<Spinner />
);
}
}
export default connect(null, {
profileListReset,
propertiesReset,
userFormReset
})(UserLogout);

View File

@ -1,498 +0,0 @@
/** Contains the base fields for a contact reuse **/
import React, { Component } from 'react';
import { Animated, Dimensions, Platform, ScrollView, View } from 'react-native';
import { Divider, FormInput } from 'react-native-elements';
import { connect } from 'react-redux';
import { contactFormReset, contactUpdate } from '../../actions';
import BizCard from '../BizCard';
import ProfileAvatar from '../ProfileAvatar';
import { FormMessage, FormLabel, Spacer } from '../common';
import { CONTACT_FORM } from '../FormTypes';
import colors from '../../config/colors.json';
import { strings } from '../../locales/i18n';
const SCREEN_WIDTH = Dimensions.get('window').width;
const DOTS_SIZE = ['first', 'second'];
const scrollX = new Animated.Value(0);
class ContactForm extends Component {
/**
* Component Will Unmount
**/
componentWillUnmount() {
this.props.contactFormReset();
}
/**
* Breaks the fullName into a first and last name for contact saving
**/
splitNameField(value) {
this.props.contactUpdate({ prop: 'fullName', value });
const nameArr = this.props.fullName.split(' ');
const firstName = nameArr[0];
this.props.contactUpdate({ prop: 'firstName', value: firstName });
if (nameArr.length > 0) {
const lastName = nameArr[1];
this.props.contactUpdate({ prop: 'lastName', value: lastName });
}
}
/**
* Renders the avatar and biz card slider
**/
renderAvatarBizCardSlider() {
const position = Animated.divide(scrollX, SCREEN_WIDTH);
return (
<View>
<ScrollView
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { x: scrollX } } }]
)}
scrollEventThrottle={16}
>
<View style={styles.slideStyle}>
<View style={{ flex: 1, justifyContent: 'center' }}>
<ProfileAvatar
{...this.props}
formType={CONTACT_FORM}
isEditMode
/>
</View>
</View>
<View style={styles.slideStyle}>
<BizCard
formType={CONTACT_FORM}
isEditMode
isScanBizCardScreen={this.props.isScanBizCardScreen}
/>
</View>
</ScrollView>
<View style={styles.dotsStyle}>
{DOTS_SIZE.map((_, i) => {
const opacity = position.interpolate({
inputRange: [i - 1, i, i + 1],
outputRange: [0.3, 1, 0.3],
extrapolate: 'clamp'
});
return (
<Animated.View
key={i}
style={{ opacity, height: 10, width: 10, backgroundColor: '#595959', margin: 8, borderRadius: 5 }}
/>
);
})}
</View>
{this.renderDetailsSection()}
</View>
);
}
/**
* Renders the top section under the avatar
**/
renderDetailsSection() {
return (
<View>
<Divider style={styles.dividerStyle} />
<Spacer />
<FormLabel bold labelText={strings('common.contactFields.fullName_labelText')} />
<FormInput
value={this.props.fullName}
placeholder={strings('common.contactFields.fullName_placeholder')}
onChangeText={value => this.splitNameField(value)}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.inputTextStyle}
/>
<Spacer />
<FormLabel bold labelText={strings('common.contactFields.headline_labelText')} />
<FormInput
value={this.props.headline}
placeholder={strings('common.contactFields.headline_placeholder')}
onChangeText={value => this.props.contactUpdate({ prop: 'headline', value })}
autoCorrect={false}
autoCapitalize='none'
multiline
numberOfLines={4}
inputStyle={styles.inputTextStyle}
/>
<Spacer />
<FormLabel bold labelText={strings('common.contactFields.titleAndCompany_labelText')} />
<FormInput
value={this.props.titleAndCompany}
placeholder={strings('common.contactFields.titleAndCompany_placeholder')}
onChangeText={value => this.props.contactUpdate({ prop: 'titleAndCompany', value })}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.inputTextStyle}
/>
<Spacer />
<FormLabel bold labelText={strings('common.contactFields.location_labelText')} />
<FormInput
value={this.props.location}
placeholder={strings('common.contactFields.location_placeholder')}
onChangeText={value => this.props.contactUpdate({ prop: 'location', value })}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.inputTextStyle}
/>
</View>
);
}
/**
* Renders the primary phone section
**/
renderPrimaryPhoneSection() {
return (
<View>
<Spacer />
<FormLabel bold labelText={strings('common.contactFields.phone_primary_labelText')} />
<FormInput
value={this.props.primaryPhone}
placeholder={strings('common.contactFields.phone_placeholder')}
onChangeText={value => this.props.contactUpdate({ prop: 'primaryPhone', value })}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.formInputStyle}
/>
</View>
);
}
/**
* Renders the secondary phone section
**/
renderSecondaryPhoneSection() {
return (
<View>
<Spacer />
<FormLabel bold labelText={strings('common.contactFields.phone_secondary_labelText')} />
<FormInput
value={this.props.secondaryPhone}
placeholder={strings('common.contactFields.phone_placeholder')}
onChangeText={value => this.props.contactUpdate({ prop: 'secondaryPhone', value })}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.formInputStyle}
/>
</View>
);
}
/**
* Renders the work phone section
**/
renderWorkPhoneSection() {
return (
<View>
<Spacer />
<FormLabel bold labelText={strings('common.contactFields.phone_work_labelText')} />
<FormInput
value={this.props.workPhone}
placeholder={strings('common.contactFields.phone_placeholder')}
onChangeText={value => this.props.contactUpdate({ prop: 'workPhone', value })}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.formInputStyle}
/>
<Spacer />
<FormLabel bold labelText={strings('common.contactFields.extension_labelText')} />
<FormInput
value={this.props.extension}
placeholder={strings('common.contactFields.extension_placeholder')}
onChangeText={value => this.props.contactUpdate({ prop: 'extension', value })}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.formInputStyle}
/>
</View>
);
}
/**
* Renders the fax phone section
**/
renderFaxSection() {
return (
<View>
<Spacer />
<FormLabel bold labelText={strings('common.contactFields.fax_labelText')} />
<FormInput
value={this.props.fax}
placeholder={strings('common.contactFields.phone_placeholder')}
onChangeText={value => this.props.contactUpdate({ prop: 'fax', value })}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.formInputStyle}
/>
</View>
);
}
/**
* Renders the email section
**/
renderEmailSection() {
return (
<View>
<Spacer />
<FormLabel bold labelText={strings('common.contactFields.email_labelText')} />
<FormInput
value={this.props.contactEmail}
placeholder={strings('common.contactFields.email_placeholder')}
onChangeText={value => this.props.contactUpdate({ prop: 'contactEmail', value })}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.formInputStyle}
/>
</View>
);
}
/**
* Renders the website section
**/
renderWebsiteSection() {
return (
<View>
<Spacer />
<FormLabel bold labelText={strings('common.contactFields.website_labelText')} />
<FormInput
value={this.props.website}
placeholder={strings('common.contactFields.website_placeholder')}
onChangeText={value => this.props.contactUpdate({ prop: 'website', value })}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.formInputStyle}
/>
</View>
);
}
/**
* Renders the address section
**/
renderAddressSection() {
return (
<View>
<Spacer />
<FormLabel bold labelText={strings('common.contactFields.address_labelText')} />
<FormInput
value={this.props.street}
placeholder={strings('common.contactFields.address_placeholder_street')}
onChangeText={value => this.props.contactUpdate({ prop: 'street', value })}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.formInputStyle}
/>
<FormInput
value={this.props.address2}
placeholder={strings('common.contactFields.address_placeholder_address2')}
onChangeText={value => this.props.contactUpdate({ prop: 'address2', value })}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.formInputStyle}
/>
<FormInput
value={this.props.city}
placeholder={strings('common.contactFields.address_placeholder_city')}
onChangeText={value => this.props.contactUpdate({ prop: 'city', value })}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.formInputStyle}
/>
<FormInput
value={this.props.region}
placeholder={strings('common.contactFields.address_placeholder_region')}
onChangeText={value => this.props.contactUpdate({ prop: 'region', value })}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.formInputStyle}
/>
<FormInput
value={this.props.postalCode}
placeholder={strings('common.contactFields.address_placeholder_postalCode')}
onChangeText={value => this.props.contactUpdate({ prop: 'postalCode', value })}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.formInputStyle}
/>
<FormInput
value={this.props.country}
placeholder={strings('common.contactFields.address_placeholder_country')}
onChangeText={value => this.props.contactUpdate({ prop: 'country', value })}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.formInputStyle}
/>
</View>
);
}
/**
* Render method
**/
render() {
return (
<View>
<View>
{this.renderAvatarBizCardSlider()}
</View>
{this.renderPrimaryPhoneSection()}
{this.renderSecondaryPhoneSection()}
{this.renderWorkPhoneSection()}
{this.renderFaxSection()}
{this.renderEmailSection()}
{this.renderWebsiteSection()}
{this.renderAddressSection()}
<Spacer />
<FormLabel labelText={strings('common.contactFields.notes_labelText')} />
<FormInput
value={this.props.notes}
placeholder={strings('common.contactFields.notes_placeholder')}
onChangeText={value => this.props.contactUpdate({ prop: 'notes', value })}
autoCorrect={false}
autoCapitalize='none'
multiline
numberOfLines={4}
containerStyle={{ minHeight: 200 }}
inputStyle={styles.textBoxStyles}
/>
<FormMessage error={this.props.error} />
</View>
);
}
}
const styles = {
slideStyle: {
width: SCREEN_WIDTH,
},
dividerStyle: {
backgroundColor: colors.middleGrey,
marginTop: 10,
marginBottom: 15
},
dotsStyle: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center'
},
centeredInputContainerStyle: {
borderBottomWidth: 0,
marginLeft: 15,
marginRight: 0
},
centeredInputTextStyle: {
textAlign: 'center',
color: colors.black
},
inputTextStyle: {
width: '100%',
color: colors.black
},
centeredTextBoxStyles: {
textAlign: 'center',
flex: 1,
width: '100%',
color: colors.black
},
nameTextStyle: {
fontSize: 22,
fontWeight: 'bold'
},
textBoxStyles: {
flex: 1,
width: '100%',
borderBottomWidth: Platform.OS === 'android' ? 1 : 0,
borderColor: colors.middleGrey,
color: colors.black
},
formInputStyle: {
color: colors.black
}
};
/**
* If you add any items here be sure to update them in the ContactCreateScreen,
* ContactEditScreen, ContactImportScreen, ContactActions, and ContactFormReducer
**/
const mapStateToProps = (state) => {
const {
contactBizCardImg,
contactAvatarImg,
firstName,
lastName,
fullName,
headline,
location,
titleAndCompany,
primaryPhone,
secondaryPhone,
workPhone,
extension,
fax,
contactEmail,
website,
street,
address2,
city,
region,
postalCode,
country,
notes,
contactCreateDate,
error
} = state.contactForm;
return {
contactBizCardImg,
contactAvatarImg,
firstName,
lastName,
fullName,
headline,
location,
titleAndCompany,
primaryPhone,
secondaryPhone,
workPhone,
extension,
fax,
contactEmail,
website,
street,
address2,
city,
region,
postalCode,
country,
notes,
contactCreateDate,
error
};
};
export default connect(mapStateToProps, {
contactFormReset, contactUpdate
})(ContactForm);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,714 +0,0 @@
/** Contains the base fields for Social Display **/
import React, { Component } from 'react';
import { Animated, Dimensions, Linking, ScrollView, Share, View } from 'react-native';
import { Card, Divider, Icon } from 'react-native-elements';
import { connect } from 'react-redux';
import { socialFormReset } from '../../actions';
import ProfileAvatar from '../ProfileAvatar';
import BizCard from '../BizCard';
import { FormLabel, Keyword, Spacer } from '../common';
import { SOCIAL_FORM } from '../FormTypes';
import colors from '../../config/colors.json';
import privateKeys from '../../../private_keys.json';
import { strings } from '../../locales/i18n';
const SCREEN_WIDTH = Dimensions.get('window').width;
const DOTS_SIZE = ['first', 'second'];
const DEV_LINKING_BASE_URL = privateKeys.deepLinking.devBaseUrl;
const TAGFER_BASE_URL = privateKeys.deepLinking.tagferBaseUrl;
const scrollX = new Animated.Value(0);
class SocialForm extends Component {
/**
* Component Will Unmount
**/
componentWillUnmount() {
this.props.socialFormReset();
}
/**
* Opens the normal hardware process for sharing something via social, sms, etc
* uid - the uid for the profile to be shared
**/
shareLink(tagferId, uid) {
Share.share(
{
message: `Add me on Tagfer! ${DEV_LINKING_BASE_URL}${tagferId}/${uid}`,
title: 'Add me on Tagfer!',
url: `${TAGFER_BASE_URL}${tagferId}/${uid}`
},
{
tintColor: 0x056ecf,
excludedActivityTypes: [
'com.apple.UIKit.activity.PostToWeibo',
'com.apple.UIKit.activity.Print',
'com.apple.UIKit.activity.CopyToPasteboard',
'com.apple.UIKit.activity.AssignToContact',
'com.apple.UIKit.activity.SaveToCameraRoll',
'com.apple.UIKit.activity.AddToReadingList',
'com.apple.UIKit.activity.PostToFlickr',
'com.apple.UIKit.activity.PostToVimeo',
'com.apple.UIKit.activity.PostToTencentWeibo',
'com.apple.UIKit.activity.AirDrop',
'com.apple.UIKit.activity.OpenInIBooks',
'com.apple.UIKit.activity.MarkupAsPDF',
'com.apple.reminders.RemindersEditorExtension', //Reminders
'com.apple.mobilenotes.SharingExtension', // Notes
'com.apple.mobileslideshow.StreamShareService', // iCloud Photo Sharing - This also does nothing :{
// Not supported
'com.linkedin.LinkedIn.ShareExtension', //LinkedIn
'pinterest.ShareExtension', //Pinterest
'com.google.GooglePlus.ShareExtension', //Google +
'com.tumblr.tumblr.Share-With-Tumblr', //Tumblr
'wefwef.YammerShare', //Yammer
'com.hootsuite.hootsuite.HootsuiteShareExt', //HootSuite
'net.naan.TwitterFonPro.ShareExtension-Pro', //Echofon
'net.whatsapp.WhatsApp.ShareExtension' //WhatsApp
]
}
);
}
/**
* Renders the avatar and biz card slider
**/
renderAvatarBizCardSlider() {
const position = Animated.divide(scrollX, SCREEN_WIDTH);
if (this.props.isAddMode) {
return (
<View>
<Spacer />
{this.renderDetailsSection()}
<Spacer />
</View>
);
}
return (
<View>
<ScrollView
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { x: scrollX } } }]
)}
scrollEventThrottle={16}
>
<View style={styles.slideStyle}>
<ProfileAvatar
{...this.props}
formType={SOCIAL_FORM}
isEditMode={this.props.isEditMode}
/>
{this.renderDetailsSection()}
</View>
<View style={styles.slideStyle}>
<BizCard
formType={SOCIAL_FORM}
isEditMode={this.props.isEditMode}
/>
</View>
</ScrollView>
<View style={styles.dotsStyle}>
{DOTS_SIZE.map((_, i) => {
const opacity = position.interpolate({
inputRange: [i - 1, i, i + 1],
outputRange: [0.3, 1, 0.3],
extrapolate: 'clamp'
});
return (
<Animated.View
key={i}
style={{ opacity, height: 10, width: 10, backgroundColor: '#595959', margin: 8, borderRadius: 5 }}
/>
);
})}
</View>
{this.renderConnectionButtons()}
</View>
);
}
/**
* Renders the top section under the avatar
**/
renderDetailsSection() {
if (this.props.isAddMode) {
return (
<View style={{ flex: 1, flexDirection: 'column', alignItems: 'center', marginLeft: -10 }}>
<FormLabel bold labelText={`@${this.props.navigation.state.params.tagferId}`} textStyle={{ color: colors.darkGreen, fontSize: 18 }} />
<FormLabel bold labelText={this.props.fullName} textStyle={{ fontSize: 22 }} />
<FormLabel bold labelText={this.props.headline} textStyle={{ fontSize: 15 }} />
<FormLabel bold labelText={this.props.titleAndCompany} textStyle={{ fontSize: 15 }} />
<FormLabel bold labelText={this.props.location} textStyle={{ fontSize: 15 }} />
</View>
);
}
return (
<View style={{ flex: 1, flexDirection: 'column', alignItems: 'center', marginLeft: -10 }}>
<FormLabel bold labelText={`@${this.props.navigation.state.params.otherTagferId}`} textStyle={{ color: colors.darkGreen, fontSize: 18 }} />
<FormLabel bold labelText={this.props.fullName} textStyle={{ fontSize: 22 }} />
<FormLabel bold labelText={this.props.headline} textStyle={{ fontSize: 15 }} />
<FormLabel bold labelText={this.props.titleAndCompany} textStyle={{ fontSize: 15 }} />
<FormLabel bold labelText={this.props.location} textStyle={{ fontSize: 15 }} />
</View>
);
}
/**
* Renders the various buttons for this connection
**/
renderConnectionButtons() {
if (this.props.isAddMode) {
return <Divider style={styles.dividerStyle} />;
}
const { otherUserId, myFriendId, otherFriendId } = this.props.navigation.state.params;
// NORMAL MODE
return (
<View>
<Divider style={styles.dividerStyle} />
<View style={styles.iconViewContainerStyle}>
<Icon
name={'phone'}
type="simple-line-icon"
size={30}
color={colors.darkGreen}
containerStyle={styles.iconStyle}
onPress={() => Linking.openURL(`tel:+${this.props.primaryPhone}`)}
/>
<Icon
name={'action-redo'}
type="simple-line-icon"
size={30}
color={colors.buttonBlue}
containerStyle={styles.iconStyle}
onPress={() => this.shareLink(this.props.tagferId, this.props.userId)}
/>
<Icon
name={'speech'}
type="simple-line-icon"
size={30}
color={colors.darkBlue}
containerStyle={styles.iconStyle}
onPress={() => this.props.navigation.navigate('messageScreen', { otherUserId, myFriendId, otherFriendId })}
/>
<Icon
name={'note'}
type="simple-line-icon"
size={30}
color={colors.orange}
containerStyle={styles.iconStyle}
onPress={() => this.props.navigation.navigate('socialNotesScreen', { friendId: myFriendId })}
/>
</View>
<Divider style={styles.dividerStyle} />
</View>
);
}
/**
* Renders the About section
**/
renderAboutSection() {
if (this.props.isAddMode) {
return;
}
// NORMAL MODE
return (
<Card>
<FormLabel bold labelText={strings('common.contactFields.about_labelText')} />
<FormLabel labelText={this.props.about} />
<FormLabel bold labelText={strings('common.contactFields.howIHelp_labelText_social')} />
<FormLabel labelText={this.props.howIHelp} />
<FormLabel bold labelText={strings('common.contactFields.whatINeed_labelText_social')} />
<FormLabel labelText={this.props.whatINeed} />
</Card>
);
}
/**
* Renders the Experience section
**/
renderExperienceSection() {
if (this.props.isAddMode) {
return;
}
// NORMAL MODE
let isAddedExperienceEmpty = true;
for (let i = 0; i < this.props.education.length; i++) {
if (!isAddedExperienceEmpty) {
break;
}
for (const property in this.props.education[i]) {
if (this.props.education[i][property].length > 0) {
isAddedExperienceEmpty = false;
break;
}
}
}
if (!isAddedExperienceEmpty) {
const experienceList = this.props.education.map((data, index) => {
if (index > 0) {
return (
<View key={index}>
<Divider style={styles.dividerStyle} />
<FormLabel labelText={data.jobTitle} />
<FormLabel labelText={data.jobCompany} />
<FormLabel labelText={data.jobDates} />
<FormLabel labelText={data.jobLocation} />
<FormLabel labelText={data.jobSummary} />
</View>
);
}
return (
<View key={index}>
<FormLabel labelText={data.jobTitle} />
<FormLabel labelText={data.jobCompany} />
<FormLabel labelText={data.jobDates} />
<FormLabel labelText={data.jobLocation} />
<FormLabel labelText={data.jobSummary} />
</View>
);
});
return (
<Card>
<FormLabel bold labelText={strings('common.contactFields.experience_labelText')} />
{experienceList}
</Card>
);
}
}
/**
* Renders the Education section
**/
renderEducationSection() {
if (this.props.isAddMode) {
return;
}
// NORMAL MODE
let isAddedEducationEmpty = true;
for (let i = 0; i < this.props.education.length; i++) {
if (!isAddedEducationEmpty) {
break;
}
for (const property in this.props.education[i]) {
if (this.props.education[i][property].length > 0) {
isAddedEducationEmpty = false;
break;
}
}
}
if (!isAddedEducationEmpty) {
const educationList = this.props.education.map((data, index) => {
if (index > 0) {
return (
<View key={index}>
<Divider style={styles.dividerStyle} />
<FormLabel labelText={data.school} />
<FormLabel labelText={data.degree} />
<FormLabel labelText={data.fieldOfStudy} />
<FormLabel labelText={data.schoolDates} />
<FormLabel labelText={data.educationSummary} />
</View>
);
}
return (
<View key={index}>
<FormLabel labelText={data.school} />
<FormLabel labelText={data.degree} />
<FormLabel labelText={data.fieldOfStudy} />
<FormLabel labelText={data.schoolDates} />
<FormLabel labelText={data.educationSummary} />
</View>
);
});
return (
<Card>
<FormLabel bold labelText={strings('common.contactFields.education_labelText')} />
{educationList}
</Card>
);
}
}
/**
* Renders the primary phone section
**/
renderPrimaryPhoneSection() {
if (this.props.isAddMode) {
return;
}
// NORMAL MODE
const { primaryPhone } = this.props;
if (!primaryPhone) {
return;
}
return (
<Card>
<FormLabel bold labelText={strings('common.contactFields.phone_primary_labelText')} />
<FormLabel labelText={this.props.primaryPhone} />
</Card>
);
}
/**
* Renders the secondary phone section
**/
renderSecondaryPhoneSection() {
if (this.props.isAddMode) {
return;
}
// NORMAL MODE
const { secondaryPhone } = this.props;
if (!secondaryPhone) {
return;
}
return (
<Card>
<FormLabel bold labelText={strings('common.contactFields.phone_secondary_labelText')} />
<FormLabel labelText={this.props.secondaryPhone} />
</Card>
);
}
/**
* Renders the work phone section
**/
renderWorkPhoneSection() {
if (this.props.isAddMode) {
return;
}
// NORMAL MODE
const { workPhone, extension } = this.props;
if (!workPhone && !extension) {
return;
}
return (
<Card>
<FormLabel bold labelText={strings('common.contactFields.phone_work_labelText')} />
<FormLabel labelText={this.props.workPhone} />
<FormLabel bold labelText={strings('common.contactFields.extension_labelText')} />
<FormLabel labelText={this.props.extension} />
</Card>
);
}
/**
* Renders the fax phone section
**/
renderFaxSection() {
if (this.props.isAddMode) {
return;
}
// NORMAL MODE
const { fax } = this.props;
if (!fax) {
return;
}
return (
<Card>
<FormLabel bold labelText={strings('common.contactFields.fax_labelText')} />
<FormLabel labelText={this.props.fax} />
</Card>
);
}
/**
* Renders the email section
**/
renderEmailSection() {
if (this.props.isAddMode) {
return;
}
// NORMAL MODE
const { profileEmail } = this.props;
if (!profileEmail) {
return;
}
return (
<Card>
<FormLabel bold labelText={strings('common.contactFields.email_labelText')} />
<FormLabel labelText={this.props.profileEmail} />
</Card>
);
}
/**
* Renders the website section
**/
renderWebsiteSection() {
if (this.props.isAddMode) {
return;
}
// NORMAL MODE
const { website } = this.props;
if (!website) {
return;
}
return (
<Card>
<FormLabel bold labelText={strings('common.contactFields.website_labelText')} />
<FormLabel labelText={this.props.website} />
</Card>
);
}
/**
* Renders the address section
**/
renderAddressSection() {
if (this.props.isAddMode) {
return;
}
// NORMAL MODE
const { street, address2, city, region, postalCode, country } = this.props;
if (!street && !address2 && !city && !region && !postalCode && !country) {
return;
}
return (
<Card>
<FormLabel bold labelText={strings('common.contactFields.address_labelText')} />
<FormLabel labelText={this.props.street} />
<FormLabel labelText={this.props.address2} />
<FormLabel labelText={this.props.city} />
<FormLabel labelText={this.props.region} />
<FormLabel labelText={this.props.postalCode} />
<FormLabel labelText={this.props.country} />
</Card>
);
}
/**
* Renders the keyword section
**/
renderKeywordSection() {
if (!this.props.navigation.state.params.keywords || this.props.navigation.state.params.keywords.length === 0) {
return;
}
return (
<Card>
<View style={styles.keywordViewStyle}>
{this.renderKeywords()}
</View>
</Card>
);
}
/**
* Renders each keyword onto the screen
**/
renderKeywords() {
if (!this.props.keywords || !this.props.navigation.state.params.keywords) {
return;
}
let keywordArray = [];
if (this.props.isAddMode) {
keywordArray = this.props.navigation.state.params.keywords.map((data, index) => {
return (
<Keyword
key={index}
title={data}
index={index}
/>
);
});
} else {
keywordArray = Object.values(this.props.keywords).map((data, index) => {
return (
<Keyword
key={index}
title={data}
index={index}
/>
);
});
}
return keywordArray;
}
/**
* Render method
**/
render() {
return (
<View>
<View style={styles.whiteBackground}>
{this.renderAvatarBizCardSlider()}
</View>
{this.renderKeywordSection()}
{this.renderAboutSection()}
{this.renderExperienceSection()}
{this.renderEducationSection()}
{this.renderPrimaryPhoneSection()}
{this.renderSecondaryPhoneSection()}
{this.renderWorkPhoneSection()}
{this.renderFaxSection()}
{this.renderEmailSection()}
{this.renderWebsiteSection()}
{this.renderAddressSection()}
</View>
);
}
}
const styles = {
slideStyle: {
width: SCREEN_WIDTH,
},
whiteBackground: {
backgroundColor: colors.white
},
dotsStyle: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
backgroundColor: colors.white
},
dividerStyle: {
backgroundColor: colors.offWhite,
marginTop: 10,
marginBottom: 15
},
iconViewContainerStyle: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center'
},
iconStyle: {
marginLeft: 20,
marginRight: 20
},
keywordViewStyle: {
flex: 1,
flexDirection: 'row',
flexWrap: 'wrap'
}
};
/**
* If you add any items here be sure to update the lists in
* SocialAddScreen, SocialEditScreen, SocialActions, and SocialReducer
**/
const mapStateToProps = (state) => {
const {
socialBizCardImg,
socialAvatarImg,
profileName,
titleAndCompany,
firstName,
lastName,
fullName,
headline,
location,
profileEmail,
primaryPhone,
secondaryPhone,
extension,
about,
howIHelp,
whatINeed,
experience,
education,
workPhone,
fax,
website,
street,
address2,
city,
region,
postalCode,
country,
keywords,
referralId,
userId,
profileId,
friendId,
tagferId,
loading,
error,
success
} = state.socialContacts;
return {
socialBizCardImg,
socialAvatarImg,
profileName,
titleAndCompany,
firstName,
lastName,
fullName,
headline,
location,
profileEmail,
primaryPhone,
secondaryPhone,
extension,
about,
howIHelp,
whatINeed,
experience,
education,
workPhone,
fax,
website,
street,
address2,
city,
region,
postalCode,
country,
keywords,
referralId,
userId,
profileId,
friendId,
tagferId,
loading,
error,
success
};
};
export default connect(mapStateToProps, {
socialFormReset
})(SocialForm);

View File

@ -1,85 +0,0 @@
/** Contains the base fields for Task reuse **/
import React, { Component } from 'react';
import { Platform, View } from 'react-native';
import { FormInput } from 'react-native-elements';
import { connect } from 'react-redux';
import { FormMessage, FormLabel } from '../common/';
import { taskFormReset, taskUpdate } from '../../actions';
import colors from '../../config/colors.json';
import { strings } from '../../locales/i18n';
class TaskForm extends Component {
/**
* Component Will Unmount
**/
componentWillUnmount() {
this.props.taskFormReset();
}
/**
* Render method
**/
render() {
return (
<View>
<FormLabel required labelText={strings('forms.task.taskName_labelText')} />
<FormInput
value={this.props.taskName}
placeholder={strings('forms.task.taskName_placeholder')}
onChangeText={value => this.props.taskUpdate({ prop: 'taskName', value })}
autoCorrect={false}
autoCapitalize='none'
inputStyle={styles.inputTextStyle}
/>
<FormLabel required labelText={strings('forms.task.taskDescription_labelText')} />
<FormInput
value={this.props.taskDescription}
placeholder={strings('forms.task.taskDescription_placeholder')}
onChangeText={value => this.props.taskUpdate({ prop: 'taskDescription', value })}
autoCorrect={false}
autoCapitalize='none'
multiline
numberOfLines={4}
inputStyle={styles.textBoxStyles}
/>
<FormMessage error={this.props.error} />
</View>
);
}
}
const styles = {
textBoxStyles: {
flex: 1,
width: '100%',
borderBottomWidth: Platform.OS === 'android' ? 1 : 0,
borderColor: colors.middleGrey,
color: colors.black
},
inputTextStyle: {
color: colors.black
}
};
// If you add any items here be sure to update the lists in
// TaskEditScreen, TaskCreateScreen, TaskActions, and TaskFormReducer
const mapStateToProps = (state) => {
const {
taskName,
taskDescription,
taskCreateDate,
error
} = state.taskForm;
return {
taskName,
taskDescription,
taskCreateDate,
error
};
};
export default connect(mapStateToProps, {
taskFormReset, taskUpdate
})(TaskForm);

View File

@ -1,87 +0,0 @@
/** Pure component for each Co-Host Item **/
import React from 'react';
import { Image, Text, View } from 'react-native';
import { CheckBox, ListItem } from 'react-native-elements';
import colors from '../../config/colors.json';
class CoHostListItem extends React.PureComponent {
/**
* Creates the contact avatar
**/
renderAvatar({ item }) {
return (
<Image
source={{ uri: item.profileAvatarImg }}
style={styles.avatarImageStyle}
/>
);
}
/**
* Render method
**/
render() {
const { isChecked, item, onPress } = this.props;
const title = `@${item.tagferId}`;
const subTitle = (
<View>
<Text>{item.fullName || ''}</Text>
<Text>{item.titleAndCompany || ''}</Text>
</View>
);
if (item.profileAvatarImg) {
return (
<ListItem
roundAvatar
key={item.uid}
title={title}
subtitle={subTitle}
titleStyle={{ fontWeight: 'bold', marginBottom: 5, color: colors.darkGreen }}
leftIcon={this.renderAvatar({ item })}
onPress={onPress}
rightIcon={
<CheckBox
checked={isChecked}
containerStyle={{ backgroundColor: colors.transparent, borderWidth: 0 }}
onPress={onPress}
/>
}
/>
);
}
return (
<ListItem
roundAvatar
key={item.key}
title={title}
subtitle={subTitle}
titleStyle={{ fontWeight: 'bold', marginBottom: 5, color: colors.darkGreen }}
leftIcon={{ name: 'account-circle', size: 50, color: colors.lightGrey }}
onPress={onPress}
rightIcon={
<CheckBox
checked={isChecked}
containerStyle={{ backgroundColor: colors.transparent, borderWidth: 0 }}
onPress={onPress}
/>
}
/>
);
}
}
const styles = {
avatarImageStyle: {
width: 40,
height: 40,
borderWidth: 1,
borderRadius: 20,
marginLeft: 5,
marginRight: 10
}
};
export default CoHostListItem;

View File

@ -1,117 +0,0 @@
/** Pure component for each Contact Item **/
import React from 'react';
import { Image, Text, View } from 'react-native';
import { Icon, ListItem } from 'react-native-elements';
import colors from '../../config/colors.json';
class ContactItem extends React.PureComponent {
/**
* When a contact is selected it goes to the edit contact screen
**/
onContactSelect(rowData) {
this.props.navigation.navigate('contactEdit', rowData);
}
/**
* When a friend is selected it goes to the edit friend screen
**/
onFriendSelect(rowData) {
this.props.navigation.navigate('socialEditScreen', rowData);
}
/**
* Creates the contact avatar
**/
renderAvatar({ item }) {
if (item.otherProfileId) {
if (item.profileAvatarImg) {
return (
<Image
source={{ uri: item.profileAvatarImg }}
style={styles.avatarImageStyle}
/>
);
}
return (
<Icon
name='account-circle'
size={50}
color={colors.lightGrey}
/>
);
}
// Phone contact
if (item.contactAvatarImg) {
return (
<Image
source={{ uri: item.contactAvatarImg }}
style={styles.avatarImageStyle}
/>
);
}
return (
<Icon
name='account-circle'
size={50}
color={colors.lightGrey}
/>
);
}
/**
* Render method
**/
render() {
const item = this.props.item;
let title = '';
let subTitle = '';
if (item.otherProfileId) {
title = `@${item.tagferId}`;
subTitle = (
<View>
<Text>{item.fullName || ''}</Text>
<Text>{item.titleAndCompany || ''}</Text>
</View>
);
return (
<ListItem
roundAvatar
key={item.uid}
title={title}
subtitle={subTitle}
leftIcon={this.renderAvatar({ item })}
onPress={() => this.onFriendSelect(item)}
titleStyle={{ fontWeight: 'bold', marginBottom: 5, color: colors.darkGreen }}
/>
);
}
title = `${item.fullName}`;
subTitle = `${item.titleAndCompany || ''}`;
return (
<ListItem
roundAvatar
key={item.uid}
title={title}
subtitle={subTitle}
leftIcon={this.renderAvatar({ item })}
onPress={() => this.onContactSelect(item)}
/>
);
}
}
const styles = {
avatarImageStyle: {
width: 40,
height: 40,
borderWidth: 1,
borderRadius: 20,
marginLeft: 5,
marginRight: 10
}
};
export default ContactItem;

View File

@ -1,212 +0,0 @@
/** Screen for listing contacts **/
import _ from 'lodash';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { SectionList, Text, View } from 'react-native';
import { SearchBar } from 'react-native-elements';
import { contactsFetch, friendsFetch } from '../../actions';
import ContactItem from './ContactItem';
import { Spacer, Spinner } from '../../components/common';
import colors from '../../config/colors.json';
import { strings } from '../../locales/i18n';
let sections = [];
let contactsAndFriends = [];
class ContactList extends Component {
state = { searchTerm: '', loading: true };
/**
* Component Did Mount
**/
componentDidMount() {
this.props.friendsFetch();
this.props.contactsFetch();
this.mergeFriendsAndContacts(this.props.contacts, this.props.friends);
this.createContactSections(contactsAndFriends);
this.setState({ loading: false });
}
/**
* Component Will Receive Props
* Merge contacts and friends if both exist, otherwise use
* the one that exists
**/
componentWillReceiveProps(nextProps) {
if (nextProps.contacts && nextProps.friends) {
this.mergeFriendsAndContacts(nextProps.contacts, nextProps.friends);
this.createContactSections(contactsAndFriends);
} else if (nextProps.contacts) {
nextProps.contacts.sort((a, b) => this.compare(a, b));
contactsAndFriends = nextProps.contacts;
this.createContactSections(nextProps.contacts);
} else if (nextProps.friends) {
nextProps.friends.sort((a, b) => this.compare(a, b));
contactsAndFriends = nextProps.friends;
this.createContactSections(nextProps.friends);
}
}
/**
* Merges the friends and contacts for all of the tabs
**/
mergeFriendsAndContacts(currentContacts, currentFriends) {
contactsAndFriends = currentContacts.concat(currentFriends);
contactsAndFriends.sort((a, b) => this.compare(a, b));
}
/**
* Formats all contacts into groups and creates headers for the SectionList
**/
createContactSections(contacts) {
for (let i = 0; i < contacts.length; i++) {
sections.push({ title: contacts[i].fullName[0], data: contacts[i] });
}
sections = _.groupBy(contacts, d => d.fullName[0].toUpperCase());
sections = _.reduce(sections, (acc, next, index) => {
acc.push({
title: index,
data: next
});
return acc;
}, []);
}
/**
* Used for sorting contacts based on name property
**/
compare(a, b) {
if (a.fullName.toLowerCase() < b.fullName.toLowerCase()) {
return -1;
}
if (a.fullName.toLowerCase() > b.fullName.toLowerCase()) {
return 1;
}
return 0;
}
/**
* Filters search results based on searchBar input
* Checks if the term is found in either name field, titleAndCompany, or tagferId
* Then creates headers based on found contacts
**/
searchUpdated(term) {
contactsAndFriends.sort((a, b) => this.compare(a, b));
let filteredProps = contactsAndFriends;
filteredProps = filteredProps.filter(
(contactContainer) => {
if (contactContainer.fullName.toLowerCase().indexOf(term) !== -1) {
return true;
}
if (contactContainer.tagferId) {
if (contactContainer.tagferId.toLowerCase().indexOf(term) !== -1) {
return true;
}
}
if (contactContainer.titleAndCompany) {
if (contactContainer.titleAndCompany.toLowerCase().indexOf(term) !== -1) {
return true;
}
}
return false;
}
);
this.createContactSections(filteredProps);
this.setState({ searchTerm: term });
}
/**
* Creates a unique key for each contact
**/
keyExtractor = (item) => {
return `${item.uid}`;
}
/**
* Creates the Section Headers that divide contact groups
**/
renderSectionHeader = ({ section }) => {
return (
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>{section.title}</Text>
</View>
);
}
/**
* Renders each row of the list
**/
renderItem = ({ item }) => {
return <ContactItem {...this.props} item={item} />;
}
/**
* Render method
**/
render() {
if (this.state.loading) {
return (
<Spinner size="large" />
);
}
return (
<View style={{ flex: 1 }}>
<SearchBar
onChangeText={(term) => this.searchUpdated(term.toLowerCase())}
containerStyle={{ backgroundColor: colors.white }}
inputStyle={{ backgroundColor: colors.white }}
placeholder={strings('common.search.searchPlaceholder_contacts')}
autoCorrect={false}
autoCapitalize='none'
/>
<SectionList
keyExtractor={this.keyExtractor}
renderSectionHeader={this.renderSectionHeader}
renderItem={this.renderItem}
sections={sections}
/>
<Spacer space={45} />
</View>
);
}
}
const styles = {
sectionContainer: {
borderBottomWidth: 1,
borderBottomColor: colors.lightGrey,
backgroundColor: colors.white
},
sectionTitle: {
color: colors.black,
fontSize: 14,
marginBottom: 8,
marginLeft: 16,
marginRight: 16,
marginTop: 24,
opacity: 0.8
}
};
const mapStateToProps = state => {
// converts contact object of objects into contact array of objects
const contacts = _.map(state.contacts, (val, uid) => {
return { ...val, uid };
});
//converts friend object of objects into friend array of objects
const friends = _.map(state.friends, (val, uid) => {
return { ...val, uid };
});
return { contacts, friends };
};
export default connect(mapStateToProps, {
contactsFetch, friendsFetch
})(ContactList);

View File

@ -1,43 +0,0 @@
/** Pure component for each Conversation Item **/
import React from 'react';
import { Text, View } from 'react-native';
import { ListItem } from 'react-native-elements';
import colors from '../../config/colors.json';
class ConversationItem extends React.PureComponent {
/**
* Action to take when a result is selected
**/
onResultSelect(rowData) {
this.props.navigation.navigate('messageScreen', rowData);
}
/**
* Render method
**/
render() {
const item = this.props.item;
const title = `@${item.tagferId}`;
const subTitle = (
<View>
<Text>{item.fullName}</Text>
<Text>{item.titleAndCompany || '' }</Text>
<Text>{new Date(item.lastMessageSentTime).toLocaleDateString()}</Text>
</View>
);
return (
<ListItem
roundAvatar
key={item.uid}
title={title}
subtitle={subTitle}
leftIcon={{ name: 'account-circle', size: 40, color: colors.lightGrey }}
onPress={() => this.onResultSelect(item)}
titleStyle={{ fontWeight: 'bold', marginBottom: 5, color: colors.darkGreen }}
/>
);
}
}
export default ConversationItem;

View File

@ -1,203 +0,0 @@
/** Screen for listing conversations **/
import _ from 'lodash';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { FlatList, Image, Text, View } from 'react-native';
import { Button } from 'react-native-elements';
import { conversationListFetch } from '../../actions';
import ConversationItem from '../../components/lists/ConversationItem';
import Footer from '../../components/common/Footer';
import { Spacer, Spinner } from '../../components/common';
import colors from '../../config/colors.json';
import { strings } from '../../locales/i18n';
const LOGO = require('../../../assets/tagfer_brand_and_logo_512.png');
let conversationResults = [];
class ConversationList extends Component {
static navigationOptions = ({ navigation }) => {
return {
title: strings('message.conversations_title'),
headerLeft: (
<Button
icon={{
name: 'settings',
type: 'simple-line-icon',
color: colors.middleGrey,
size: 30
}}
onPress={() => navigation.navigate('DrawerOpen')}
backgroundColor={colors.transparent}
/>
)
};
}
state = { loading: true, noItems: false };
/**
* Component Will Mount
**/
componentWillMount() {
this.props.conversationListFetch();
}
/**
* Component Did Mount
**/
componentDidMount() {
this.setState({ loading: false });
}
/**
* Component Will Receive Props
**/
componentWillReceiveProps(nextProps) {
if (nextProps.conversations) {
this.populateConversationResults(nextProps);
}
if (!_.isEqual(this.props.conversations, nextProps.conversations) || this.props.success) {
this.setState({ loading: false });
if (nextProps.conversations.length === 0) {
this.setState({ noItems: true });
}
}
}
/**
* Formats conversationResults for the FlatList
**/
populateConversationResults(nextProps) {
nextProps.conversations.sort((a, b) => this.compare(a, b));
conversationResults = nextProps.conversations;
}
/**
* Used for sorting conversationResults based on fullName property
**/
compare(a, b) {
if (a.fullName.toLowerCase() < b.fullName.toLowerCase()) {
return -1;
}
if (a.fullName.toLowerCase() > b.fullName.toLowerCase()) {
return 1;
}
return 0;
}
/**
* Creates a unique key for each contact
**/
keyExtractor = (item) => {
return `${item.uid}`;
}
/**
* Renders each row of the list
**/
renderItem = ({ item }) => {
return <ConversationItem {...this.props} item={item} />;
}
/**
* Render method
**/
render() {
if (this.state.loading) {
return (
<Spinner size="large" />
);
}
if (this.state.noItems) {
return (
<View style={styles.noItemsViewStyle}>
<Spacer />
<Image
source={LOGO}
style={styles.logoStyle}
/>
<Spacer space={45} />
<Text style={styles.noItemsTextStyle}>
{strings('message.noItems')}
</Text>
<View />
<View style={styles.mainFooterStyle}>
<Footer
{...this.props}
index={3}
/>
</View>
</View>
);
}
return (
<View style={{ flex: 1, backgroundColor: colors.white }}>
<FlatList
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
data={conversationResults}
/>
<View style={styles.mainFooterStyle}>
<Footer
{...this.props}
index={3}
/>
</View>
</View>
);
}
}
const styles = {
noItemsViewStyle: {
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'center',
backgroundColor: colors.white
},
logoStyle: {
height: 200,
width: 200,
marginTop: 15,
marginBottom: 15
},
noItemsTextStyle: {
fontSize: 18,
marginHorizontal: 15
},
mainFooterStyle: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 60
}
};
/**
* Map State To Props
**/
const mapStateToProps = state => {
// converts conversation objects into conversation arrays
const conversations = _.map(state.conversations, (val, uid) => {
return { ...val, uid };
});
// Required for the avatar to load in the footer
const {
profileAvatarImg
} = state.profileForm;
const { success } = state.conversationStatus;
return { conversations, profileAvatarImg, success };
};
export default connect(mapStateToProps, {
conversationListFetch
})(ConversationList);

View File

@ -1,78 +0,0 @@
/** Pure component for each Event Details Attendee Item **/
import React from 'react';
import { Image, Text, View } from 'react-native';
import { ListItem } from 'react-native-elements';
import colors from '../../config/colors.json';
class EventDetailsAttendeeItem extends React.PureComponent {
/**
* Creates the contact avatar
**/
renderAvatar({ profile }) {
return (
<Image
source={{ uri: profile.profileAvatarImg }}
style={styles.avatarImageStyle}
/>
);
}
/**
* Render method
**/
render() {
const { item } = this.props;
const { profile } = item;
const title = `@${profile.tagferId}`;
const subTitle = (
<View>
<Text>{profile.fullName || ''}</Text>
<Text>{profile.titleAndCompany || ''}</Text>
</View>
);
if (item.profileAvatarImg) {
return (
<ListItem
roundAvatar
key={item.uid}
title={title}
subtitle={subTitle}
titleStyle={{ fontWeight: 'bold', marginBottom: 5, color: colors.darkGreen }}
leftIcon={this.renderAvatar({ profile })}
rightIcon={
<View />
}
/>
);
}
return (
<ListItem
roundAvatar
key={item.uid}
title={title}
subtitle={subTitle}
titleStyle={{ fontWeight: 'bold', marginBottom: 5, color: colors.darkGreen }}
leftIcon={{ name: 'account-circle', size: 50, color: colors.lightGrey }}
rightIcon={
<View />
}
/>
);
}
}
const styles = {
avatarImageStyle: {
width: 40,
height: 40,
borderWidth: 1,
borderRadius: 20,
marginLeft: 5,
marginRight: 10
}
};
export default EventDetailsAttendeeItem;

View File

@ -1,276 +0,0 @@
/** Pure component for each Event Item **/
import firebase from 'firebase';
import React from 'react';
import { Dimensions, Image, Text, TouchableOpacity, View } from 'react-native';
import { Card, Icon } from 'react-native-elements';
import { GOING, NOT_GOING } from '../../components/AttendeeStatusTypes';
import colors from '../../config/colors.json';
import { strings } from '../../locales/i18n';
const TEMP_IMAGE = require('../../../assets/tagfer_brand_and_logo_512.png');
const SCREEN_WIDTH = Dimensions.get('window').width;
let attendeesResult = {};
let attendees = [];
class EventItem extends React.PureComponent {
/**
* Component Will Mount
**/
async componentWillMount() {
attendeesResult = await firebase.database().ref(`/eventAttendees/${this.props.item.uid}`).once('value');
attendeesResult = JSON.parse(JSON.stringify(attendeesResult));
if (attendeesResult === null || (Object.keys(attendeesResult).length === 0 && attendeesResult.constructor === Object)) {
return;
}
// If there are attendees we need to force a rerender since it doesn't happen on a pure component
this.forceUpdate();
}
/**
* Renders the main image for an event
**/
renderEventImage(item) {
if (item.eventMainImg) {
return (
<Image
source={{ uri: item.eventMainImg }}
style={styles.eventImageStyle}
/>
);
}
return (
<Image
source={TEMP_IMAGE}
style={styles.eventImageStyle}
/>
);
}
/**
* Renders attendees avatars on each event
**/
renderEventAttendees() {
if (attendeesResult === null || (Object.keys(attendeesResult).length === 0 && attendeesResult.constructor === Object)) {
return <Text>0 </Text>;
}
attendees = [];
for (const property in attendeesResult) {
if (attendeesResult[property].status === GOING) {
attendees.push(attendeesResult[property]);
}
}
if (attendees.length === 0) {
return <Text>0 </Text>;
}
return attendees.map((attendee) => {
if (attendee.profile.profileAvatarImg) {
return (
<Image
key={attendee.uid}
source={{ uri: attendee.profile.profileAvatarImg }}
style={styles.avatarImageStyle}
/>
);
}
return (
<Icon
key={attendee.uid}
name='account-circle'
size={35}
color={colors.lightTeal}
containerStyle={{ marginBottom: 5 }}
/>
);
});
}
/**
* Renders the attendee status section
**/
renderAttendeeStatus(status, profileName) {
if (status === GOING) {
return (
<View style={{ flex: 1 }}>
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
<Icon
name={'ios-checkmark-circle'}
type="ionicon"
size={30}
color={colors.green}
containerStyle={styles.iconStyle}
/>
<Text style={{ fontSize: 15, fontWeight: 'bold', color: colors.green }}>{status}</Text>
</View>
<Text style={{ fontSize: 15, fontWeight: 'bold', alignSelf: 'center', marginBottom: 5, color: colors.buttonBlue }}>{profileName}</Text>
</View>
);
} else if (status === NOT_GOING) {
return (
<View style={{ flex: 1 }}>
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
<Icon
name={'ios-checkmark-circle'}
type="ionicon"
size={30}
color={colors.red}
containerStyle={styles.iconStyle}
/>
<Text style={{ fontSize: 15, fontWeight: 'bold', color: colors.red }}>{status}</Text>
</View>
</View>
);
}
return (
<View style={{ flex: 1 }}>
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
<Icon
name={'ios-checkmark-circle'}
type="ionicon"
size={30}
color={colors.orange}
containerStyle={styles.iconStyle}
/>
<Text style={{ fontSize: 15, fontWeight: 'bold', color: colors.orange }}>{status}</Text>
</View>
<Text style={{ fontSize: 15, fontWeight: 'bold', alignSelf: 'center', marginBottom: 5, color: colors.buttonBlue }}>{profileName}</Text>
</View>
);
}
/**
* Render method
**/
render() {
const item = this.props.item;
const {
eventName,
hostProfile,
endTime,
eventLocation,
eventTimeZone,
startDate,
startTime,
uid
} = item;
const formattedStartTime = `${startTime.substr(0, 4)}${startTime.substr(7, startTime.length)}`;
const formattedEndTime = `${endTime.substr(0, 4)}${endTime.substr(7, endTime.length)}`;
let status = NOT_GOING;
let profileName = '';
if (this.props.myAttendeeStatus.length > 0) {
for (let i = 0; i < this.props.myAttendeeStatus.length; i++) {
if (this.props.myAttendeeStatus[i].uid === item.uid) {
status = this.props.myAttendeeStatus[i].status;
profileName = this.props.myAttendeeStatus[i].profile.profileName;
}
}
}
return (
<Card containerStyle={{ padding: 0, marginHorizontal: 5 }}>
<TouchableOpacity
style={styles.eventContainer}
onPress={() => this.props.navigation.navigate('eventDetailsScreen', { item, myAttendeeStatus: this.props.myAttendeeStatus })}
>
{this.renderEventImage(item)}
<View style={{ width: SCREEN_WIDTH * 0.5 }}>
<Text style={styles.eventTitleStyle}>{eventName}</Text>
<Text style={styles.eventMiddleText}>{startDate}</Text>
<Text style={[styles.eventMiddleText, { fontSize: 12 }]}>{formattedStartTime} - {formattedEndTime} {eventTimeZone}</Text>
<Text style={styles.eventMiddleText}>{eventLocation}</Text>
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
{this.renderEventAttendees()}
<Text style={styles.eventSmallText}>{strings('event.homeScreen.areGoing_label')}</Text>
</View>
</View>
</TouchableOpacity>
<View style={styles.eventActionButtonsContainer}>
{this.renderAttendeeStatus(status, profileName)}
<TouchableOpacity
style={styles.shareEventContainer}
onPress={() => {
this.props.navigation.navigate('shareEventQRCodeScreen', { eventName, eventLocation, hostTagferId: hostProfile.tagferId, startDate, startTime: formattedStartTime, endTime: formattedEndTime, uid });
}}
>
<Icon
name={'action-redo'}
type="simple-line-icon"
size={30}
color={colors.buttonBlue}
containerStyle={styles.iconStyle}
/>
</TouchableOpacity>
</View>
</Card>
);
}
}
const styles = {
eventContainer: {
flex: 1,
flexDirection: 'row',
width: '100%'
},
eventImageStyle: {
width: SCREEN_WIDTH / 3,
height: '100%',
marginRight: 5,
resizeMode: 'contain'
},
eventTitleStyle: {
fontSize: 18,
fontWeight: 'bold',
marginVertical: 5
},
eventMiddleText: {
fontSize: 15,
marginBottom: 5
},
eventSmallText: {
fontSize: 12
},
eventActionButtonsContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
borderTopWidth: 1,
borderColor: colors.lightGrey,
},
shareEventContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
borderLeftWidth: 1,
borderColor: colors.lightGrey
},
iconStyle: {
marginLeft: 20,
marginRight: 5,
},
shareButtonStyle: {
fontSize: 18
},
avatarImageStyle: {
width: 30,
height: 30,
borderRadius: 15,
marginBottom: 5,
marginRight: 5
}
};
export default EventItem;

View File

@ -1,79 +0,0 @@
/** Pure component for each Exhibitor Item **/
import React from 'react';
import { Image, Text, View } from 'react-native';
import { ListItem } from 'react-native-elements';
import colors from '../../config/colors.json';
class ExhibitorListItem extends React.PureComponent {
/**
* Creates the contact avatar
**/
renderAvatar({ item }) {
return (
<Image
source={{ uri: item.profileAvatarImg }}
style={styles.avatarImageStyle}
/>
);
}
/**
* Render method
**/
render() {
const { item, onPress } = this.props;
const title = `@${item.tagferId}`;
const subTitle = (
<View>
<Text>{item.fullName || ''}</Text>
<Text>{item.titleAndCompany || ''}</Text>
</View>
);
if (item.profileAvatarImg) {
return (
<ListItem
roundAvatar
key={item.uid}
title={title}
subtitle={subTitle}
titleStyle={{ fontWeight: 'bold', marginBottom: 5, color: colors.darkGreen }}
onPress={onPress}
leftIcon={this.renderAvatar({ item })}
rightIcon={
<View />
}
/>
);
}
return (
<ListItem
roundAvatar
key={item.uid}
title={title}
subtitle={subTitle}
titleStyle={{ fontWeight: 'bold', marginBottom: 5, color: colors.darkGreen }}
onPress={onPress}
leftIcon={{ name: 'account-circle', size: 50, color: colors.lightGrey }}
rightIcon={
<View />
}
/>
);
}
}
const styles = {
avatarImageStyle: {
width: 40,
height: 40,
borderWidth: 1,
borderRadius: 20,
marginLeft: 5,
marginRight: 10
}
};
export default ExhibitorListItem;

View File

@ -1,237 +0,0 @@
/** Pure component for each My Event Item **/
import firebase from 'firebase';
import React from 'react';
import { Dimensions, Image, Text, TouchableOpacity, View } from 'react-native';
import { Card, Icon } from 'react-native-elements';
import { GOING, INTERESTED } from '../../components/AttendeeStatusTypes';
import colors from '../../config/colors.json';
import { strings } from '../../locales/i18n';
const TEMP_IMAGE = require('../../../assets/tagfer_brand_and_logo_512.png');
const SCREEN_WIDTH = Dimensions.get('window').width;
let attendeesResult = {};
let attendees = [];
let interested = [];
class MyEventItem extends React.PureComponent {
/**
* Component Will Mount
**/
async componentWillMount() {
attendeesResult = await firebase.database().ref(`/eventAttendees/${this.props.item.uid}`).once('value');
attendeesResult = JSON.parse(JSON.stringify(attendeesResult));
if (attendeesResult === null || (Object.keys(attendeesResult).length === 0 && attendeesResult.constructor === Object)) {
return;
}
// If there are attendees we need to force a rerender since it doesn't happen on a pure component
this.forceUpdate();
}
/**
* Renders the main image for an event
**/
renderEventImage(item) {
if (item.eventMainImg) {
return (
<Image
source={{ uri: item.eventMainImg }}
style={styles.eventImageStyle}
/>
);
}
return (
<Image
source={TEMP_IMAGE}
style={styles.eventImageStyle}
/>
);
}
/**
* Renders attendees avatars on each event
**/
renderEventAttendees() {
if (attendeesResult === null || (Object.keys(attendeesResult).length === 0 && attendeesResult.constructor === Object)) {
return <Text>0 </Text>;
}
attendees = [];
interested = [];
for (const property in attendeesResult) {
if (attendeesResult[property].status === GOING) {
attendees.push(attendeesResult[property]);
}
if (attendeesResult[property].status === INTERESTED) {
interested.push(attendeesResult[property]);
}
}
if (attendees.length === 0) {
return <Text>0 </Text>;
}
return attendees.map((attendee) => {
if (attendee.profile.profileAvatarImg) {
return (
<Image
key={attendee.uid}
source={{ uri: attendee.profile.profileAvatarImg }}
style={styles.avatarImageStyle}
/>
);
}
return (
<Icon
key={attendee.uid}
name='account-circle'
size={35}
color={colors.lightTeal}
containerStyle={{ marginBottom: 5 }}
/>
);
});
}
/**
* Render method
**/
render() {
const item = this.props.item;
return (
<Card containerStyle={{ padding: 0, marginHorizontal: 5 }}>
<TouchableOpacity
style={styles.eventContainer}
onPress={() => this.props.navigation.navigate('eventEditScreen', item)}
>
{this.renderEventImage(item)}
<View style={{ width: SCREEN_WIDTH * 0.5 }}>
<Text style={styles.eventTitleStyle}>{item.eventName}</Text>
<Text style={styles.eventMiddleText}>{item.startDate} | {item.startTime} {item.endTime} {item.eventTimeZone}</Text>
<Text style={styles.eventMiddleText}>{item.location}</Text>
<View style={{ flex: 1, flexDirection: 'row', alignItems: 'center' }}>
{this.renderEventAttendees(item)}
<Text style={styles.eventSmallText}>{strings('event.homeScreen.areGoing_label')}</Text>
</View>
</View>
</TouchableOpacity>
<View>
<Text style={styles.attendeeCountInfoTextStyle}>
<Text style={styles.attendeesTextStyle}>{attendees.length} {strings('event.homeScreen.attendees_label')}</Text>
<Text style={styles.interestedTextStyle}> | {interested.length} {strings('event.homeScreen.interested_label')}</Text>
</Text>
</View>
<View style={styles.eventActionButtonsContainer}>
<View style={{ flex: 1 }}>
<TouchableOpacity style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginVertical: 10 }}>
<Text style={styles.shareButtonStyle}>{strings('event.homeScreen.options_button')}</Text>
<Icon
name={'chevron-down'}
type="material-community"
size={20}
color={colors.buttonBlue}
containerStyle={styles.iconStyle}
/>
</TouchableOpacity>
</View>
<TouchableOpacity style={styles.shareEventContainer}>
<Icon
name={'action-redo'}
type="simple-line-icon"
size={30}
color={colors.buttonBlue}
containerStyle={styles.iconStyle}
/>
</TouchableOpacity>
</View>
<View style={{ marginVertical: 5, borderTopWidth: 1, borderColor: colors.lightGrey }}>
<Text style={styles.attendeeCountInfoTextStyle}>
<Text style={styles.interestedTextStyle}>{item.confirmedMeetingsCount} {strings('event.homeScreen.confirmed_label')} | </Text>
<Text style={styles.attendeesTextStyle}>{item.newRequestsCount} {strings('event.homeScreen.requests_label')}</Text>
</Text>
</View>
</Card>
);
}
}
const styles = {
eventContainer: {
flex: 1,
flexDirection: 'row',
width: '100%'
},
eventImageStyle: {
width: SCREEN_WIDTH / 3,
height: '100%',
marginRight: 5,
resizeMode: 'contain'
},
eventTitleStyle: {
fontSize: 18,
fontWeight: 'bold',
marginVertical: 5
},
eventMiddleText: {
fontSize: 15,
marginBottom: 5
},
eventSmallText: {
fontSize: 12
},
eventActionButtonsContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
borderTopWidth: 1,
borderColor: colors.lightGrey,
},
shareEventContainer: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
borderLeftWidth: 1,
borderColor: colors.lightGrey
},
iconStyle: {
marginLeft: 20,
marginRight: 5,
},
attendeeCountInfoTextStyle: {
flex: 1,
justifyContent: 'center',
alignSelf: 'center',
marginVertical: 5
},
attendeesTextStyle: {
fontSize: 16,
fontWeight: 'bold',
color: colors.darkBlue
},
interestedTextStyle: {
fontSize: 14,
color: colors.darkBlue
},
shareButtonStyle: {
fontSize: 18
},
avatarImageStyle: {
width: 30,
height: 30,
borderRadius: 15,
marginBottom: 5,
marginRight: 5
}
};
export default MyEventItem;

View File

@ -1,55 +0,0 @@
/** Pure component for each Network Search Item **/
import React from 'react';
import { Text, View } from 'react-native';
import { ListItem } from 'react-native-elements';
import colors from '../../config/colors.json';
class NetworkSearchItem extends React.PureComponent {
/**
* Action to take when a result is selected
**/
onResultSelect(rowData) {
this.props.navigation.navigate('socialAddScreen', rowData);
}
/**
* Render method
**/
render() {
const item = this.props.item;
const title = `@${item.tagferId}`;
const subTitle = (
<View>
<Text>{item.fullName || ''}</Text>
<Text>{item.titleAndCompany || ''}</Text>
</View>
);
if (item.profileAvatarImg) {
return (
<ListItem
roundAvatar
key={item.uid}
title={title}
subtitle={subTitle}
titleStyle={{ fontWeight: 'bold', marginBottom: 5, color: colors.darkGreen }}
avatar={{ uri: item.profileAvatarImg }}
onPress={() => this.onResultSelect(item)}
/>
);
}
return (
<ListItem
roundAvatar
key={item.uid}
title={title}
subtitle={subTitle}
titleStyle={{ fontWeight: 'bold', marginBottom: 5, color: colors.darkGreen }}
leftIcon={{ name: 'account-circle', size: 40, color: colors.lightGrey }}
onPress={() => this.onResultSelect(item)}
/>
);
}
}
export default NetworkSearchItem;

View File

@ -1,40 +0,0 @@
/** Pure component for each Note Item **/
import React from 'react';
import { ListItem } from 'react-native-elements';
import colors from '../../config/colors.json';
class NoteItem extends React.PureComponent {
/**
* Render method
**/
render() {
const item = this.props.item;
const title = new Date(item.uid).toLocaleDateString();
const subTitle = item.note;
return (
<ListItem
key={item.uid}
title={title}
subtitle={subTitle}
titleStyle={styles.listItemTitleStyle}
subtitleStyle={styles.listItemSubTitleStyle}
subtitleNumberOfLines={0}
hideChevron
/>
);
}
}
const styles = {
listItemTitleStyle: {
color: colors.middleGrey,
fontSize: 13
},
listItemSubTitleStyle: {
color: colors.black,
fontWeight: 'normal'
}
};
export default NoteItem;

View File

@ -1,34 +0,0 @@
/** Pure component for each Notification Item **/
import React from 'react';
import { ListItem } from 'react-native-elements';
import { REQUEST_SENT } from '../NotificationListTypes';
class NotificationItem extends React.PureComponent {
/**
* When a notification is selected it goes to the notification display screen
**/
onNotificationSelect(rowData) {
this.props.navigation.navigate('notificationStatusScreen', rowData);
}
/**
* Render method
**/
render() {
const item = this.props.item;
// Other options are REFERRAL_RECEIVED, REQUEST_ACCEPTED, and REQUEST_RECEIVED
const title = item.notificationType === !REQUEST_SENT ? `@${item.myTagferId}` : `@${item.otherTagferId}`;
const subTitle = `${new Date(item.socialAddDate).toLocaleDateString()}`;
return (
<ListItem
key={item.uid}
title={title}
subtitle={subTitle}
onPress={() => this.onNotificationSelect(item)}
/>
);
}
}
export default NotificationItem;

View File

@ -1,246 +0,0 @@
/** Screen for listing notifications **/
import _ from 'lodash';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Dimensions, Image, SectionList, Text, View } from 'react-native';
import NotificationItem from '../../components/lists/NotificationItem';
import { Spacer, Spinner } from '../../components/common';
import {
fetchProfileInfoByProfileId,
profileListReset,
notificationsFetch
} from '../../actions';
import {
REFERRAL_RECEIVED,
REQUEST_ACCEPTED,
REQUEST_SENT,
REQUEST_RECEIVED
} from '../NotificationListTypes';
import colors from '../../config/colors.json';
import { strings } from '../../locales/i18n';
const LOGO = require('../../../assets/tagfer_brand_and_logo_512.png');
const SCREEN_HEIGHT = Dimensions.get('window').height;
let sections = [];
class NotificationList extends Component {
state = { loading: true, noItems: false };
/**
* Component Did Mount
**/
componentWillMount() {
this.props.profileListReset();
this.props.notificationsFetch();
}
/**
* Component Will Receive Props
**/
componentWillReceiveProps(nextProps) {
if (nextProps.notifications && nextProps.notifications.length > 0 && nextProps.profiles.length === 0) {
const userIds = [];
const profileIds = [];
for (let i = 0; i < nextProps.notifications.length; i++) {
userIds.push(nextProps.notifications[i].otherUserId);
profileIds.push(nextProps.notifications[i].otherProfileId);
}
if (userIds.length > 0) {
this.props.fetchProfileInfoByProfileId(userIds, profileIds);
}
}
if (nextProps.profiles) {
this.createNotificationSections(nextProps.notifications, nextProps.profiles);
}
if (!_.isEqual(this.props.notifications, nextProps.notifications) || this.props.success) {
this.setState({ loading: false });
if (nextProps.notifications.length === 0) {
this.setState({ noItems: true });
}
}
}
/**
* Formats all notifications into groups and creates headers for the SectionList
**/
createNotificationSections(notifications, profiles) {
for (let i = 0; i < notifications.length; i++) {
for (let j = 0; j < profiles.length; j++) {
if (notifications[i].otherProfileId === profiles[j].uid) {
// combines the notification and profile data via profileId matching
sections.push({
title: notifications[i].notificationType,
data: Object.assign(notifications[i], profiles[j])
});
break;
}
}
}
sections = _.groupBy(notifications, d => d.notificationType);
sections = _.reduce(sections, (acc, next, index) => {
acc.push({
title: index,
data: next
});
return acc;
}, []);
}
/**
* Creates a unique key for each notification
**/
keyExtractor = (item) => {
return `${item.uid}_${item.myProfileId}`;
}
/**
* Creates the Section Headers that divide notification groups
**/
renderSectionHeader = ({ section }) => {
switch (section.title) {
case REFERRAL_RECEIVED:
return (
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>{strings('notification.listHeader_referrals')}</Text>
</View>
);
case REQUEST_ACCEPTED:
return (
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>{strings('notification.listHeader_acceptedRequests')}</Text>
</View>
);
case REQUEST_RECEIVED:
return (
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>{strings('notification.listHeader_receivedRequests')}</Text>
</View>
);
case REQUEST_SENT:
return (
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>{strings('notification.listHeader_sentRequests')}</Text>
</View>
);
default:
return (
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>{section.title}</Text>
</View>
);
}
}
/**
* Renders each row of the list
**/
renderItem = ({ item }) => {
return <NotificationItem {...this.props} item={item} />;
}
/**
* Render method
**/
render() {
if (this.state.loading) {
return (
<View style={{ flex: 1, backgroundColor: colors.white }}>
<Spinner size="large" />;
</View>
);
}
if (this.state.noItems) {
return (
<View style={styles.noItemsViewStyle}>
<Spacer />
<Image
source={LOGO}
style={styles.logoStyle}
/>
<Spacer space={45} />
<View>
<Text style={styles.noItemsTextStyle}>
{strings('notification.noItems')}
</Text>
</View>
</View>
);
}
return (
<View style={{ flex: 1, backgroundColor: colors.white, minHeight: SCREEN_HEIGHT }}>
<SectionList
keyExtractor={this.keyExtractor}
renderSectionHeader={this.renderSectionHeader}
renderItem={this.renderItem}
sections={sections}
/>
</View>
);
}
}
const styles = {
sectionContainer: {
borderBottomWidth: 1,
borderBottomColor: colors.transparent,
backgroundColor: colors.offWhite
},
sectionTitle: {
color: colors.black,
fontSize: 14,
marginBottom: 8,
marginLeft: 16,
marginRight: 16,
marginTop: 24,
opacity: 0.8
},
noItemsViewStyle: {
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-start',
alignItems: 'center',
backgroundColor: colors.white,
height: SCREEN_HEIGHT
},
logoStyle: {
height: 200,
width: 200,
marginTop: 15,
marginBottom: 15
},
noItemsTextStyle: {
fontSize: 18,
marginHorizontal: 15
}
};
const mapStateToProps = state => {
// converts notification object of objects into notification array of objects
const notifications = _.map(state.notifications, (val, uid) => {
return { ...val, uid };
});
// converts profiles object of objects into profiles array of objects
const profiles = _.map(state.profiles, (val, uid) => {
return { ...val, uid };
});
const { success } = state.notificationStatus;
return { profiles, notifications, success };
};
export default connect(mapStateToProps, {
fetchProfileInfoByProfileId, profileListReset, notificationsFetch,
})(NotificationList);

View File

@ -1,132 +0,0 @@
/** Screen for listing profiles **/
import _ from 'lodash';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { ListView } from 'react-native';
import { List, ListItem } from 'react-native-elements';
import { profileFetch, propertiesFetch, socialSendFriendRequest } from '../../actions';
import colors from '../../config/colors.json';
import { strings } from '../../locales/i18n';
class ProfileList extends Component {
static navigationOptions = () => {
return {
title: strings('profile.list_title')
};
}
/**
* Component Will Mount
**/
componentWillMount() {
this.props.profileFetch();
this.props.propertiesFetch();
this.createDataSource(this.props);
}
/**
* Component Will Receive Props
**/
componentWillReceiveProps(nextProps) {
if (nextProps.profiles) {
this.createDataSource(nextProps);
}
}
/**
* When selected from the ProfileListScreen navigates to the edit screen
* When called from the SocialAddFriendScreen the user chooses an profile to share
* profileId - the profileId of the requested user
* userId - the userId of the requested user
**/
onProfileSelect(rowData) {
if (this.props.isProfileSelectModal) {
const { tagferId, otherUserId, otherProfileId } = this.props;
const myProfileId = rowData.uid;
const socialAddDate = new Date().toString();
this.props.socialSendFriendRequest({
myProfileId,
myTagferId: tagferId,
otherUserId,
otherTagferId: this.props.navigation.state.params.tagferId,
otherProfileId,
socialAddDate
});
this.props.closeModal();
this.props.navigation.goBack();
} else {
this.props.navigation.navigate('profileEdit', rowData);
}
}
/**
* Creates a data source for each profile in the list
**/
createDataSource({ profiles }) {
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
this.dataSource = ds.cloneWithRows(profiles);
}
/**
* Renders each row of the list
**/
renderRow(rowData) {
const title = `${rowData.profileName}`;
const subTitle = `${rowData.titleAndCompany}`;
if (rowData.profileAvatarImg) {
return (
<ListItem
roundAvatar
key={rowData.uid}
title={title}
subtitle={subTitle}
avatar={{ uri: rowData.profileAvatarImg }}
onPress={() => this.onProfileSelect(rowData)}
/>
);
}
return (
<ListItem
roundAvatar
key={rowData.uid}
title={title}
subtitle={subTitle}
leftIcon={{ name: 'account-circle', size: 40, color: colors.lightGrey }}
onPress={() => this.onProfileSelect(rowData)}
/>
);
}
/**
* Render method
**/
render() {
return (
<List>
<ListView
enableEmptySections
dataSource={this.dataSource}
renderRow={this.renderRow.bind(this)}
/>
</List>
);
}
}
const mapStateToProps = state => {
// converts Profile objects into profile arrays
const profiles = _.map(state.profiles, (val, uid) => {
return { ...val, uid };
});
const { tagferId } = state.properties;
return { profiles, tagferId };
};
export default connect(mapStateToProps, {
profileFetch, propertiesFetch, socialSendFriendRequest
})(ProfileList);

View File

@ -1,219 +0,0 @@
/** Screen for listing socialContacts **/
import _ from 'lodash';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { FlatList, View } from 'react-native';
import { SearchBar } from 'react-native-elements';
import {
profileFetch,
friendsFetch,
notificationsFetch,
socialListReset,
socialSearch
} from '../../actions';
import NetworkSearchItem from '../../components/lists/NetworkSearchItem';
import { Spinner } from '../../components/common';
import colors from '../../config/colors.json';
import { strings } from '../../locales/i18n';
let searchResults = [];
class SocialList extends Component {
constructor() {
super();
this.onChangeTextDelayed = _.debounce(this.searchUpdated, 250);
this.state = { searchTerm: '', loading: true };
}
/**
* Component Did Mount
**/
componentDidMount() {
this.props.profileFetch();
this.props.friendsFetch();
this.props.notificationsFetch();
this.props.socialListReset();
this.setState({ loading: false });
}
/**
* Component Will Receive Props
**/
componentWillReceiveProps(nextProps) {
if (nextProps.socialContacts) {
this.populateSearchResults(nextProps);
}
}
/**
* Formats searchResults for the FlatList
**/
populateSearchResults(nextProps) {
if (nextProps) {
nextProps.socialContacts.sort((a, b) => this.compare(a, b));
searchResults = this.removeMyProfilesFromSearchResults(nextProps.socialContacts);
searchResults = this.removeMyFriendsFromSearchResults(nextProps.socialContacts);
searchResults = this.removeMyNotificationsFromSearchResults(nextProps.socialContacts);
} else {
this.props.socialContacts.sort((a, b) => this.compare(a, b));
searchResults = this.removeMyProfilesFromSearchResults(this.props.socialContacts);
searchResults = this.removeMyFriendsFromSearchResults(this.props.socialContacts);
searchResults = this.removeMyNotificationsFromSearchResults(this.props.socialContacts);
}
}
/**
* Removes any of the user's profiles from displaying as options for them to add
**/
removeMyProfilesFromSearchResults(searchResultsContacts) {
const profiles = this.props.profiles;
const searchContacts = searchResultsContacts;
for (let i = 0; i < searchContacts.length; i++) {
for (let j = 0; j < profiles.length; j++) {
if (searchContacts[i].uid === profiles[j].uid) {
searchContacts.splice(i, 1);
i--;
break;
}
}
}
return searchContacts;
}
/**
* Removes any of the user's current friends from displaying on search results
**/
removeMyFriendsFromSearchResults(searchResultsContacts) {
const friends = this.props.friends;
const searchContacts = searchResultsContacts;
for (let i = 0; i < searchContacts.length; i++) {
for (let j = 0; j < friends.length; j++) {
if (searchContacts[i].uid === friends[j].uid) {
searchContacts.splice(i, 1);
i--;
break;
}
}
}
return searchContacts;
}
/**
* Removes any of the user's current notifications from displaying on search results
**/
removeMyNotificationsFromSearchResults(searchResultsContacts) {
const notifications = this.props.notifications;
const searchContacts = searchResultsContacts;
for (let i = 0; i < searchContacts.length; i++) {
for (let j = 0; j < notifications.length; j++) {
if (searchContacts[i].profileId === notifications[j].otherProfileId) {
searchContacts.splice(i, 1);
i--;
break;
}
}
}
return searchContacts;
}
/**
* Used for sorting searchResults based on fullName property
**/
compare(a, b) {
if (a.fullName.toLowerCase() < b.fullName.toLowerCase()) {
return -1;
}
if (a.fullName.toLowerCase() > b.fullName.toLowerCase()) {
return 1;
}
return 0;
}
/**
* Filters search results based on searchBar input
**/
searchUpdated = (term) => {
const trimmedTerm = term.trim();
if (trimmedTerm.length <= 2) {
this.props.socialListReset();
} else {
this.props.socialSearch(trimmedTerm);
this.populateSearchResults();
this.setState({ searchTerm: trimmedTerm });
}
}
/**
* Creates a unique key for each contact
**/
keyExtractor = (item) => {
return `${item.uid}`;
}
/**
* Renders each row of the list
**/
renderItem = ({ item }) => {
return <NetworkSearchItem {...this.props} item={item} />;
}
/**
* Render method
**/
render() {
if (this.state.loading) {
return (
<Spinner size="large" />
);
}
return (
<View style={{ flex: 1 }}>
<SearchBar
onChangeText={this.onChangeTextDelayed}
containerStyle={{ backgroundColor: colors.white }}
inputStyle={{ backgroundColor: colors.white }}
placeholder={strings('common.search.searchPlaceholder_network')}
autoCorrect={false}
autoCapitalize='none'
/>
<FlatList
keyExtractor={this.keyExtractor}
renderItem={this.renderItem}
data={searchResults}
/>
</View>
);
}
}
const mapStateToProps = state => {
// converts Profile objects into profile arrays
const profiles = _.map(state.profiles, (val, uid) => {
return { ...val, uid };
});
//converts friend object of objects into friend array of objects
const friends = _.map(state.friends, (val, uid) => {
return { ...val, uid };
});
// converts notification object of objects into notification array of objects
const notifications = _.map(state.notifications, (val, uid) => {
return { ...val, uid };
});
// converts contact object of objects into contact array of objects
const socialContacts = _.map(state.socialList, (val, uid) => {
return { ...val, uid };
});
return { profiles, friends, notifications, socialContacts };
};
export default connect(mapStateToProps, {
profileFetch, friendsFetch, notificationsFetch, socialListReset, socialSearch
})(SocialList);

View File

@ -1,121 +0,0 @@
/** Pure component for each Speaker Item **/
import React from 'react';
import { Image, Text, View } from 'react-native';
import { CheckBox, ListItem } from 'react-native-elements';
import colors from '../../config/colors.json';
class SpeakerListItem extends React.PureComponent {
/**
* Creates the contact avatar
**/
renderAvatar({ item }) {
return (
<Image
source={{ uri: item.profileAvatarImg }}
style={styles.avatarImageStyle}
/>
);
}
/**
* Render method
**/
render() {
const { isChecked, item, onPress, hasCheckBox } = this.props;
const title = `@${item.tagferId}`;
const subTitle = (
<View>
<Text>{item.fullName || ''}</Text>
<Text>{item.titleAndCompany || ''}</Text>
</View>
);
if (item.profileAvatarImg) {
if (hasCheckBox) {
return (
<ListItem
roundAvatar
key={item.uid}
title={title}
subtitle={subTitle}
titleStyle={{ fontWeight: 'bold', marginBottom: 5, color: colors.darkGreen }}
leftIcon={this.renderAvatar({ item })}
onPress={onPress}
rightIcon={
<CheckBox
checked={isChecked}
containerStyle={{ backgroundColor: colors.transparent, borderWidth: 0 }}
onPress={onPress}
/>
}
/>
);
}
// Don't display the checkbox - for Event Details Screen
return (
<ListItem
roundAvatar
key={item.uid}
title={title}
subtitle={subTitle}
titleStyle={{ fontWeight: 'bold', marginBottom: 5, color: colors.darkGreen }}
leftIcon={this.renderAvatar({ item })}
onPress={onPress}
rightIcon={
<View />
}
/>
);
}
if (hasCheckBox) {
return (
<ListItem
roundAvatar
key={item.key}
title={title}
subtitle={subTitle}
titleStyle={{ fontWeight: 'bold', marginBottom: 5, color: colors.darkGreen }}
leftIcon={{ name: 'account-circle', size: 50, color: colors.lightGrey }}
onPress={onPress}
rightIcon={
<CheckBox
checked={isChecked}
containerStyle={{ backgroundColor: colors.transparent, borderWidth: 0 }}
onPress={onPress}
/>
}
/>
);
}
// Don't display the checkbox - for Event Details Screen
return (
<ListItem
roundAvatar
key={item.key}
title={title}
subtitle={subTitle}
titleStyle={{ fontWeight: 'bold', marginBottom: 5, color: colors.darkGreen }}
leftIcon={{ name: 'account-circle', size: 50, color: colors.lightGrey }}
onPress={onPress}
rightIcon={
<View />
}
/>
);
}
}
const styles = {
avatarImageStyle: {
width: 40,
height: 40,
borderWidth: 1,
borderRadius: 20,
marginLeft: 5,
marginRight: 10
}
};
export default SpeakerListItem;

View File

@ -1,80 +0,0 @@
/** Pure component for each Wallet Request Item **/
import React from 'react';
import { Image, Text, View } from 'react-native';
import { Icon, ListItem } from 'react-native-elements';
import colors from '../../config/colors.json';
class WalletRequestItem extends React.PureComponent {
/**
* Navigates to the request details screen
**/
onRequestItemSelect(rowData) {
this.props.navigation.navigate('requestDetailsScreen', rowData);
}
/**
* Creates the requesting users' avatar
**/
renderRequestAvatar({ item }) {
if (item.socialBizCardImg) {
return (
<Image
source={{ uri: item.socialBizCardImg }}
style={styles.avatarImageStyle}
/>
);
}
return (
<Icon
name='account-circle'
size={75}
color={colors.lightGrey}
containerStyle={{ marginRight: 10 }}
/>
);
}
/**
* Render method
**/
render() {
const item = this.props.item;
const title = `@${item.tagferId}`;
const subTitle = (
<View>
<Text>{item.fullName} | {item.transactionAmount} TT</Text>
<Text
style={{ fontWeight: 'bold', marginTop: 5 }}
numberOfLines={1}
>
{item.requestMessage}
</Text>
</View>
);
return (
<ListItem
roundAvatar
key={item.uid}
title={title}
subtitle={subTitle}
leftIcon={this.renderRequestAvatar({ item })}
onPress={() => this.onRequestItemSelect(item)}
titleStyle={{ fontWeight: 'bold', marginBottom: 5, color: colors.darkGreen }}
/>
);
}
}
const styles = {
avatarImageStyle: {
width: 60,
height: 60,
borderWidth: 1,
borderRadius: 30,
marginLeft: 10,
marginRight: 10
}
};
export default WalletRequestItem;

View File

@ -1,106 +0,0 @@
/** Pure component for each Wallet Transaction Item **/
import React from 'react';
import { Text, View } from 'react-native';
import { Icon, ListItem } from 'react-native-elements';
import { EVENT_TRANSACTION, EARNED_TRANSACTION, RECEIVED_TRANSACTION, REQUESTED_TRANSACTION, SENT_TRANSACTION } from '../../components/TransactionTypes';
import colors from '../../config/colors.json';
class WalletTransactionItem extends React.PureComponent {
/**
* Navigates to the transaction details screen
**/
onTransactionItemSelect(rowData) {
this.props.navigation.navigate('transactionDetailsScreen', rowData);
}
/**
* Creates the requesting users' avatar
**/
renderTransactionAvatar({ item }) {
switch (item.transactionType) {
case EVENT_TRANSACTION:
return (
<Icon
name={'event'}
size={25}
color={colors.buttonBlue}
containerStyle={{ marginRight: 5, marginLeft: -5 }}
/>
);
case EARNED_TRANSACTION:
return (
<Icon
name={'trophy'}
type='entypo'
size={25}
color={colors.teal}
containerStyle={{ marginRight: 5, marginLeft: -5 }}
/>
);
case RECEIVED_TRANSACTION:
return (
<Icon
name={'arrow-long-left'}
type='entypo'
size={25}
color={colors.green}
containerStyle={{ marginRight: 5, marginLeft: -5 }}
/>
);
case REQUESTED_TRANSACTION:
return (
<Icon
name={'hand'}
type='entypo'
size={25}
color={colors.pink}
containerStyle={{ marginRight: 5, marginLeft: -5 }}
/>
);
case SENT_TRANSACTION:
return (
<Icon
name={'arrow-long-right'}
type='entypo'
size={25}
color={colors.orange}
containerStyle={{ marginRight: 5, marginLeft: -5 }}
/>
);
default:
break;
}
}
/**
* Render method
**/
render() {
const item = this.props.item;
const title = `@${item.tagferId}`;
const subTitle = (
<View>
<Text>{item.fullName} | {item.transactionAmount} TT</Text>
<Text
style={{ fontWeight: 'bold', marginTop: 5 }}
numberOfLines={1}
>
{item.transactionMessage}
</Text>
</View>
);
return (
<ListItem
key={item.uid}
title={title}
subtitle={subTitle}
leftIcon={this.renderTransactionAvatar({ item })}
onPress={() => this.onTransactionItemSelect(item)}
titleStyle={{ fontWeight: 'bold', marginBottom: 5, color: colors.darkGreen }}
/>
);
}
}
export default WalletTransactionItem;

View File

@ -4,7 +4,7 @@ import { ListItem, Avatar } from 'react-native-elements';
import colors from '../../config/colors.json';
import { isEmpty } from '../../utils/aux';
import TagferCheckbox from '../new/TagferCheckbox.js';
import TagferCheckbox from './TagferCheckbox.js';
const ContactListItem = ({ contact, selected, onPress }) => {
const { givenName, familyName, phoneNumbers } = contact;

View File

@ -0,0 +1,55 @@
import React from 'react';
import { View } from 'react-native';
import { FormInput, CheckBox } from 'react-native-elements';
import { strings } from '../../locales/i18n';
import colors from '../../config/colors.json'
const PasswordField = ({ value, onChangeText, inputStyle, visible = false, onEyePress }) => {
return (
<View>
{/* PASSWORD INPUT */}
<FormInput
secureTextEntry={!visible}
value={value}
onChangeText={onChangeText}
placeholder={strings('userCreateFlow.signUp.minPassword')}
autoCorrect={false}
autoCapitalize='none'
inputStyle={inputStyle}
/>
{/* PASSWORD EYE */}
<CheckBox
iconRight
iconType='material-community'
checkedIcon='eye-off'
uncheckedIcon='eye'
onIconPress={onEyePress}
onPress={onEyePress}
uncheckedColor={colors.grey}
checkedColor={colors.grey}
checked={visible}
size={25}
containerStyle={styles.passwordEye}
/>
</View>
);
};
const styles = {
passwordEye: {
position: 'absolute',
top: 5,
right: 25,
backgroundColor: colors.transparent,
borderWidth: 0,
margin: 0,
padding: 0,
marginRight: 0
}
}
export default PasswordField;

View File

@ -3,7 +3,7 @@ import { Avatar } from 'react-native-elements';
import { isEmpty } from '../../utils/aux';
const TagferAvatar = ({ size, photoURL }) => {
const TagferAvatar = ({ size, photoURL, style, color }) => {
const uri = !isEmpty(photoURL) ? { uri: photoURL } : null;
const icon = isEmpty(photoURL) ? { name: 'user', type: 'entypo' } : null;
@ -14,6 +14,8 @@ const TagferAvatar = ({ size, photoURL }) => {
icon={icon}
source={uri}
activeOpacity={0.7}
containerStyle={style}
overlayContainerStyle={{ backgroundColor: color }}
/>
);
};

View File

@ -1,4 +1,4 @@
const baseurl = 'http://192.168.1.201:3000';
const baseurl = 'https://us-central1-tagfer-inc.cloudfunctions.net/api';
const endpoints = {
login: {
method: 'POST',
@ -39,18 +39,6 @@ const endpoints = {
signup: {
method: 'PUT',
url: `${baseurl}/auth/signup`
},
retrieveLinkedInInfo: (linkedInQS) => ({
method: 'GET',
url: `${baseurl}/auth/linkedin/get/profile${linkedInQS}`
}),
linkedInToken: {
method: 'GET',
url: `${baseurl}/auth/linkedin/token`
},
linkedInLogin: {
method: 'GET',
url: `${baseurl}/auth/linkedin/login`
}
};

View File

@ -6,7 +6,7 @@ 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/SignupScreen4';
import SignupScreenFour from '../screens/auth/SignupScreen4';
import SignupScreenFive from '../screens/auth/SignupScreen5';
import SignupScreenSix from '../screens/auth/SignupScreen6';
import TwitterWebView from '../screens/auth/TwitterWebView';

View File

@ -46,8 +46,8 @@
"education_educationSummary_placeholder": "Education Summary",
"education_fieldOfStudy_placeholder": "Field Of Study",
"education_school_placeholder": "College/University",
"email_labelText": "Email Address",
"email_or_tagferid_label": "EMAIL OR TAGFERID",
"email_labelText": "EMAIL ADDRESS",
"email_or_tagferid_label": "EMAIL OR TAGFER ID",
"email_or_tagferid_placeholder": "username@tagfer.com",
"email_placeholder": "Email Address",
"experience_labelText": "Experience",
@ -58,7 +58,7 @@
"extension_labelText": "Extension",
"extension_placeholder": "Extension",
"fax_labelText": "Fax",
"firstName_labelText": "First Name",
"firstName_labelText": "FIRST NAME",
"firstName_placeholder": "First Name",
"FromTo_placeholder": "From - To",
"fullName_labelText": "Name",
@ -74,17 +74,17 @@
"keywordsAdd_labelText2": "(Limit 8. Separate multiple keywords with a comma.)",
"keywordsHeader_labelText": "Keywords",
"keywords_placeholder": "Marketing",
"lastName_labelText": "Last Name",
"lastName_labelText": "LAST NAME",
"lastName_placeholder": "Last Name",
"linkedIn_import": "Import data from LinkedIn",
"company_number_labelText": "Company Number",
"company_number_placeholder": "0000000000",
"company_title_labelText": "Title Profession",
"company_title_placeholder": "E.g.: CEO, UX Designer",
"company_name_labelText": "Company Name",
"company_name_placeholder": "Your company name",
"company_address_labelText": "Company Email Address",
"company_address_placeholder": "Your company email address",
"company_number_labelText": "COMPANY NUMBER",
"company_number_placeholder": "(XXX) XXX-XXXX",
"company_title_labelText": "TITLE PROFESSION",
"company_title_placeholder": "CEO, UX Designer",
"company_name_labelText": "COMPANY NAME",
"company_name_placeholder": "Company name",
"company_address_labelText": "COMPANY EMAIL ADDRESS",
"company_address_placeholder": "example@email.com",
"location_labelText": "Location",
"location_placeholder": "Santa Clara, CA",
"notes_labelText": "Notes",
@ -100,7 +100,7 @@
"referrer_labelText": "Referral Id",
"referrer_placeholder": "Referral Id",
"setPublic_labelText": "Make This Profile Public",
"tagferId_labelText": "Tagfer ID",
"tagferId_labelText": "TAGFER ID",
"tagferId_placeholder": "Tagfer ID",
"titleAndCompany_labelText": "Title and Company",
"titleAndCompany_placeholder": "Founder and CEO of Acme Corp",
@ -573,7 +573,7 @@
},
"signUp": {
"minPassword": "6+ characters",
"password": "Password",
"password": "PASSWORD",
"password2": "Confirm Password",
"title": "Signup",
"urgencyMessage": "Claim your Tagfer ID before it's taken."

View File

@ -0,0 +1,135 @@
import Realm from 'realm';
import { UserSchema, ProfileSchema, InvitesSchema, ContactsSchema, SignupConfig, AuthConfig, SessionSchema } from '../schemas/AuthSchema';
// ############################ MAIN #############################
var authRealm;
var signupRealm;
// Loads the signup state; a `then` should be chained at the component level to save the state
export async function loadSignupState(screen) {
const realm = await getRealm(screen);
return getPreviousStateFor(screen, realm);
}
// Saves signup state for screens 1-5, we do not save 6 as the data loaded is random and should change everytime.
export async function saveSignupState(screen, state, isUpdate) {
try {
const realm = await getRealm(screen);
realm.write(() => saveCurrentStateFor(screen, state, realm, isUpdate));
} catch (error) {
console.log(error);
}
}
export function isContactSelected(recordID) {
return signupRealm.objectForPrimaryKey(ContactsSchema.name, recordID);
}
export async function saveAuthState(sessionId) {
try {
const realm = await getRealm(0);
realm.write(() => saveCurrentStateFor(0, { sessionId }, realm, !realm.empty));
} catch (error) {
console.log(error);
}
}
export async function getAuthState() {
try {
const realm = await getRealm(0);
return realm.objectForPrimaryKey(SessionSchema.name, primaryKey);
} catch (error) {
console.log(error);
}
}
// ############################ HELPERS #############################
async function getRealm(screen) {
if (screen === 0) {
if (!authRealm) {
authRealm = await Realm.open(AuthConfig);
}
return authRealm;
}
if (!signupRealm) {
signupRealm = await Realm.open(SignupConfig);
}
return signupRealm;
}
function getSchemaFor(screen) {
if (screen === 0) {
return SessionSchema;
} else if (screen === 4) {
return ProfileSchema;
} else if (screen === 5) {
return InvitesSchema;
}
return UserSchema;
}
function getPreviousStateFor(screen, realm) {
let isUpdate = true;
let prevState = {};
if (screen === 0) {
prevState = realm.objectForPrimaryKey(SessionSchema.name, primaryKey);
isUpdate = !!prevState;
} else if (screen === 1) {
prevState = realm.objectForPrimaryKey(UserSchema.name, primaryKey);
isUpdate = !!prevState;
} else if (screen === 2) {
const object = realm.objectForPrimaryKey(UserSchema.name, primaryKey);
if (!!object && !!object.tagferId) {
const { tagferId, twitterUsername } = object;
prevState = { tagferId, twitterUsername };
}
} else if (screen === 3) {
const object = realm.objectForPrimaryKey(UserSchema.name, primaryKey);
if (!!object && !!object.phoneNumber) {
const { phoneNumber, cca2, callingCode } = object;
prevState = { phoneNumber, cca2, callingCode };
}
} else if (screen === 4) {
prevState = realm.objectForPrimaryKey(ProfileSchema.name, primaryKey);
isUpdate = !!prevState;
} else if (screen === 5) {
isUpdate = !!realm.objectForPrimaryKey(InvitesSchema.name, primaryKey);
} else if (screen === 6) {
const user = realm.objectForPrimaryKey(UserSchema.name, primaryKey);
const profile = realm.objectForPrimaryKey(ProfileSchema.name, primaryKey);
const invites = realm.objectForPrimaryKey(InvitesSchema.name, primaryKey);
prevState = { user, profile, invites };
}
return { isUpdate, prevState };
}
function saveCurrentStateFor(screen, state, realm, isUpdate) {
if (screen === -5) {
return saveSelectedContacts(realm, state);
}
const schema = getSchemaFor(screen);
try {
realm.create(schema.name, { key: primaryKey, ...state }, isUpdate);
} catch(error) {
console.log(error);
}
}
// NOTE: `selectedContacts` is an iterator passed from Map.keys()
function saveSelectedContacts(realm, selectedContacts) {
realm.delete(realm.objects(ContactsSchema.name));
for(let recordID of selectedContacts) {
realm.create(ContactsSchema.name, { recordID });
}
}
const primaryKey = 0;

View File

@ -0,0 +1,67 @@
export const UserSchema = {
name: 'User',
primaryKey: 'key',
properties: {
key: 'int',
email: 'string',
password: 'string',
firstName: 'string',
lastName: 'string',
tagferId: 'string?',
twitterUsername: 'bool?',
phoneNumber: 'string?',
cca2: 'string?',
callingCode: 'string?',
formattedPhoneNumber: 'string?'
}
};
export const ProfileSchema = {
name: 'Profile',
primaryKey: 'key',
properties: {
key: 'int',
jobTitle: 'string',
companyName: 'string',
companyEmail: 'string',
companyPhoneNumber: 'string',
photoBytes: 'string?'
}
};
export const InvitesSchema = {
name: 'Invites',
primaryKey: 'key',
properties: {
key: 'int',
tagferIds: 'string[]',
phoneNumbers: 'string[]'
}
};
export const ContactsSchema = {
name: 'Contacts',
primaryKey: 'recordID',
properties: {
recordID: 'string'
}
};
export const SessionSchema = {
name: 'Session',
primaryKey: 'key',
properties: {
key: 'int',
sessionId: 'string'
}
}
export const SignupConfig = {
path: 'Signup.realm',
schema: [UserSchema, ProfileSchema, InvitesSchema, ContactsSchema]
};
export const AuthConfig = {
path: 'Auth.realm',
schema: [SessionSchema]
};

View File

@ -81,8 +81,7 @@ export default class ForgotPasswordScreen extends React.Component {
{/* TEXT INPUTS */}
<View style={{ flex: 3 }}>
<Spacer />
<FormLabel
required
<FormLabel
labelText={strings('common.contactFields.email_labelText').toUpperCase()}
textStyle={styles.labelStyle}
/>
@ -106,7 +105,7 @@ export default class ForgotPasswordScreen extends React.Component {
onPress={this.onRequestResetPress.bind(this)}
title={strings('settings.forgotPasswordScreen.submitButton')}
buttonStyle={styles.buttonStyle}
disabled={!isEmail(this.state.email)}
disabled={!isEmail(this.state.email) || this.state.loading}
loading={this.state.loading}
fontSize={20}
/>

View File

@ -1,12 +1,14 @@
import React from 'react';
import { Image, KeyboardAvoidingView, Platform, Dimensions, View, Text, Keyboard, TouchableWithoutFeedback } from 'react-native';
import { Image, KeyboardAvoidingView, Platform, Dimensions, View, Text, Keyboard, ScrollView, Alert } from 'react-native';
import { Button, FormInput } from 'react-native-elements';
import { FormLabel, Spacer, TextButton } from '../../components/common';
import { strings } from '../../locales/i18n';
import { fetchAuth } from '../../utils/fetch';
import { showFlashErrorMessage } from '../../utils/errorHandler';
import { saveAuthState } from '../../realm/actions/AuthActions';
import PasswordField from '../../components/new/PasswordField';
import endpoints from '../../config/apiEndpoints';
import errors from '../../config/errors';
import colors from '../../config/colors.json';
@ -26,30 +28,23 @@ export default class LoginScreen extends React.Component {
};
}
/**
* Handles the login press performs the network request and parses the response
* and displays the error if needed.
*/
onLoginPress() {
async onLoginPress() {
Keyboard.dismiss();
this.setState({ loading: true });
const body = this.getLoginRequestBody();
const resp = await fetchAuth(endpoints.login, body)
this.setState({ loading: false });
fetchAuth(endpoints.login, body).then(res => {
this.setState({ loading: false });
if (res.error) {
this.clearInvalidInputField(res.error);
showFlashErrorMessage(res.error);
}
}).catch(() => {
this.setState({ loading: false, password: '' });
showFlashErrorMessage(errors.APP_NETWORK_ERROR);
});
if (resp.error) {
this.clearInvalidInputField(resp.error);
showFlashErrorMessage(resp.error);
} else {
saveAuthState(resp.sessionId);
Alert.alert('Signup Success 🎉', "Check back in a couple of weeks for more features.");
}
}
/**
* Creates the body of the request from the current state. It determines if the identifier is an email or a tagferId.
*/
// Creates the body of the request from the current state; determines if the identifier is an email or a tagferId.
getLoginRequestBody() {
const body = { password: this.state.password };
if (this.state.id.indexOf('@') !== -1) {
@ -72,13 +67,12 @@ export default class LoginScreen extends React.Component {
* Validation for the disbaling the button, if the there is id or password is less than 6.
*/
isButtonDiabled() {
return this.state.id.length < 1 || this.state.password.length < 6;
return this.state.id.length < 1 || this.state.password.length < 6 || this.state.loading;
}
renderContent() {
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
<View style={{ flex: 1 }}>
<ScrollView contentContainerStyle={{ flexGrow: 1, backgroundColor: colors.white }} keyboardShouldPersistTaps="handled" >
<View style={{ flex: 1 }}>
{/* LOGO */}
<View style={styles.logoContainerStyle}>
@ -92,7 +86,6 @@ export default class LoginScreen extends React.Component {
<View style={{ flex: 3 }}>
<Spacer />
<FormLabel
required
labelText={strings('common.contactFields.email_or_tagferid_label')}
textStyle={styles.labelStyle}
/>
@ -107,18 +100,15 @@ export default class LoginScreen extends React.Component {
<Spacer />
<FormLabel
required
labelText={strings('userLoginScreen.password')}
textStyle={styles.labelStyle}
/>
<FormInput
secureTextEntry
value={this.state.password}
onChangeText={password => this.setState({ password })}
placeholder={strings('userLoginScreen.password_placeholder')}
autoCorrect={false}
autoCapitalize='none'
<PasswordField
value={this.state.password}
onChangeText={password => this.setState({ password })}
inputStyle={styles.inputTextStyle}
visible={this.state.passwordVisible}
onEyePress={() => this.setState({ passwordVisible: !this.state.passwordVisible })}
/>
<Spacer />
{/* FORGOT PASSWORD OR SIGNUP */}
@ -148,8 +138,8 @@ export default class LoginScreen extends React.Component {
disabled={this.isButtonDiabled()}
fontSize={20}
/>
</View>
</TouchableWithoutFeedback>
</ScrollView>
);
}
@ -196,7 +186,6 @@ const styles = {
backgroundColor: colors.buttonBlue
},
inputTextStyle: {
marginLeft: 15,
color: colors.black
}
};

View File

@ -7,12 +7,15 @@ import { strings } from '../../locales/i18n';
import { fetchAuth } from '../../utils/fetch';
import { getErrorObject, showFlashErrorMessage } from '../../utils/errorHandler';
import { isEmail, isEmpty } from '../../utils/aux';
import { loadSignupState, saveSignupState } from '../../realm/actions/AuthActions';
import PasswordField from '../../components/new/PasswordField';
import logo from '../../../assets/logo-round.png';
import colors from '../../config/colors.json';
import endpoints from '../../config/apiEndpoints';
import errors from '../../config/errors';
export default class SignupScreenOne extends React.Component {
static navigationOptions = {
header: null
@ -26,43 +29,42 @@ export default class SignupScreenOne extends React.Component {
email: '',
password: '',
loading: false,
error: ''
error: '',
passwordVisible: false
};
loadSignupState(1).then(result => {
this.setState({ ...result.prevState });
this.isUpdate = result.isUpdate;
});
}
/**
* Handles the button press by sending the request, based on the response received either navigates the user
* to SignupScreenTwo or displays an error message.
*/
onSaveButtonPress() {
// Saves the current information locally and handles error processing
async onSaveButtonPress() {
Keyboard.dismiss();
this.setState({ loading: true });
const endpointWithParams = endpoints.emailExists(this.state.email);
fetchAuth(endpointWithParams).then(res => {
this.setState({ loading: false });
if (res.error) {
showFlashErrorMessage(res.error);
} else if (res.result === true) {
this.input.shake();
const errorObject = getErrorObject(errors.AUTH_EMAIL_ALREADY_EXISTS);
this.setState({ error: errorObject.desc });
} else {
this.props.navigation.navigate('signup2');
}
}).catch(() => {
this.setState({ loading: false });
showFlashErrorMessage(errors.APP_NETWORK_ERROR);
});
const resp = await fetchAuth(endpoints.emailExists(this.state.email));
this.setState({ loading: false });
if (resp.error) {
showFlashErrorMessage(resp.error);
} else if (resp.result === true) {
this.input.shake();
const errorObject = getErrorObject(errors.AUTH_EMAIL_ALREADY_EXISTS);
this.setState({ error: errorObject.desc });
} else {
this.props.navigation.navigate('signup2');
saveSignupState(1, this.state, this.isUpdate);
}
}
/**
* Boolean function that says if the button is disabled based on if the fields are empty.
*/
isButtonDisabled() {
const { firstName, lastName, email, password, error } = this.state;
return isEmpty(firstName) || isEmpty(lastName) || !isEmail(email) || password.length < 6 || !isEmpty(error);
const { firstName, lastName, email, password, error, loading } = this.state;
return isEmpty(firstName) || isEmpty(lastName) || !isEmail(email) || password.length < 6 || !isEmpty(error) || loading;
}
renderContent() {
@ -75,15 +77,12 @@ export default class SignupScreenOne extends React.Component {
</View>
{/* HEADER */}
<View>
<Text style={styles.headerStyle} >Signup</Text>
</View>
<Text style={styles.headerStyle} >Signup</Text>
{/* TEXT INPUTS */}
<View style={{ flex: 3 }}>
<Spacer />
<FormLabel
required
labelText={strings('common.contactFields.firstName_labelText')}
textStyle={styles.labelStyle}
/>
@ -98,7 +97,6 @@ export default class SignupScreenOne extends React.Component {
<Spacer />
<FormLabel
required
labelText={strings('common.contactFields.lastName_labelText')}
textStyle={styles.labelStyle}
/>
@ -113,7 +111,6 @@ export default class SignupScreenOne extends React.Component {
<Spacer />
<FormLabel
required
labelText={strings('common.contactFields.email_labelText')}
textStyle={styles.labelStyle}
/>
@ -131,19 +128,16 @@ export default class SignupScreenOne extends React.Component {
</View>
<FormLabel
required
labelText={strings('userCreateFlow.signUp.password')}
textStyle={styles.labelStyle}
containerStyle={{ marginTop: 0 }}
/>
<FormInput
secureTextEntry
value={this.state.password}
onChangeText={password => this.setState({ password })}
placeholder={strings('userCreateFlow.signUp.minPassword')}
autoCorrect={false}
autoCapitalize='none'
<PasswordField
value={this.state.password}
onChangeText={password => this.setState({ password })}
inputStyle={styles.inputTextStyle}
visible={this.state.passwordVisible}
onEyePress={() => this.setState({ passwordVisible: !this.state.passwordVisible })}
/>
{/* TERMS AND CONDITIONS */}
@ -157,17 +151,15 @@ export default class SignupScreenOne extends React.Component {
</View>
</View>
<View>
<Button
onPress={this.onSaveButtonPress.bind(this)}
title={strings('common.buttons.saveAndContinue')}
buttonStyle={styles.buttonStyle}
disabled={this.isButtonDisabled()}
loading={this.state.loading}
fontSize={20}
iconRight={styles.chevronIconStyle}
/>
</View>
<Button
onPress={this.onSaveButtonPress.bind(this)}
title={strings('common.buttons.saveAndContinue')}
buttonStyle={styles.buttonStyle}
disabled={this.isButtonDisabled()}
loading={this.state.loading}
fontSize={20}
iconRight={styles.chevronIconStyle}
/>
</View>
</ScrollView>
);
@ -192,7 +184,7 @@ const styles = {
headerStyle: {
color: colors.headerBlue,
fontWeight: 'bold',
marginLeft: 20,
marginLeft: 15,
fontSize: 24
},
logoContainerStyle: {
@ -207,7 +199,7 @@ const styles = {
},
buttonStyle: {
width: SCREEN_WIDTH * 0.9,
marginTop: 15,
marginTop: 5,
marginBottom: 30,
borderRadius: 5,
backgroundColor: colors.buttonBlue
@ -219,7 +211,6 @@ const styles = {
style: { marginLeft: 0 }
},
inputTextStyle: {
marginLeft: 15,
color: colors.black
},
formValidationStyle: {
@ -239,7 +230,6 @@ const styles = {
fontWeight: 'bold'
},
labelStyle: {
color: colors.labelBlue,
textTransform: 'uppercase'
color: colors.labelBlue
}
};

View File

@ -6,6 +6,7 @@ import { FormLabel } from '../../components/common';
import { strings } from '../../locales/i18n';
import { isEmpty } from '../../utils/aux';
import { getErrorObject, showFlashErrorMessage } from '../../utils/errorHandler';
import { loadSignupState, saveSignupState } from '../../realm/actions/AuthActions';
import { fetchAuth } from '../../utils/fetch';
import colors from '../../config/colors.json';
@ -30,6 +31,10 @@ export default class SignupScreenTwo extends React.Component {
twitterUsername: false
};
loadSignupState(2).then(result => {
this.isUpdate = result.isUpdate;
this.setState({ ...result.prevState });
});
this.keyboardDidHide = this.keyboardDidHide.bind(this);
this.onChangeText = this.onChangeText.bind(this);
this.openTwitterWebView = this.openTwitterWebView.bind(this);
@ -56,34 +61,24 @@ export default class SignupScreenTwo extends React.Component {
this.setState({ tagferId, twitterDisabled: true, error });
}
/**
* Calls Tagfer API to check if the tagferId exists.
* Toggles the loading state.
* Displays an error if it does.
* Displays a flash message if there are any other errors.
*/
onSaveButtonPress() {
async onSaveButtonPress() {
Keyboard.dismiss();
this.setState({ loading: true });
const endpointWithParams = endpoints.tagferIdExists(this.state.tagferId.toLowerCase());
const tagferId = this.state.tagferId;
const resp = await fetchAuth(endpoints.tagferIdExists(tagferId));
this.setState({ loading: false });
fetchAuth(endpointWithParams).then(res => {
this.setState({ loading: false });
if (res.error) {
showFlashErrorMessage(res.error);
} else if (res.result === true) {
this.input.shake();
const errorObject = getErrorObject(errors.AUTH_UID_ALREADY_EXISTS);
this.setState({ error: errorObject.desc });
} else {
this.props.navigation.navigate('signup3a');
}
}).catch(() => {
this.setState({ loading: false });
showFlashErrorMessage(errors.APP_NETWORK_ERROR);
});
if (resp.error) {
showFlashErrorMessage(resp.error);
} else if (resp.result === true) {
this.input.shake();
const errorObject = getErrorObject(errors.AUTH_UID_ALREADY_EXISTS);
this.setState({ error: errorObject.desc });
} else {
this.props.navigation.navigate('signup3a');
saveSignupState(2, this.state, this.isUpdate);
}
}
// Wrapper to set the tagferId to the twitter username
@ -94,12 +89,14 @@ export default class SignupScreenTwo extends React.Component {
// Network request to our API to get an OAuth token
async getTwitterAuthURL() {
this.setState({ loading: true });
const twitter = { authenticate: 'https://api.twitter.com/oauth/authenticate' };
const result = await fetchAuth(endpoints.twitterToken);
if (result.error) { throw result.error; }
const resp = await fetchAuth(endpoints.twitterToken);
this.setState({ loading: false });
if (resp.error) { throw resp.error; }
return `${twitter.authenticate}?oauth_token=${result.token}`;
return `${twitter.authenticate}?oauth_token=${resp.token}`;
}
// Navigates to a twitter webview modal, when the username is fetched successfully it will execute onSuccess
@ -122,7 +119,7 @@ export default class SignupScreenTwo extends React.Component {
// Button is diabled if there is no tagferId or there is an error
isButtonDisabled() {
return isEmpty(this.state.tagferId) || !isEmpty(this.state.error);
return isEmpty(this.state.tagferId) || !isEmpty(this.state.error) || this.state.loading;
}
// Disbaled if user is typing or we are during a request
@ -169,7 +166,6 @@ export default class SignupScreenTwo extends React.Component {
{/* TEXT INPUT */}
<FormLabel
required
labelText={strings('common.contactFields.tagferId_labelText')}
textStyle={styles.labelStyle}
/>
@ -295,7 +291,6 @@ const styles = {
color
}),
inputTextStyle: {
marginLeft: 15,
color: colors.black
},
examplesStyle: {
@ -311,7 +306,6 @@ const styles = {
fontSize: 17
},
labelStyle: {
color: colors.labelBlue,
textTransform: 'uppercase'
color: colors.labelBlue
}
};

View File

@ -9,8 +9,8 @@ import { showFlashErrorMessage } from '../../utils/errorHandler';
import { fetchAuth } from '../../utils/fetch';
import colors from '../../config/colors.json';
import errors from '../../config/errors';
import endpoints from '../../config/apiEndpoints';
import { loadSignupState, saveSignupState } from '../../realm/actions/AuthActions';
export default class SignupScreenThreeA extends React.Component {
@ -25,30 +25,34 @@ export default class SignupScreenThreeA extends React.Component {
super(props);
this.state = {
phoneNumber: '',
country: {
cca2: 'US',
callingCode: '1'
}
cca2: 'US',
callingCode: '1',
loading: false
};
loadSignupState(3).then(result => {
this.setState({ ...result.prevState });
this.isUpdate = result.isUpdate;
});
}
// Reformat the masked number and send the request
onSaveButtonPress() {
async onSaveButtonPress() {
this.setState({ loading: true });
const phoneNumber = `+${this.state.country.callingCode}${this.input.getRawValue()}`;
fetchAuth(endpoints.sendCode, { phoneNumber }).then(res => {
if (res.error) {
showFlashErrorMessage(res.error);
} else {
this.props.navigation.navigate('signup3b', { phoneNumber });
}
}).catch(() => showFlashErrorMessage(errors.APP_NETWORK_ERROR))
.finally(() => this.setState({ loading: false }));
const phoneNumber = `+${this.state.callingCode}${this.input.getRawValue()}`;
const resp = await fetchAuth(endpoints.sendCode, { phoneNumber });
this.setState({ loading: false });
if (resp.error) {
showFlashErrorMessage(resp.error);
} else {
this.props.navigation.navigate('signup3b', { phoneNumber });
saveSignupState(3, { ...this.state, formattedPhoneNumber: phoneNumber }, this.isUpdate);
}
}
// 14 chars include mask |(999) 999-9999|, its actually 10
isDisabled() {
return this.state.phoneNumber.length < 14;
return this.state.phoneNumber.length < 14 || this.state.loading;
}
renderContent() {
@ -63,15 +67,14 @@ export default class SignupScreenThreeA extends React.Component {
<CountryPicker
closeable
filterable
onChange={(country) => this.setState({ country })}
onClose={() => this.props.navigation.goBack()}
cca2={this.state.country.cca2}
onChange={(country) => this.setState({ ...country })}
cca2={this.state.cca2}
translation='eng'
styles={{ itemCountryName: { borderBottomWidth: 0 } }}
/>
{/* CALLING CODE */}
<Text style={styles.callingCodeText}>+{this.state.country.callingCode}</Text>
<Text style={styles.callingCodeText}>+{this.state.callingCode}</Text>
{/* PHONE NUMBER */}
<TextInputMask

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Dimensions, KeyboardAvoidingView, Platform, ScrollView, Text, View } from 'react-native';
import { Dimensions, KeyboardAvoidingView, Platform, ScrollView, Text, View, TouchableOpacity } from 'react-native';
import { Button } from 'react-native-elements';
import { TextInputMask } from 'react-native-masked-text';
@ -23,33 +23,45 @@ export default class SignupScreenThreeB extends React.Component {
constructor(props) {
super(props);
this.state = {
code: ''
code: '',
loading: false
};
}
// Reformat the masked code and send the request
onSaveButtonPress() {
async onSaveButtonPress() {
this.setState({ loading: true });
const phoneNumber = this.props.navigation.state.params.phoneNumber;
const code = this.input.getRawValue();
fetchAuth(endpoints.verifyCode, { phoneNumber, code }).then(res => {
if (res.error) {
showFlashErrorMessage(res.error);
} else if (res.result === false) {
showFlashErrorMessage(errors.AUTH_VERIFICATION_CODE_MISMATCH);
} else {
this.props.navigation.navigate('signup4');
}
}).catch(() => showFlashErrorMessage(errors.APP_NETWORK_ERROR))
.finally(() => this.setState({ loading: false }));
const resp = await fetchAuth(endpoints.verifyCode, { phoneNumber, code });
this.setState({ loading: false });
if (resp.error) {
showFlashErrorMessage(resp.error);
} else if (resp.result === false) {
showFlashErrorMessage(errors.AUTH_VERIFICATION_CODE_MISMATCH);
} else {
this.props.navigation.navigate('signup4');
}
}
async onResendPress() {
this.setState({ loading: true });
const phoneNumber = this.props.navigation.state.params.phoneNumber;
const resp = await fetchAuth(endpoints.sendCode, { phoneNumber });
this.setState({ loading: false });
if (resp.error) {
showFlashErrorMessage(resp.error);
}
}
isDisabled() {
return this.state.code.length !== 7;
return this.state.code.length !== 7 || this.state.loading;
}
renderContent() {
const phoneNumber = this.props.navigation.state.params.phoneNumber;
const phoneNumber = '';//this.props.navigation.state.params.phoneNumber;
return (
<ScrollView contentContainerStyle={{ flexGrow: 1, backgroundColor: colors.white }} keyboardShouldPersistTaps="handled">
@ -77,8 +89,15 @@ export default class SignupScreenThreeB extends React.Component {
/>
</View>
<Text style={styles.wrongNumberButtonStyle} onPress={() => this.props.navigation.goBack()}>Wrong Number?</Text>
{/* HELP BUTTONS */}
<View style={{ marginTop: 25 }}>
<TouchableOpacity onPress={() => this.props.navigation.goBack()}>
<Text style={styles.textButtonStyle}>Wrong Number?</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => this.onResendPress()}>
<Text style={styles.textButtonStyle}>Resend code?</Text>
</TouchableOpacity>
</View>
</View>
@ -151,11 +170,11 @@ const styles = {
size: 30,
style: { marginLeft: 0 }
},
wrongNumberButtonStyle: {
textButtonStyle: {
color: colors.darkBlue,
fontSize: 15,
fontSize: 16,
fontWeight: 'bold',
marginLeft: 20,
marginTop: 20
marginBottom: 20
}
};

View File

@ -0,0 +1,259 @@
/** Screen for creating default User Profile **/
import React from 'react';
import { Button, FormInput, Alert } from 'react-native-elements';
import { KeyboardAvoidingView, Platform, ScrollView, Text, View, TouchableOpacity } from 'react-native';
import OpenAppSettings from 'react-native-app-settings';
import ImagePicker from 'react-native-image-picker';
import Permissions from 'react-native-permissions';
import { strings } from '../../locales/i18n';
import { loadSignupState, saveSignupState } from '../../realm/actions/AuthActions';
import { FormLabel, Spacer, TextButton } from '../../components/common';
import styles from './styles';
import TagferAvatar from '../../components/new/TagferAvatar';
import { isEmail } from '../../utils/aux';
export default class SignupScreenFour extends React.Component {
static navigationOptions = {
title: strings('userCreateFlow.defaultProfileCreate.title'),
headerStyle: { borderBottomWidth: 0 },
headerTintColor: '#0D497E',
headerRight: <Text style={styles.navigationHeaderRightStyle}>4/6</Text>
};
constructor(props) {
super(props);
this.state = {
jobTitle: '',
companyName: '',
companyEmail: '',
companyPhoneNumber: '',
photoBytes: '',
loading: false
};
loadSignupState(4).then(result => {
this.isUpdate = result.isUpdate;
this.setState({ ...result.prevState });
});
this.renderAvatarPhoto = this.renderAvatarPhoto.bind(this);
this.onUploadPhotoPress = this.onUploadPhotoPress.bind(this);
this.onTakePhotoPress = this.onTakePhotoPress.bind(this);
}
onSaveButtonPress() {
this.props.navigation.navigate('signup5');
saveSignupState(4, this.state, this.isUpdate);
}
async onTakePhotoPress() {
this.setState({ loading: true });
try {
const response = await Permissions.request('camera');
if (response !== 'authorized') {
this.createSettingsAlert('Camera Access Deinied', 'To enable access, tap Settings and turn on Camera');
this.setState({ loading: false });
} else {
ImagePicker.launchCamera({ mediaType: 'photo' }, result => this.setState({ loading: false, photoBytes: result.data }));
}
} catch (error) {
console.log(error);
}
}
async onUploadPhotoPress() {
this.setState({ loading: true });
try {
const response = await Permissions.request('photo');
if (response !== 'authorized') {
this.createSettingsAlert('Photos Access Deinied', 'To enable access, tap Settings and turn on Photos');
this.setState({ loading: false });
} else {
ImagePicker.launchImageLibrary({ allowsEditing: true }, result => this.setState({ loading: false, photoBytes: result.data }));
}
} catch (error) {
console.log(error);
}
}
createSettingsAlert(title, message) {
Alert.alert(
title,
message,
[
{ text: 'Cancel', style: 'cancel' },
{ text: 'Settings', onPress: () => OpenAppSettings.open() }
],
{ cancelable: false }
);
}
isDisabled() {
const { jobTitle, companyName, companyEmail, companyPhoneNumber, loading } = this.state;
return !jobTitle || !companyName || !isEmail(companyEmail) || companyPhoneNumber.length < 10 || loading;
}
renderLinkedInButtons() {
return (
<TextButton
rightText={strings('common.contactFields.linkedIn_import')}
rightTextStyle={{ fontSize: 18, fontWeight: 'normal', color: '#53ACF0' }}
containerStyle={{ marginLeft: -2, marginTop: 15 }}
onPress={this.openLinkedInWebView}
disabled
/>
);
}
renderAvatarPhoto() {
let photoURL = '';
if (this.state.photoBytes) {
photoURL = `data:image/jpeg;base64,${this.state.photoBytes}`;
}
return (
<View style={styles.avatarStyle}>
<TagferAvatar size='xlarge' photoURL={photoURL} style={{ marginLeft: 25 }} color='#47C0E237' />
{this.renderPhotoButtons()}
</View>
);
}
renderPhotoButtons() {
return (
<View style={styles.buttonContainerStyle}>
<TouchableOpacity onPress={this.onUploadPhotoPress} disabled={this.props.loading}>
<Text style={styles.textButtonStyle}>{strings('common.buttons.uploadPhoto')}</Text>
</TouchableOpacity>
<Text style={styles.photoButtonsOrTextStyle}>{strings('profile.or')}</Text>
<TouchableOpacity onPress={this.onTakePhotoPress} disabled={this.props.loading}>
<Text style={styles.textButtonStyle}>{strings('common.buttons.takePhoto')}</Text>
</TouchableOpacity>
</View>
);
}
renderInputComponents(labelString, inputPlaceholderText, onChangeInput) {
return (
<View>
<FormLabel
labelText={labelString}
textStyle={styles.labelStyle}
/>
<FormInput
underlineColorAndroid={styles.colors.greyBlue}
value={this.state[onChangeInput]}
placeholder={inputPlaceholderText}
onChangeText={value => this.setState({ [onChangeInput]: value })}
inputStyle={styles.inputTextStyle}
autoCorrect={false}
autoCapitalize='none'
/>
</View>
);
}
renderContent() {
return (
<View style={{ flex: 1 }}>
<ScrollView contentContainerStyle={styles.parentContentScrollViewStyle} keyboardShouldPersistTaps="handled">
<View style={{ flex: 1 }}>
{/* INSTRUCTIONS */}
<Text style={styles.instructions}>{strings('userCreateFlow.defaultProfileCreate.instructions')}</Text>
<Spacer />
<View style={{ flex: 1, flexDirection: 'column', justifyContent: 'center' }}>
{this.renderAvatarPhoto()}
<Spacer />
{this.renderLinkedInButtons()}
{this.renderInputComponents(strings('common.contactFields.company_title_labelText'), strings('common.contactFields.company_title_placeholder'), 'jobTitle')}
{this.renderInputComponents(strings('common.contactFields.company_name_labelText'), strings('common.contactFields.company_name_placeholder'), 'companyName')}
{this.renderInputComponents(strings('common.contactFields.company_address_labelText'), strings('common.contactFields.company_address_placeholder'), 'companyEmail')}
{this.renderInputComponents(strings('common.contactFields.company_number_labelText'), strings('common.contactFields.company_number_placeholder'), 'companyPhoneNumber')}
<Text style={{ marginLeft: 17, color: '#ABABAB' }}>{strings('userCreateFlow.defaultProfileCreate.no_verification_needed')}</Text>
<Spacer />
</View>
</View>
</ScrollView>
{/* SAVE AND CONTINUE BUTTON */ }
<Button
onPress={this.onSaveButtonPress.bind(this)}
title={strings('common.buttons.saveAndContinue')}
disabled={this.isDisabled()}
loading={this.state.loading}
buttonStyle={styles.buttonStyle}
fontSize={20}
iconRight={styles.chevronIconStyle}
/>
</View>
);
}
render() {
if (Platform.OS === 'android') {
return this.renderContent();
}
// IOS
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
keyboardVerticalOffset={90}
behavior="padding"
>
{this.renderContent()}
</KeyboardAvoidingView>
);
}
}
// Moved here for debugging for now
// // Network request to our API to get an OAuth token
// async getLinkedInAuthURL() {
// console.log('getting linkedin auth info');
// this.setState({ loading: true });
// console.log('sending fetch request');
// const result = await fetchAuth(endpoints.linkedInLogin);
// if (result.error) {
// showFlashErrorMessage(result.error);
// return null;
// }
// return result;
// }
// async getLinkedInData(linkedInAuthData) {
// try {
// const result = await fetchAuth(endpoints.retrieveLinkedInInfo(toQueryString(linkedInAuthData)));
// if (result.error) {
// showFlashErrorMessage(result.error);
// } else {
// //this call will be altered once we gefull linkedin partner api access
// //this.setState({ profileInfo: result, loading: false });
// }
// } catch (error) {
// showFlashErrorMessage(error);
// this.setState({ loading: false });
// }
// }
// // Navigates to a LinkedIn webview modal, when the auth creds are fetched successfully it will execute onSuccess
// async openLinkedInWebView() {
// console.log('linkedWebview Func hit');
// this.setState({ loading: true });
// try {
// const result = await this.getLinkedInAuthURL();
// this.setState({ loading: false });
// console.log('navigating to linkedin webview');
// this.props.navigation.navigate('linkedInWebView', { result, onSuccess: this.getLinkedInData });
// } catch (error) {
// showFlashErrorMessage(error);
// this.setState({ loading: false });
// }
// }

View File

@ -1,284 +0,0 @@
/** Screen for creating default User Profile **/
import React from 'react';
import { Icon, FormInput, Alert, Linking } from 'react-native-elements';
import { Image, KeyboardAvoidingView, Platform, ScrollView, Text, View, Button, TouchableOpacity } from 'react-native';
import ImagePicker from 'react-native-image-picker';
import Permissions from 'react-native-permissions';
import { strings } from '../../../locales/i18n';
import { showFlashErrorMessage } from '../../../utils/errorHandler';
import { fetchAuth } from '../../../utils/fetch';
import endpoints from '../../../config/apiEndpoints';
import { toQueryString } from '../../../utils/aux';
import { FormLabel, Spacer, TextButton } from '../../../components/common';
import styles from './styles';
export default class SignupScreenFour extends React.Component {
static navigationOptions = {
title: strings('userCreateFlow.defaultProfileCreate.title'),
headerStyle: { borderBottomWidth: 0 },
headerTintColor: '#0D497E',
headerRight: <Text style={styles.navigationHeaderRightStyle}>4/6</Text>
};
constructor(props) {
super(props);
this.state = {
base64Img: null,
cameraRollStatus: '',
cameraStatus: '',
loading: false,
jobTitle: '',
companyName: '',
companyAddress: '',
companyNumber: ''
};
this.openLinkedInWebView = this.openLinkedInWebView.bind(this);
this.getLinkedInData = this.getLinkedInData.bind(this);
this.getLinkedInAuthURL = this.getLinkedInAuthURL.bind(this);
this.renderAvatarPhoto = this.renderAvatarPhoto.bind(this);
this.launchPicker = this.launchPicker.bind(this);
}
onSaveButtonPress() {
//this will be replaced with realm once it has been added so that we can persist the user data
//and send the object in this request
this.props.navigation.navigate('signup5', {
jobTitle: this.state.jobTitle,
companyName: this.state.companyName,
companyAddress: this.state.companyAddress,
companyNumber: this.state.companyNumber
});
}
async onImageButtonPress(pickerMethod, pickerTypeState) {
try {
if (this.state[pickerTypeState] !== 'authorized') {
const response = await Permissions.request('photo');
if (response !== 'authorized') {
Alert.alert(strings(`common.error.permissionDenied_camera${pickerTypeState === 'cameraRollStatus' ? 'Roll' : ''}`));
Linking.openURL('app-settings:');
this.setState({ [pickerTypeState]: response });
return;
} else if (response === 'authorized') {
const status = await Permissions.check('photo');
await this.launchPicker(pickerMethod, pickerTypeState, status);
}
}
} catch (error) {
console.log(error);
}
}
// Network request to our API to get an OAuth token
async getLinkedInAuthURL() {
console.log('getting linkedin auth info');
this.setState({ loading: true });
console.log('sending fetch request');
const result = await fetchAuth(endpoints.linkedInLogin);
if (result.error) {
showFlashErrorMessage(result.error);
return null;
}
return result;
}
async getLinkedInData(linkedInAuthData) {
try {
const result = await fetchAuth(endpoints.retrieveLinkedInInfo(toQueryString(linkedInAuthData)));
if (result.error) {
showFlashErrorMessage(result.error);
} else {
//this call will be altered once we gefull linkedin partner api access
//this.setState({ profileInfo: result, loading: false });
}
} catch (error) {
showFlashErrorMessage(error);
this.setState({ loading: false });
}
}
// Navigates to a LinkedIn webview modal, when the auth creds are fetched successfully it will execute onSuccess
async openLinkedInWebView() {
console.log('linkedWebview Func hit');
this.setState({ loading: true });
try {
const result = await this.getLinkedInAuthURL();
this.setState({ loading: false });
console.log('navigating to linkedin webview');
this.props.navigation.navigate('linkedInWebView', { result, onSuccess: this.getLinkedInData });
} catch (error) {
showFlashErrorMessage(error);
this.setState({ loading: false });
}
}
async launchPicker(pickerMethod, pickerTypeState, status) {
await ImagePicker[pickerMethod]({
allowsEditing: true,
mediaTypes: 'photo'
}, (libraryResponse) => {
if (libraryResponse.didCancel) {
console.log('User cancelled image picker');
} else if (libraryResponse.error) {
console.log('ImagePicker Error: ', libraryResponse.error);
} else if (libraryResponse.customButton) {
console.log('User tapped custom button: ', libraryResponse.customButton);
}
this.setState({
base64Img: libraryResponse.data,
[pickerTypeState]: status
});
});
}
renderLinkedInButtons() {
return (
<TextButton
rightText={strings('common.contactFields.linkedIn_import')}
rightTextStyle={{ fontSize: 20, fontWeight: 'normal', color: styles.colors.grey }}
containerStyle={{ marginLeft: -2, marginTop: 15 }}
onPress={this.openLinkedInWebView}
disabled
/>
);
}
/**
* Renders the avatar cirle on profiles
**/
renderAvatarPhoto() {
if (this.state.base64Img) {
return (
<View style={styles.avatarStyle}>
<Image
source={{ uri: this.state.base64Img }}
style={styles.avatarStyle}
/>
{this.renderPhotoButtons()}
</View>
);
}
// No profile Image
return (
<View style={styles.noAvatarStyle}>
<Icon
name='account-circle'
size={150}
color={styles.colors.lightTeal}
/>
{this.renderPhotoButtons()}
</View>
);
}
/**
* Renders the photo buttons
**/
renderPhotoButtons() {
const self = this;
return (
<View style={styles.buttonContainerStyle}>
<TouchableOpacity
onPress={this.onImageButtonPress.bind(self, 'launchImageLibrary', 'cameraRollStatus')}
disabled={this.props.loading}
>
<Text style={styles.textButtonStyle}>{strings('common.buttons.uploadPhoto')}</Text>
</TouchableOpacity>
<Text style={styles.photoButtonsOrTextStyle}>{strings('profile.or')}</Text>
<TouchableOpacity
onPress={this.onImageButtonPress.bind(self, 'launchCamera', 'cameraStatus')}
disabled={this.props.loading}
>
<Text style={styles.textButtonStyle}>{strings('common.buttons.takePhoto')}</Text>
</TouchableOpacity>
</View>
);
}
renderInputComponents(labelString, inputPlaceholderText, onChangeInput) {
return (
<View>
<FormLabel
bold
labelText={labelString}
textStyle={styles.labelStyle}
/>
<FormInput
underlineColorAndroid={styles.colors.greyBlue}
placeholder={inputPlaceholderText}
onChangeText={value => this.setState({ [onChangeInput]: value })}
style={styles.inputTextStyle}
autoCorrect={false}
autoCapitalize='none'
/>
</View>
);
}
renderContent() {
return (
<ScrollView contentContainerStyle={styles.parentContentScrollViewStyle} keyboardShouldPersistTaps="handled">
<View style={{ flex: 1 }}>
{/* INSTRUCTIONS */}
<Text style={styles.instructions}>{strings('userCreateFlow.defaultProfileCreate.instructions')}</Text>
<Spacer />
<View
style={{
flex: 1,
flexDirection: 'column',
justifyContent: 'center'
}}
>
{this.renderAvatarPhoto()}
<Spacer />
{this.renderLinkedInButtons()}
{this.renderInputComponents(strings('common.contactFields.company_title_labelText'), strings('common.contactFields.company_title_placeholder'), 'jobTitle')}
{this.renderInputComponents(strings('common.contactFields.company_name_labelText'), strings('common.contactFields.company_name_placeholder'), 'companyName')}
{this.renderInputComponents(strings('common.contactFields.company_address_labelText'), strings('common.contactFields.company_address_placeholder'), 'companyAddress')}
{this.renderInputComponents(strings('common.contactFields.company_number_labelText'), strings('common.contactFields.company_number_placeholder'), 'companyNumber')}
<Text>{strings('userCreateFlow.defaultProfileCreate.no_verification_needed')}</Text>
<Spacer />
</View>
</View>
{/* SAVE AND CONTINUE BUTTON */}
<Button
onPress={this.onSaveButtonPress.bind(this)}
title={strings('common.buttons.saveAndContinue')}
loading={this.state.loading}
style={styles.buttonStyle}
fontSize={20}
/>
</ScrollView>
);
}
/**
* Render method
**/
render() {
if (Platform.OS === 'android') {
return this.renderContent();
}
// IOS
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
keyboardVerticalOffset={90}
behavior="padding"
>
{this.renderContent()}
</KeyboardAvoidingView>
);
}
}

View File

@ -1,95 +0,0 @@
import { SCREEN_WIDTH, colors } from '../../styleVars';
const styles = {
logoContainerStyle: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
},
logoStyle: {
marginTop: 20,
width: 120,
height: 120,
borderWidth: 1,
borderRadius: 60
},
buttonStyle: {
width: SCREEN_WIDTH * 0.9,
marginTop: 15,
marginBottom: 30,
borderRadius: 5,
backgroundColor: colors.buttonBlue
},
inputTextStyle: {
marginLeft: 15,
color: colors.black,
borderBottomWidth: 1,
borderBottomColor: colors.black,
},
instructions: {
fontSize: 16,
fontFamily: 'Sans Serif',
textAlign: 'left',
color: colors.darkGrey,
marginBottom: 5,
marginHorizontal: 10
},
freeTextStyle: {
left: 20,
marginTop: 10,
fontSize: 18
},
navigationHeaderRightStyle: {
color: colors.middleGrey,
marginRight: 5
},
parentContentScrollViewStyle: {
flexGrow: 1,
backgroundColor: colors.white
},
buttonContainerStyle: {
flex: 2,
marginTop: 15,
flexDirection: 'column',
justifyContent: 'center'
},
photoButtonsOrTextStyle: {
fontSize: 20,
fontWeight: 'bold',
color: colors.lightGrey,
marginTop: 5,
marginBottom: 5,
marginLeft: 15
},
avatarStyle: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginLeft: 25
},
noAvatarStyle: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginLeft: 10
},
textButtonStyle: {
fontSize: 20,
fontWeight: 'bold',
color: colors.labelBlue,
marginTop: 5,
marginBottom: 5,
marginLeft: 0
},
labelStyle: {
fontWeight: 'bold',
color: colors.labelBlue,
marginTop: 5,
marginLeft: 0
},
colors
};
export default styles;

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Alert, Dimensions, Text, View, FlatList, TouchableOpacity, Platform } from 'react-native';
import { Alert, Dimensions, Text, View, FlatList, TouchableOpacity, Platform, ActivityIndicator } from 'react-native';
import { Button, CheckBox } from 'react-native-elements';
import OpenAppSettings from 'react-native-app-settings';
import Permissions from 'react-native-permissions';
@ -7,12 +7,12 @@ import Contacts from 'react-native-contacts';
import { strings } from '../../locales/i18n';
import { fetchAuth } from '../../utils/fetch';
import { showFlashErrorMessage } from '../../utils/errorHandler';
import { loadSignupState, saveSignupState, isContactSelected } from '../../realm/actions/AuthActions';
import colors from '../../config/colors.json';
import errors from '../../config/errors';
import endpoints from '../../config/apiEndpoints';
import ContactListItem from '../../components/lists/ContactListItem';
import ContactListItem from '../../components/new/ContactListItem';
export default class SignupScreenFive extends React.Component {
static navigationOptions = {
@ -26,54 +26,71 @@ export default class SignupScreenFive extends React.Component {
super(props);
this.state = {
allContacts: [],
selectedContacts: new Map()
selectedContacts: new Map(),
isUpdate: false,
loading: true
};
loadSignupState(5).then(result => {
this.isUpdate = result.isUpdate;
this.setState({ ...result.prevState }, () => this.fetchContacts());
});
this.onSelectAll = this.onSelectAll.bind(this);
this.fetchContacts();
this.onSaveButtonPress = this.onSaveButtonPress.bind(this);
}
// Handles when the user presses on the select all button
onSelectAll() {
const { allContacts } = this.state;
const selectedContacts = new Map();
if (this.state.selectedContacts.size === 0) {
for (let i = 0; i < allContacts.length; i++) {
selectedContacts.set(i, allContacts[i].phoneNumbers);
}
const { allContacts, selectedContacts } = this.state;
if (allContacts.length === selectedContacts.size) {
this.setState({ selectedContacts: new Map() });
} else {
this.setState({ selectedContacts: this.selectAllContacts(allContacts) });
}
this.setState({ selectedContacts });
}
// When a user clicks on a single contact
onSelectContact(index) {
onSelectContact(recordID, index) {
this.setState((state) => {
const selectedContacts = new Map(state.selectedContacts);
if (selectedContacts.has(index)) {
selectedContacts.delete(index);
if (selectedContacts.has(recordID)) {
selectedContacts.delete(recordID);
} else {
const phoneNumbers = state.allContacts[index].phoneNumbers;
selectedContacts.set(index, phoneNumbers);
const phoneNumbers = this.state.allContacts[index].phoneNumbers;
selectedContacts.set(recordID, phoneNumbers);
}
return { selectedContacts };
});
}
// Sends the request and navigates the user to next page.
onSaveButtonPress() {
// Sends the request and navigates the user to next page.
// Saves the users selected contacts and saves the request result in realm to be processed later.
async onSaveButtonPress() {
const { selectedContacts } = this.state;
saveSignupState(-5, selectedContacts.keys());
if (selectedContacts.size === 0) {
this.props.navigation.navigate('signup6');
return saveSignupState(5, { tagferIds: [], phoneNumbers: [] }, this.isUpdate);
}
this.setState({ loading: true });
const phoneNumbers = [];
for (const record of selectedContacts.values()) {
phoneNumbers.push(...record.map(entry => entry.number));
}
fetchAuth(endpoints.findUsersByPhone, { phoneNumbers })
.then(res => console.log(res))
.catch(() => console.log(errors.APP_NETWORK_ERROR));
const resp = await fetchAuth(endpoints.findUsersByPhone, { phoneNumbers });
this.setState({ loading: false });
if (resp.error) {
return showFlashErrorMessage(resp.error);
}
this.props.navigation.navigate('signup6');
saveSignupState(5, { tagferIds: resp.inNetwork, phoneNumbers: resp.outNetwork }, this.isUpdate);
}
// BEGIN: Fetch contacts
@ -82,12 +99,8 @@ export default class SignupScreenFive extends React.Component {
if (error) {
this.props.navigation.navigate('signup6');
} else {
const selectedContacts = new Map();
for (let i = 0; i < contacts.length; i++) {
selectedContacts.set(i, contacts[i].phoneNumbers);
}
this.setState({ allContacts: contacts, selectedContacts });
const selectedContacts = this.isUpdate ? this.restoreSelectedContacts(contacts) : this.selectAllContacts(contacts);
this.setState({ allContacts: contacts, selectedContacts, loading: false });
}
});
}
@ -99,6 +112,26 @@ export default class SignupScreenFive extends React.Component {
});
}
selectAllContacts(contacts) {
const selectedContacts = new Map();
for (let i = 0; i < contacts.length; i++) {
selectedContacts.set(contacts[i].recordID, contacts[i].phoneNumbers);
}
return selectedContacts;
}
restoreSelectedContacts(contacts) {
const selectedContacts = new Map();
for (let i = 0; i < contacts.length; i++) {
if (isContactSelected(contacts[i].recordID)) {
selectedContacts.set(contacts[i].recordID, contacts[i].phoneNumbers);
}
}
return selectedContacts;
}
createSettingsAlert() {
Alert.alert(
'Contacts Access Denied',
@ -106,7 +139,8 @@ export default class SignupScreenFive extends React.Component {
[
{ text: 'Cancel', onPress: () => this.props.navigation.navigate('signup6') },
{ text: 'Settings', onPress: () => OpenAppSettings.open() }
]
],
{ cancelable: false }
);
}
@ -127,8 +161,16 @@ export default class SignupScreenFive extends React.Component {
// END: Fetch Contacts
renderItem(contact, index) {
const selected = this.state.selectedContacts.has(index);
return <ContactListItem contact={contact} selected={selected} onPress={() => this.onSelectContact(index)} />;
const selected = this.state.selectedContacts.has(contact.recordID);
return <ContactListItem contact={contact} selected={selected} onPress={() => this.onSelectContact(contact.recordID, index)} />;
}
renderActivityIndicator() {
return (
<View style={{ flexDirection: 'row', justifyContent: 'center', marginTop: 20 }}>
<ActivityIndicator size='large' color={colors.labelBlue} />
</View>
);
}
render() {
@ -136,7 +178,7 @@ export default class SignupScreenFive extends React.Component {
<View style={styles.container}>
{/* INSTRUCTIONS */}
<Text style={styles.instructions}>{strings('contact.importScreen_description')}</Text>
{/* FLAT LIST OF CONTACTS */}
<FlatList
ListHeaderComponent={<ListHeader isChecked={this.state.selectedContacts.size === this.state.allContacts.length} onPress={this.onSelectAll} onSkip={() => this.props.navigation.navigate('signup6')} />}
@ -145,12 +187,15 @@ export default class SignupScreenFive extends React.Component {
renderItem={({ item, index }) => this.renderItem(item, index)}
keyExtractor={(item) => item.recordID}
style={{ flex: 1, marginHorizontal: 10 }}
ListEmptyComponent={this.renderActivityIndicator()}
/>
{/* SAVE AND CONTINUE BUTTON */}
<Button
onPress={this.onSaveButtonPress.bind(this)}
onPress={this.onSaveButtonPress}
title={strings('common.buttons.saveAndContinue')}
loading={this.state.loading}
disabled={this.state.loading}
buttonStyle={styles.buttonStyle}
fontSize={20}
iconRight={styles.chevronIconStyle}

View File

@ -1,5 +1,5 @@
import React from 'react';
import { Dimensions, Text, View, FlatList, TouchableOpacity } from 'react-native';
import { Alert, Dimensions, Text, View, FlatList, TouchableOpacity } from 'react-native';
import { Button } from 'react-native-elements';
import { strings } from '../../locales/i18n';
@ -11,6 +11,7 @@ import ListActivityIndicator from '../../components/new/ListActivityIndicator';
import colors from '../../config/colors.json';
import errors from '../../config/errors';
import endpoints from '../../config/apiEndpoints';
import { loadSignupState, saveAuthState } from '../../realm/actions/AuthActions';
export default class SignupScreenSix extends React.Component {
static navigationOptions = {
@ -24,17 +25,22 @@ export default class SignupScreenSix extends React.Component {
super(props);
this.state = {
users: [],
selectedUsers: new Map()
selectedUsers: new Map(),
loading: true
};
this.onLoad();
this.onSignup = this.onSignup.bind(this);
}
// Load suggested contacts
onLoad() {
fetchAuth(endpoints.suggestProfiles)
.then(result => this.setState({ users: result.profiles }))
.catch(() => showFlashErrorMessage(errors.APP_NETWORK_ERROR));
async onLoad() {
const resp = await fetchAuth(endpoints.suggestProfiles);
this.setState({ loading: false, users: resp.profiles });
if (resp.error) {
showFlashErrorMessage(errors.APP_NETWORK_ERROR);
}
}
// Add user to selected map
@ -51,11 +57,61 @@ export default class SignupScreenSix extends React.Component {
});
}
// Implement the signup functionality
// TODO: Add all the Realm handlers to handle saving the data
onSignup() {
console.log('Unimplemented Signup');
console.log([...this.state.selectedUsers.values()]);
async onSignup() {
this.setState({ loading: true });
const { prevState } = await loadSignupState(6);
const { user, profile, invites } = prevState;
const invitesObject = this.createInvitesObject(invites, this.state.selectedUsers.values());
const requestBody = this.createSignupRequest(user, profile, invitesObject);
console.log(requestBody);
const resp = await fetchAuth(endpoints.signup, requestBody);
this.setState({ loading: false });
if (resp.error) {
showFlashErrorMessage(resp.error);
} else {
saveAuthState(resp.sessionId);
Alert.alert('Signup Success 🎉', 'Check back next week for more features.');
}
}
createSignupRequest(user, profile, invites) {
const { email, password, firstName, lastName, tagferId, formattedPhoneNumber } = user;
const { jobTitle, companyName, companyEmail, companyPhoneNumber, photoBytes } = profile;
return {
user: {
email,
password,
tagferId,
phoneNumber: formattedPhoneNumber
},
profile: {
fullName: `${firstName} ${lastName}`,
jobTitle,
companyName,
companyEmail,
companyPhoneNumber,
photoBytes
},
invites: {
phoneNumbers: invites.phoneNumbers,
requests: invites.tagferIds
}
};
}
createInvitesObject(invites, suggested) {
const { tagferIds, phoneNumbers } = invites;
const set = new Set();
// Avoids sending duplicate tagferIds
for (const tagferId of suggested) { set.add(tagferId); }
for (let i = 0; i < tagferIds.length; i++) { set.add(tagferIds[i]); }
return {
tagferIds: [...set.values()],
phoneNumbers: [...phoneNumbers.values()]
};
}
// Render a single entry in the list
@ -82,10 +138,11 @@ export default class SignupScreenSix extends React.Component {
{/* SIGNUP BUTTON */}
<Button
onPress={this.onSignup.bind(this)}
onPress={this.onSignup}
title={strings('userCreateFlow.signUp.title')}
buttonStyle={styles.buttonStyle}
loading={this.state.loading}
disabled={this.state.loading}
fontSize={20}
/>
</View>

View File

@ -0,0 +1,91 @@
import { SCREEN_WIDTH, colors } from '../styleVars';
const styles = {
logoContainerStyle: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
},
logoStyle: {
marginTop: 20,
width: 120,
height: 120,
borderWidth: 1,
borderRadius: 60
},
buttonStyle: {
width: SCREEN_WIDTH * 0.9,
marginTop: 0,
marginBottom: 0,
borderRadius: 5,
backgroundColor: colors.buttonBlue
},
inputTextStyle: {
color: colors.black
},
instructions: {
fontSize: 16,
textAlign: 'left',
color: colors.darkGrey,
marginBottom: 5,
marginHorizontal: 10
},
freeTextStyle: {
left: 20,
marginTop: 10,
fontSize: 18
},
navigationHeaderRightStyle: {
color: colors.middleGrey,
marginRight: 5
},
parentContentScrollViewStyle: {
flexGrow: 1,
backgroundColor: colors.white
},
buttonContainerStyle: {
flex: 2,
marginLeft: 30,
flexDirection: 'column',
justifyContent: 'center'
},
photoButtonsOrTextStyle: {
fontSize: 22,
color: colors.lightGrey
},
avatarStyle: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#FAFAFA'
},
noAvatarStyle: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
marginLeft: 10
},
textButtonStyle: {
fontSize: 20,
color: colors.labelBlue,
marginTop: 5,
marginBottom: 5,
marginLeft: 0
},
labelStyle: {
color: colors.labelBlue,
marginTop: 5,
marginLeft: 0
},
chevronIconStyle: {
name: 'chevron-right',
color: colors.white,
size: 30,
style: { marginLeft: 0 }
},
colors
};
export default styles;

View File

@ -1,4 +1,5 @@
import appConfig from '../config/appConfig.json';
import errors from '../config/errors.js';
export function fetchAuth(endpoint, body) {
const request = {
@ -10,5 +11,5 @@ export function fetchAuth(endpoint, body) {
body: JSON.stringify(body)
};
return fetch(endpoint.url, request).then(res => res.json());
return fetch(endpoint.url, request).then(res => res.json()).catch(() => ({ error: errors.APP_NETWORK_ERROR }));
}