aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/assets/icons/purple_ring+.svg1
-rw-r--r--src/assets/icons/purple_ring.svg1
-rw-r--r--src/assets/icons/ring+.svg1
-rw-r--r--src/assets/icons/ring.svg1
-rw-r--r--src/components/common/SocialIcon.tsx3
-rw-r--r--src/components/common/SocialLinkModal.tsx118
-rw-r--r--src/components/common/index.ts1
-rw-r--r--src/components/onboarding/SocialMediaLinker.tsx112
-rw-r--r--src/components/taggs/Tagg.tsx146
-rw-r--r--src/components/taggs/TaggsBar.tsx101
-rw-r--r--src/constants/api.ts7
-rw-r--r--src/constants/constants.ts20
-rw-r--r--src/routes/authentication/AuthProvider.tsx60
-rw-r--r--src/routes/viewProfile/ProfileProvider.tsx49
-rw-r--r--src/screens/profile/SocialMediaTaggs.tsx33
-rw-r--r--src/services/SocialLinkingService.ts184
-rw-r--r--src/services/UserProfileService.ts40
-rw-r--r--src/services/index.ts1
18 files changed, 607 insertions, 272 deletions
diff --git a/src/assets/icons/purple_ring+.svg b/src/assets/icons/purple_ring+.svg
new file mode 100644
index 00000000..f86252ce
--- /dev/null
+++ b/src/assets/icons/purple_ring+.svg
@@ -0,0 +1 @@
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 216 216"><defs><style>.cls-1{fill:#8f01ff;}.cls-2{fill:url(#linear-gradient);}.cls-3{fill:#fff;}</style><linearGradient id="linear-gradient" x1="213.07" y1="207.1" x2="164.63" y2="162.74" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6ee7e7"/><stop offset="1" stop-color="#8f01ff"/></linearGradient></defs><path class="cls-1" d="M106.17,1.83A106.17,106.17,0,1,0,212.34,108,106.16,106.16,0,0,0,106.17,1.83Zm0,198.58A92.41,92.41,0,1,1,198.58,108,92.39,92.39,0,0,1,106.17,200.41Z"/><circle class="cls-2" cx="183.16" cy="179.71" r="32.84"/><rect class="cls-3" x="180.47" y="160.8" width="5.38" height="37.82" rx="2.45"/><rect class="cls-3" x="180.47" y="160.8" width="5.38" height="37.82" rx="2.45" transform="translate(3.45 362.87) rotate(-90)"/></svg> \ No newline at end of file
diff --git a/src/assets/icons/purple_ring.svg b/src/assets/icons/purple_ring.svg
new file mode 100644
index 00000000..2eef0d28
--- /dev/null
+++ b/src/assets/icons/purple_ring.svg
@@ -0,0 +1 @@
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216 216"><defs><style>.cls-1{fill:#8f01ff;}</style></defs><path class="cls-1" d="M108,1.83A106.17,106.17,0,1,0,214.17,108,106.16,106.16,0,0,0,108,1.83Zm0,198.58A92.41,92.41,0,1,1,200.41,108,92.39,92.39,0,0,1,108,200.41Z"/></svg> \ No newline at end of file
diff --git a/src/assets/icons/ring+.svg b/src/assets/icons/ring+.svg
new file mode 100644
index 00000000..6ff79515
--- /dev/null
+++ b/src/assets/icons/ring+.svg
@@ -0,0 +1 @@
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 216 216"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:url(#linear-gradient-2);}.cls-3{fill:#fff;}</style><linearGradient id="linear-gradient" x1="163.49" y1="206.28" x2="56.49" y2="22.83" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6ee7e7"/><stop offset="1" stop-color="#8f01ff"/></linearGradient><linearGradient id="linear-gradient-2" x1="213.07" y1="207.1" x2="164.63" y2="162.74" xlink:href="#linear-gradient"/></defs><path class="cls-1" d="M106.17,1.83A106.17,106.17,0,1,0,212.34,108,106.16,106.16,0,0,0,106.17,1.83Zm0,198.58A92.41,92.41,0,1,1,198.58,108,92.39,92.39,0,0,1,106.17,200.41Z"/><circle class="cls-2" cx="183.16" cy="179.71" r="32.84"/><rect class="cls-3" x="180.47" y="160.8" width="5.38" height="37.82" rx="2.45"/><rect class="cls-3" x="180.47" y="160.8" width="5.38" height="37.82" rx="2.45" transform="translate(3.45 362.87) rotate(-90)"/></svg> \ No newline at end of file
diff --git a/src/assets/icons/ring.svg b/src/assets/icons/ring.svg
new file mode 100644
index 00000000..4b7448a2
--- /dev/null
+++ b/src/assets/icons/ring.svg
@@ -0,0 +1 @@
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 216 216"><defs><style>.cls-1{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="165.32" y1="206.28" x2="58.33" y2="22.83" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#6ee7e7"/><stop offset="1" stop-color="#8f01ff"/></linearGradient></defs><path class="cls-1" d="M108,1.83A106.17,106.17,0,1,0,214.17,108,106.16,106.16,0,0,0,108,1.83Zm0,198.58A92.41,92.41,0,1,1,200.41,108,92.39,92.39,0,0,1,108,200.41Z"/></svg> \ No newline at end of file
diff --git a/src/components/common/SocialIcon.tsx b/src/components/common/SocialIcon.tsx
index a46b1445..84da1ca7 100644
--- a/src/components/common/SocialIcon.tsx
+++ b/src/components/common/SocialIcon.tsx
@@ -22,6 +22,9 @@ const SocialIcon: React.FC<SocialIconProps> = ({
case 'Twitter':
var icon = require('../../assets/images/twitter-icon.png');
break;
+ case 'Tiktok':
+ var icon = require('../../assets/images/tiktok-icon.png');
+ break;
case 'Twitch':
var icon = require('../../assets/images/twitch-icon.png');
break;
diff --git a/src/components/common/SocialLinkModal.tsx b/src/components/common/SocialLinkModal.tsx
new file mode 100644
index 00000000..3cea2567
--- /dev/null
+++ b/src/components/common/SocialLinkModal.tsx
@@ -0,0 +1,118 @@
+import React from 'react';
+import {Modal, StyleSheet, Text, TouchableHighlight, View} from 'react-native';
+import {TextInput} from 'react-native-gesture-handler';
+import {SCREEN_WIDTH} from '../../utils';
+
+interface SocialLinkModalProps {
+ modalVisible: boolean;
+ setModalVisible: (_: boolean) => void;
+ completionCallback: (username: string) => void;
+}
+
+const SocialLinkModal: React.FC<SocialLinkModalProps> = ({
+ modalVisible,
+ setModalVisible,
+ completionCallback,
+}) => {
+ const [username, setUsername] = React.useState('');
+ return (
+ <>
+ <View style={styles.centeredView}>
+ <Modal
+ animationType="slide"
+ transparent={true}
+ visible={modalVisible}
+ onRequestClose={() => {}}>
+ <View style={styles.centeredView}>
+ <View style={styles.modalView}>
+ <TextInput
+ autoCapitalize={'none'}
+ autoCorrect={false}
+ textAlign={'center'}
+ placeholder={'Your username'}
+ style={styles.textInput}
+ onChangeText={setUsername}
+ value={username}
+ />
+ {/* link button */}
+ <TouchableHighlight
+ style={styles.openButton}
+ onPress={() => {
+ setModalVisible(!modalVisible);
+ setUsername('');
+ completionCallback(username);
+ }}>
+ <Text style={styles.textStyle}>Link</Text>
+ </TouchableHighlight>
+ {/* cancel button */}
+ <Text
+ onPress={() => {
+ setUsername('');
+ setModalVisible(!modalVisible);
+ }}
+ style={styles.cancelStyle}>
+ Cancel
+ </Text>
+ </View>
+ </View>
+ </Modal>
+ </View>
+ </>
+ );
+};
+
+const styles = StyleSheet.create({
+ centeredView: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginTop: 22,
+ },
+ modalView: {
+ width: (SCREEN_WIDTH * 2) / 3,
+ margin: 20,
+ backgroundColor: 'white',
+ borderRadius: 20,
+ padding: 35,
+ alignItems: 'center',
+ shadowColor: '#000',
+ shadowOffset: {
+ width: 0,
+ height: 2,
+ },
+ shadowOpacity: 0.25,
+ shadowRadius: 3.84,
+ elevation: 5,
+ },
+ openButton: {
+ borderRadius: 20,
+ padding: 10,
+ elevation: 2,
+ backgroundColor: '#2196F3',
+ },
+ textStyle: {
+ color: 'white',
+ fontWeight: 'bold',
+ textAlign: 'center',
+ },
+ cancelStyle: {
+ position: 'relative',
+ height: 17,
+ top: 17,
+ fontStyle: 'normal',
+ fontWeight: '500',
+ fontSize: 14,
+ /* identical to box height */
+ textAlign: 'center',
+ color: '#698DD3',
+ },
+ textInput: {
+ height: 20,
+ width: '75%',
+ borderBottomWidth: 0.4,
+ borderBottomColor: '#C4C4C4',
+ marginBottom: 20,
+ },
+});
+
+export default SocialLinkModal;
diff --git a/src/components/common/index.ts b/src/components/common/index.ts
index cd72a70b..61d826bd 100644
--- a/src/components/common/index.ts
+++ b/src/components/common/index.ts
@@ -9,4 +9,5 @@ export {default as TabsGradient} from './TabsGradient';
export {default as RecentSearches} from '../search/RecentSearches';
export {default as LoadingIndicator} from './LoadingIndicator';
export {default as DateLabel} from './DateLabel';
+export {default as SocialLinkModal} from './SocialLinkModal';
export * from './post';
diff --git a/src/components/onboarding/SocialMediaLinker.tsx b/src/components/onboarding/SocialMediaLinker.tsx
index 15afb731..da637f99 100644
--- a/src/components/onboarding/SocialMediaLinker.tsx
+++ b/src/components/onboarding/SocialMediaLinker.tsx
@@ -1,24 +1,14 @@
-import AsyncStorage from '@react-native-community/async-storage';
import React from 'react';
import {
- Alert,
Image,
StyleSheet,
Text,
TouchableOpacity,
TouchableOpacityProps,
} from 'react-native';
-import InAppBrowser from 'react-native-inappbrowser-reborn';
import {LinkerType} from 'src/types';
-import {
- LINK_FB_ENDPOINT,
- LINK_FB_OAUTH,
- LINK_IG_ENDPOINT,
- LINK_IG_OAUTH,
- LINK_TWITTER_ENDPOINT,
- LINK_TWITTER_OAUTH,
-} from '../../constants';
import {SOCIAL_FONT_COLORS} from '../../constants/constants';
+import {handlePressForAuthBrowser} from '../../services';
import SocialIcon from '../common/SocialIcon';
interface SocialMediaLinkerProps extends TouchableOpacityProps {
@@ -29,102 +19,14 @@ const SocialMediaLinker: React.FC<SocialMediaLinkerProps> = ({
social: {label},
}) => {
const [state, setState] = React.useState({
- authenticated: false,
+ socialLinked: false,
});
- const integrated_endpoints: {[label: string]: [string, string]} = {
- Instagram: [LINK_IG_OAUTH, LINK_IG_ENDPOINT],
- Facebook: [LINK_FB_OAUTH, LINK_FB_ENDPOINT],
- Twitter: [LINK_TWITTER_OAUTH, LINK_TWITTER_ENDPOINT],
- };
-
- const registerSocialLink: (token: string) => Promise<boolean> = async (
- callback_url,
- ) => {
- if (!(label in integrated_endpoints)) {
- // This error is already handled earlier, more of a safety check here
- return false;
- }
- const user_token = await AsyncStorage.getItem('token');
- const response = await fetch(integrated_endpoints[label][1], {
- method: 'POST',
- headers: {
- Authorization: `Token ${user_token}`,
- },
- body: JSON.stringify({
- callback_url: callback_url,
- }),
- });
- if (!(response.status === 201)) {
- console.log(await response.json());
- }
- return response.status === 201;
- };
-
const handlePress = async () => {
- try {
- const isAvailable = await InAppBrowser.isAvailable();
- if (!(label in integrated_endpoints)) {
- // TODO handle non-integrated social links with a modal
- // TODO remove the alert below
- Alert.alert('Coming soon!');
- return;
- }
- let url = integrated_endpoints[label][0];
-
- // We will need to do an extra step for twitter sign-in
- if (label === 'Twitter') {
- const user_token = await AsyncStorage.getItem('token');
- const response = await fetch(url, {
- method: 'GET',
- headers: {
- Authorization: `Token ${user_token}`,
- },
- });
- url = response.url;
- }
-
- if (isAvailable) {
- InAppBrowser.openAuth(url, 'taggid://callback', {
- ephemeralWebSession: true,
- })
- .then(async (response) => {
- console.log(response);
- if (response.type === 'success' && response.url) {
- const success = await registerSocialLink(response.url);
- if (!success) {
- throw new Error('Unable to register with backend');
- }
- setState({
- ...state,
- authenticated: true,
- });
- Alert.alert(`Successfully linked ${label} 🎉`);
- } else {
- throw new Error(`Unable to link with ${label} API`);
- }
- })
- .catch((error) => {
- console.log(error);
- Alert.alert(`Something went wrong, we can't link with ${label} 😔`);
- });
- } else {
- // Okay... to open an external browser and have it link back to
- // the app is a bit tricky, we will need to have navigation routes
- // setup for this screen and have it hooked up.
- // See https://github.com/proyecto26/react-native-inappbrowser#authentication-flow-using-deep-linking
- // Though this isn't the end of the world, from the documentation,
- // the in-app browser should be supported from iOS 11, which
- // is about 98.5% of all iOS devices in the world.
- // See https://support.apple.com/en-gb/HT209574
- Alert.alert(
- 'Sorry! Your device was unable to open a browser to let you sign-in! 😔',
- );
- }
- } catch (error) {
- console.log(error);
- Alert.alert(`Something went wrong, we can't link with ${label} 😔`);
- }
+ setState({
+ ...state,
+ socialLinked: await handlePressForAuthBrowser(label),
+ });
};
switch (label) {
@@ -166,7 +68,7 @@ const SocialMediaLinker: React.FC<SocialMediaLinkerProps> = ({
style={styles.container}>
<SocialIcon social={label} style={styles.icon} />
<Text style={[styles.label, {color: font_color}]}>{label}</Text>
- {state.authenticated && (
+ {state.socialLinked && (
<Image
source={require('../../assets/images/link-tick.png')}
style={styles.tick}
diff --git a/src/components/taggs/Tagg.tsx b/src/components/taggs/Tagg.tsx
index 9274e0eb..c64da5ef 100644
--- a/src/components/taggs/Tagg.tsx
+++ b/src/components/taggs/Tagg.tsx
@@ -1,51 +1,141 @@
import {useNavigation} from '@react-navigation/native';
-import React from 'react';
-import {StyleSheet, TouchableOpacity, View} from 'react-native';
-import LinearGradient from 'react-native-linear-gradient';
-import {TAGGS_GRADIENT} from '../../constants';
+import React, {Fragment, useState} from 'react';
+import {Alert, Linking, StyleSheet, TouchableOpacity, View} from 'react-native';
+import PurpleRingPlus from '../../assets/icons/purple_ring+.svg';
+import PurpleRing from '../../assets/icons/purple_ring.svg';
+import RingPlus from '../../assets/icons/ring+.svg';
+import Ring from '../../assets/icons/ring.svg';
+import {INTEGRATED_SOCIAL_LIST, TAGG_ICON_DIM} from '../../constants';
+import {
+ handlePressForAuthBrowser,
+ registerNonIntegratedSocialLink,
+} from '../../services';
+import {SocialIcon, SocialLinkModal} from '../common';
interface TaggProps {
- style: object;
social: string;
isProfileView: boolean;
+ isLinked: boolean;
+ isIntegrated: boolean;
+ setTaggsNeedUpdate: (_: boolean) => void;
+ setSocialDataNeedUpdate: (_: string[]) => void;
}
-const Tagg: React.FC<TaggProps> = ({style, social, isProfileView}) => {
+const Tagg: React.FC<TaggProps> = ({
+ social,
+ isProfileView,
+ isLinked,
+ isIntegrated,
+ setTaggsNeedUpdate,
+ setSocialDataNeedUpdate,
+}) => {
const navigation = useNavigation();
+ const [modalVisible, setModalVisible] = useState(false);
+ const youMayPass = isLinked || isProfileView;
- return (
- <TouchableOpacity
- onPress={() =>
+ /*
+ case isProfileView:
+ case linked:
+ show normal ring, navigate to taggs view
+ case !linked:
+ don't show tagg
+ case !isProfileView:
+ case linked:
+ show normal ring, navigate to taggs view
+ case !linked:
+ show ring+, then...
+ case integrated_social:
+ show auth browser
+ case !integrated_social:
+ show modal
+ Tagg's "Tagg" will use the Ring instead of PurpleRing
+ */
+
+ const modalOrAuthBrowserOrPass = async () => {
+ if (youMayPass) {
+ if (INTEGRATED_SOCIAL_LIST.indexOf(social) !== -1) {
navigation.navigate('SocialMediaTaggs', {
socialMediaType: social,
isProfileView: isProfileView,
- })
- }>
- <LinearGradient
- colors={[TAGGS_GRADIENT.start, TAGGS_GRADIENT.end]}
- useAngle={true}
- angle={154.72}
- angleCenter={{x: 0.5, y: 0.5}}
- style={[styles.gradient, style]}>
- <View style={styles.image} />
- </LinearGradient>
- </TouchableOpacity>
+ });
+ } else {
+ // TODO: we don't know what the link is...?
+ Linking.openURL(
+ `http://google.com/search?q=take+me+to+${social}+profile+page`,
+ );
+ }
+ } else {
+ if (isIntegrated) {
+ handlePressForAuthBrowser(social).then((success) => {
+ setTaggsNeedUpdate(success);
+ setSocialDataNeedUpdate(success ? [social] : []);
+ });
+ } else {
+ setModalVisible(true);
+ }
+ }
+ };
+
+ const pickTheRightRingHere = () => {
+ if (youMayPass) {
+ if (social === 'Tagg') {
+ return <Ring width={TAGG_ICON_DIM} height={TAGG_ICON_DIM} />;
+ } else {
+ return <PurpleRing width={TAGG_ICON_DIM} height={TAGG_ICON_DIM} />;
+ }
+ } else {
+ if (social === 'Tagg') {
+ return <RingPlus width={TAGG_ICON_DIM} height={TAGG_ICON_DIM} />;
+ } else {
+ return <PurpleRingPlus width={TAGG_ICON_DIM} height={TAGG_ICON_DIM} />;
+ }
+ }
+ };
+
+ const linkNonIntegratedSocial = async (username: string) => {
+ if (await registerNonIntegratedSocialLink(social, username)) {
+ Alert.alert(`Successfully linked ${social} 🎉`);
+ setTaggsNeedUpdate(true);
+ } else {
+ // If we display too fast the alert will get dismissed with the modal
+ setTimeout(() => {
+ Alert.alert(`Something went wrong, we can't link with ${social} 😔`);
+ }, 500);
+ }
+ };
+
+ return (
+ <>
+ {isProfileView && !isLinked ? (
+ <Fragment />
+ ) : (
+ <TouchableOpacity onPress={modalOrAuthBrowserOrPass}>
+ <SocialLinkModal
+ modalVisible={modalVisible}
+ setModalVisible={setModalVisible}
+ completionCallback={linkNonIntegratedSocial}
+ />
+ <View style={styles.container}>
+ <SocialIcon style={styles.image} social={social} />
+ {pickTheRightRingHere()}
+ </View>
+ </TouchableOpacity>
+ )}
+ </>
);
};
const styles = StyleSheet.create({
- gradient: {
- width: 80,
- height: 80,
- borderRadius: 40,
+ container: {
justifyContent: 'center',
alignItems: 'center',
+ marginHorizontal: 5,
},
image: {
- width: 72,
- height: 72,
- borderRadius: 37.5,
- backgroundColor: 'pink',
+ width: TAGG_ICON_DIM,
+ height: TAGG_ICON_DIM,
+ borderRadius: TAGG_ICON_DIM / 2,
+ position: 'absolute',
},
});
diff --git a/src/components/taggs/TaggsBar.tsx b/src/components/taggs/TaggsBar.tsx
index 88f670b5..520cc266 100644
--- a/src/components/taggs/TaggsBar.tsx
+++ b/src/components/taggs/TaggsBar.tsx
@@ -1,10 +1,16 @@
// @refresh react
-import React from 'react';
+import React, {useEffect, useState} from 'react';
import {StyleSheet} from 'react-native';
import Animated from 'react-native-reanimated';
-import Tagg from './Tagg';
-import {PROFILE_CUTOUT_BOTTOM_Y} from '../../constants';
+import {
+ INTEGRATED_SOCIAL_LIST,
+ PROFILE_CUTOUT_BOTTOM_Y,
+ SOCIAL_LIST,
+} from '../../constants';
+import {AuthContext, ProfileContext} from '../../routes';
+import {getLinkedSocials} from '../../services';
import {StatusBarHeight} from '../../utils';
+import Tagg from './Tagg';
const {View, ScrollView, interpolate, Extrapolate} = Animated;
interface TaggsBarProps {
@@ -17,43 +23,59 @@ const TaggsBar: React.FC<TaggsBarProps> = ({
profileBodyHeight,
isProfileView,
}) => {
- const taggs: Array<JSX.Element> = [];
+ let [taggs, setTaggs] = useState<Object[]>([]);
+ let [taggsNeedUpdate, setTaggsNeedUpdate] = useState(true);
+ const context = isProfileView
+ ? React.useContext(ProfileContext)
+ : React.useContext(AuthContext);
+ const {user, socialsNeedUpdate} = context;
- taggs.push(
- <Tagg
- key={0}
- style={styles.tagg}
- social={'Instagram'}
- isProfileView={isProfileView}
- />,
- );
- taggs.push(
- <Tagg
- key={1}
- style={styles.tagg}
- social={'Facebook'}
- isProfileView={isProfileView}
- />,
- );
- taggs.push(
- <Tagg
- key={2}
- style={styles.tagg}
- social={'Twitter'}
- isProfileView={isProfileView}
- />,
- );
+ useEffect(() => {
+ const loadData = async () => {
+ getLinkedSocials(user.userId).then((linkedSocials) => {
+ const unlinkedSocials = SOCIAL_LIST.filter(
+ (s) => linkedSocials.indexOf(s) === -1,
+ );
+ let new_taggs = [];
+ let i = 0;
+ for (let social of linkedSocials) {
+ new_taggs.push(
+ <Tagg
+ key={i}
+ social={social}
+ isProfileView={isProfileView}
+ isLinked={true}
+ isIntegrated={INTEGRATED_SOCIAL_LIST.indexOf(social) !== -1}
+ setTaggsNeedUpdate={setTaggsNeedUpdate}
+ setSocialDataNeedUpdate={socialsNeedUpdate}
+ />,
+ );
+ i++;
+ }
+ for (let social of unlinkedSocials) {
+ new_taggs.push(
+ <Tagg
+ key={i}
+ social={social}
+ isProfileView={isProfileView}
+ isLinked={false}
+ isIntegrated={INTEGRATED_SOCIAL_LIST.indexOf(social) !== -1}
+ setTaggsNeedUpdate={setTaggsNeedUpdate}
+ setSocialDataNeedUpdate={socialsNeedUpdate}
+ />,
+ );
+ i++;
+ }
+ setTaggs(new_taggs);
+ setTaggsNeedUpdate(false);
+ });
+ };
+
+ if (taggsNeedUpdate) {
+ loadData();
+ }
+ }, [isProfileView, taggsNeedUpdate, user.userId]);
- for (let i = 3; i < 10; i++) {
- taggs.push(
- <Tagg
- key={i}
- style={styles.tagg}
- social={'Instagram'}
- isProfileView={isProfileView}
- />,
- );
- }
const shadowOpacity: Animated.Node<number> = interpolate(y, {
inputRange: [
PROFILE_CUTOUT_BOTTOM_Y + profileBodyHeight,
@@ -105,9 +127,6 @@ const styles = StyleSheet.create({
alignItems: 'center',
paddingHorizontal: 15,
},
- tagg: {
- marginHorizontal: 14,
- },
});
export default TaggsBar;
diff --git a/src/constants/api.ts b/src/constants/api.ts
index d9e199d2..ce9b24f4 100644
--- a/src/constants/api.ts
+++ b/src/constants/api.ts
@@ -21,7 +21,12 @@ export const FOLLOW_USER_ENDPOINT: string = API_URL + 'follow/';
export const UNFOLLOW_USER_ENDPOINT: string = API_URL + 'unfollow/';
export const FOLLOWERS_ENDPOINT: string = API_URL + 'followers/';
-// Social Link
+// Register Social Link (Non-integrated)
+export const LINK_SNAPCHAT_ENDPOINT: string = API_URL + 'link-sc/';
+export const LINK_TIKTOK_ENDPOINT: string = API_URL + 'link-tt/';
+
+// Register Social Link (Integrated)
+export const LINKED_SOCIALS_ENDPOINT: string = API_URL + 'linked-socials/';
export const LINK_IG_ENDPOINT: string = API_URL + 'link-ig/';
export const LINK_FB_ENDPOINT: string = API_URL + 'link-fb/';
export const LINK_TWITTER_ENDPOINT: string = API_URL + 'link-twitter/';
diff --git a/src/constants/constants.ts b/src/constants/constants.ts
index 5f341f41..6434f6e4 100644
--- a/src/constants/constants.ts
+++ b/src/constants/constants.ts
@@ -12,17 +12,32 @@ export const COVER_HEIGHT = SCREEN_WIDTH * (7 / 5);
export const AVATAR_DIM = 44;
export const AVATAR_GRADIENT_DIM = 50;
-export const SOCIAL_LIST: Array<string> = [
+export const TAGG_ICON_DIM = 72;
+
+export const INTEGRATED_SOCIAL_LIST: string[] = [
+ 'Instagram',
+ 'Facebook',
+ 'Twitter',
+];
+
+export const SOCIAL_LIST: string[] = [
'Instagram',
'Facebook',
'Twitter',
- 'TikTok',
'Snapchat',
+ 'Tiktok',
+ // TODO: we don't have endpoints to support these yet...
+ // 'Twitch',
+ // 'Pinterest',
+ // 'Whatsapp',
+ // 'Linkedin',
+ // 'Youtube',
];
export const INSTAGRAM_FONT_COLOR: string = '#FF97DE';
export const FACEBOOK_FONT_COLOR: string = '#6697FD';
export const TWITTER_FONT_COLOR: string = '#74C9FD';
+export const TIKTOK_FONT_COLOR: string = '#78B5FD';
export const TWITCH_FONT_COLOR: string = '#CB93FF';
export const PINTEREST_FONT_COLOR: string = '#FF7584';
export const WHATSAPP_FONT_COLOR: string = '#4AC959';
@@ -44,6 +59,7 @@ export const SOCIAL_FONT_COLORS = {
INSTAGRAM: INSTAGRAM_FONT_COLOR,
FACEBOOK: FACEBOOK_FONT_COLOR,
TWITTER: TWITTER_FONT_COLOR,
+ TIKTOK: TIKTOK_FONT_COLOR,
TWITCH: TWITCH_FONT_COLOR,
PINTEREST: PINTEREST_FONT_COLOR,
WHATSAPP: WHATSAPP_FONT_COLOR,
diff --git a/src/routes/authentication/AuthProvider.tsx b/src/routes/authentication/AuthProvider.tsx
index a705f074..5bd4278d 100644
--- a/src/routes/authentication/AuthProvider.tsx
+++ b/src/routes/authentication/AuthProvider.tsx
@@ -1,10 +1,6 @@
import AsyncStorage from '@react-native-community/async-storage';
import React, {createContext, useEffect, useState} from 'react';
-import {
- GET_FB_POSTS_ENDPOINT,
- GET_IG_POSTS_ENDPOINT,
- GET_TWITTER_POSTS_ENDPOINT,
-} from '../../constants';
+import {INTEGRATED_SOCIAL_LIST} from '../../constants';
import {
loadAvatar,
loadCover,
@@ -30,6 +26,7 @@ interface AuthContextProps {
recentSearches: Array<ProfilePreviewType>;
newMomentsAvailable: boolean;
updateMoments: (value: boolean) => void;
+ socialsNeedUpdate: (_: string[]) => void;
}
const NO_USER: UserType = {
@@ -43,11 +40,10 @@ const NO_PROFILE: ProfileType = {
name: '',
};
-// Not necessary, but safer, in case SocialAccountType object is undefined
const NO_SOCIAL_ACCOUNTS: Record<string, SocialAccountType> = {
- Instagram: {},
- Facebook: {},
- Twitter: {},
+ Instagram: {posts: []},
+ Facebook: {posts: []},
+ Twitter: {posts: []},
};
export const AuthContext = createContext<AuthContextProps>({
@@ -57,10 +53,11 @@ export const AuthContext = createContext<AuthContextProps>({
logout: () => {},
avatar: null,
cover: null,
- socialAccounts: NO_SOCIAL_ACCOUNTS,
recentSearches: [],
newMomentsAvailable: true,
updateMoments: () => {},
+ socialAccounts: NO_SOCIAL_ACCOUNTS,
+ socialsNeedUpdate: () => {},
});
/**
@@ -78,6 +75,10 @@ const AuthProvider: React.FC = ({children}) => {
Array<ProfilePreviewType>
>([]);
const [newMomentsAvailable, setNewMomentsAvailable] = useState<boolean>(true);
+ // Default update all integrated social lists on start
+ const [socialsNeedUpdate, setSocialsNeedUpdate] = useState<string[]>([
+ ...INTEGRATED_SOCIAL_LIST,
+ ]);
const {userId} = user;
useEffect(() => {
if (!userId) {
@@ -95,33 +96,25 @@ const AuthProvider: React.FC = ({children}) => {
loadAvatar(token, userId, setAvatar);
loadCover(token, userId, setCover);
loadRecentlySearchedUsers(setRecentSearches);
- loadSocialPosts(
- token,
- userId,
- 'Instagram',
- GET_IG_POSTS_ENDPOINT,
- socialAccounts,
- ).then((newSocialAccounts) => setSocialAccounts(newSocialAccounts));
- loadSocialPosts(
- token,
- userId,
- 'Facebook',
- GET_FB_POSTS_ENDPOINT,
- socialAccounts,
- ).then((newSocialAccounts) => setSocialAccounts(newSocialAccounts));
- loadSocialPosts(
- token,
- userId,
- 'Twitter',
- GET_TWITTER_POSTS_ENDPOINT,
- socialAccounts,
- ).then((newSocialAccounts) => setSocialAccounts(newSocialAccounts));
} catch (err) {
console.log(err);
}
};
loadData();
- }, [socialAccounts, userId]);
+ }, [userId]);
+
+ useEffect(() => {
+ if (socialsNeedUpdate.length > 0 && userId) {
+ for (let social of socialsNeedUpdate) {
+ loadSocialPosts(userId, social).then((accountData) => {
+ socialAccounts[social] = accountData;
+ setSocialAccounts(socialAccounts);
+ console.log('Updated posts data', social);
+ });
+ }
+ setSocialsNeedUpdate([]);
+ }
+ }, [socialAccounts, socialsNeedUpdate, userId]);
return (
<AuthContext.Provider
@@ -150,6 +143,9 @@ const AuthProvider: React.FC = ({children}) => {
updateMoments: (value) => {
setNewMomentsAvailable(value);
},
+ socialsNeedUpdate: (socials: string[]) => {
+ setSocialsNeedUpdate(socials);
+ },
}}>
{children}
</AuthContext.Provider>
diff --git a/src/routes/viewProfile/ProfileProvider.tsx b/src/routes/viewProfile/ProfileProvider.tsx
index a4b6cb14..c4942ea0 100644
--- a/src/routes/viewProfile/ProfileProvider.tsx
+++ b/src/routes/viewProfile/ProfileProvider.tsx
@@ -1,9 +1,6 @@
import AsyncStorage from '@react-native-community/async-storage';
import React, {createContext, useEffect, useState} from 'react';
-import {
- GET_IG_POSTS_ENDPOINT,
- GET_TWITTER_POSTS_ENDPOINT,
-} from '../../constants';
+import {INTEGRATED_SOCIAL_LIST} from '../../constants';
import {
loadAvatar,
loadCover,
@@ -21,6 +18,7 @@ interface ProfileContextProps {
newMomentsAvailable: boolean;
updateMoments: (value: boolean) => void;
socialAccounts: Record<string, SocialAccountType>;
+ socialsNeedUpdate: (_: string[]) => void;
}
const NO_USER: UserType = {
userId: '',
@@ -32,11 +30,10 @@ const NO_PROFILE: ProfileType = {
name: '',
};
-// Not necessary, but safer, in case SocialAccountType object is undefined
const NO_SOCIAL_ACCOUNTS: Record<string, SocialAccountType> = {
- Instagram: {},
- Facebook: {},
- Twitter: {},
+ Instagram: {posts: []},
+ Facebook: {posts: []},
+ Twitter: {posts: []},
};
export const ProfileContext = createContext<ProfileContextProps>({
@@ -48,6 +45,7 @@ export const ProfileContext = createContext<ProfileContextProps>({
newMomentsAvailable: true,
updateMoments: () => {},
socialAccounts: NO_SOCIAL_ACCOUNTS,
+ socialsNeedUpdate: () => {},
});
/**
@@ -59,10 +57,14 @@ const ProfileProvider: React.FC = ({children}) => {
const [avatar, setAvatar] = useState<string | null>(null);
const [cover, setCover] = useState<string | null>(null);
const [newMomentsAvailable, setNewMomentsAvailable] = useState<boolean>(true);
-
const [socialAccounts, setSocialAccounts] = useState<
Record<string, SocialAccountType>
>(NO_SOCIAL_ACCOUNTS);
+ // Default update all integrated social lists on start
+ const [socialsNeedUpdate, setSocialsNeedUpdate] = useState<string[]>([
+ ...INTEGRATED_SOCIAL_LIST,
+ ]);
+
const {userId} = user;
useEffect(() => {
if (!userId) {
@@ -79,26 +81,25 @@ const ProfileProvider: React.FC = ({children}) => {
loadProfileInfo(token, userId, setProfile);
loadAvatar(token, userId, setAvatar);
loadCover(token, userId, setCover);
- loadSocialPosts(
- token,
- userId,
- 'Instagram',
- GET_IG_POSTS_ENDPOINT,
- socialAccounts,
- ).then((newSocialAccounts) => setSocialAccounts(newSocialAccounts));
- loadSocialPosts(
- token,
- userId,
- 'Twitter',
- GET_TWITTER_POSTS_ENDPOINT,
- socialAccounts,
- ).then((newSocialAccounts) => setSocialAccounts(newSocialAccounts));
} catch (err) {
console.log(err);
}
};
loadData();
- }, [socialAccounts, userId]);
+ }, [userId]);
+
+ useEffect(() => {
+ if (socialsNeedUpdate.length > 0 && userId) {
+ for (let social of socialsNeedUpdate) {
+ loadSocialPosts(userId, social).then((accountData) => {
+ socialAccounts[social] = accountData;
+ setSocialAccounts(socialAccounts);
+ console.log('Updated posts data', social);
+ });
+ }
+ setSocialsNeedUpdate([]);
+ }
+ }, [socialAccounts, socialsNeedUpdate, userId]);
return (
<ProfileContext.Provider
diff --git a/src/screens/profile/SocialMediaTaggs.tsx b/src/screens/profile/SocialMediaTaggs.tsx
index 0ac6d1ef..2a319326 100644
--- a/src/screens/profile/SocialMediaTaggs.tsx
+++ b/src/screens/profile/SocialMediaTaggs.tsx
@@ -1,17 +1,18 @@
import {RouteProp} from '@react-navigation/native';
-import React from 'react';
-import {Alert, ScrollView, StatusBar, StyleSheet, View} from 'react-native';
+import React, {useEffect, useState} from 'react';
+import {ScrollView, StatusBar, StyleSheet, View} from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
-import {AVATAR_GRADIENT} from '../../constants';
import {
SocialMediaInfo,
TabsGradient,
TaggPost,
TwitterTaggPost,
} from '../../components';
-import {AuthContext, ProfileStackParams, ProfileContext} from '../../routes';
+import {AVATAR_GRADIENT} from '../../constants';
+import {AuthContext, ProfileContext, ProfileStackParams} from '../../routes';
+import {loadSocialPosts} from '../../services';
+import {SimplePostType, SocialAccountType, TwitterPostType} from '../../types';
import {headerBarHeightWithImage, SCREEN_HEIGHT} from '../../utils';
-import {SimplePostType, TwitterPostType} from '../../types';
type SocialMediaTaggsRouteProp = RouteProp<
ProfileStackParams,
@@ -22,17 +23,6 @@ interface SocialMediaTaggsProps {
route: SocialMediaTaggsRouteProp;
}
-/**
- * Social media taggs screen for a user's social media
- * includes:
- * + tagg profile pic
- * + username from social media
- * + post
- * + caption
- * + sharebutton + number of shares
- * + date posted
- * + dark background
- */
const SocialMediaTaggs: React.FC<SocialMediaTaggsProps> = ({route}) => {
const {socialMediaType, isProfileView} = route.params;
const context = isProfileView
@@ -42,11 +32,8 @@ const SocialMediaTaggs: React.FC<SocialMediaTaggsProps> = ({route}) => {
profile: {name},
socialAccounts,
} = context;
-
-
- const handle = socialAccounts[socialMediaType].handle;
- const posts = socialAccounts[socialMediaType].posts || [];
const headerHeight = headerBarHeightWithImage();
+ let accountData = socialAccounts[socialMediaType];
return (
<LinearGradient
@@ -65,15 +52,15 @@ const SocialMediaTaggs: React.FC<SocialMediaTaggsProps> = ({route}) => {
<SocialMediaInfo
fullname={name}
type={socialMediaType}
- handle={handle}
+ handle={accountData.handle}
/>
- {(posts as Array<
+ {(accountData.posts as Array<
SimplePostType | TwitterPostType
>).map((post, index) =>
socialMediaType === 'Twitter' ? (
<TwitterTaggPost
key={index}
- ownerHandle={handle || '_'}
+ ownerHandle={accountData.handle || '_'}
post={post as TwitterPostType}
/>
) : (
diff --git a/src/services/SocialLinkingService.ts b/src/services/SocialLinkingService.ts
new file mode 100644
index 00000000..8d67d90e
--- /dev/null
+++ b/src/services/SocialLinkingService.ts
@@ -0,0 +1,184 @@
+import AsyncStorage from '@react-native-community/async-storage';
+import {Alert} from 'react-native';
+import InAppBrowser from 'react-native-inappbrowser-reborn';
+import {
+ LINKED_SOCIALS_ENDPOINT,
+ LINK_FB_ENDPOINT,
+ LINK_FB_OAUTH,
+ LINK_IG_ENDPOINT,
+ LINK_IG_OAUTH,
+ LINK_SNAPCHAT_ENDPOINT,
+ LINK_TIKTOK_ENDPOINT,
+ LINK_TWITTER_ENDPOINT,
+ LINK_TWITTER_OAUTH,
+} from '../constants';
+
+// A list of endpoint strings for all the integrated socials
+export const integratedEndpoints: {[social: string]: [string, string]} = {
+ Instagram: [LINK_IG_OAUTH, LINK_IG_ENDPOINT],
+ Facebook: [LINK_FB_OAUTH, LINK_FB_ENDPOINT],
+ Twitter: [LINK_TWITTER_OAUTH, LINK_TWITTER_ENDPOINT],
+};
+
+export const nonIntegratedEndponits: {[social: string]: string} = {
+ Snapchat: LINK_SNAPCHAT_ENDPOINT,
+ TikTok: LINK_TIKTOK_ENDPOINT,
+};
+
+export const registerNonIntegratedSocialLink: (
+ socialType: string,
+ username: string,
+) => Promise<boolean> = async (socialType, username) => {
+ if (!(socialType in nonIntegratedEndponits)) {
+ return false;
+ }
+ try {
+ const user_token = await AsyncStorage.getItem('token');
+ const response = await fetch(nonIntegratedEndponits[socialType], {
+ method: 'POST',
+ headers: {
+ Authorization: `Token ${user_token}`,
+ },
+ body: JSON.stringify({
+ username: username,
+ }),
+ });
+ return response.status === 200;
+ } catch (error) {
+ console.log(error);
+ return false;
+ }
+};
+
+// We have already received the short-lived token (callback_data), sending it
+// to backend to exchange for and store the long-lived token.
+export const registerIntegratedSocialLink: (
+ callback_data: string,
+ user_token: string,
+ socialType: string,
+) => Promise<boolean> = async (callback_data, user_token, socialType) => {
+ if (!(socialType in integratedEndpoints)) {
+ return false;
+ }
+ const response = await fetch(integratedEndpoints[socialType][1], {
+ method: 'POST',
+ headers: {
+ Authorization: `Token ${user_token}`,
+ },
+ body: JSON.stringify({
+ callback_url: callback_data,
+ }),
+ });
+ if (!(response.status === 201)) {
+ console.log(await response.json());
+ }
+ return response.status === 201;
+};
+
+// Twitter is a special case since they use OAuth1, we will need to request
+// for a request_token before we can begin browser signin.
+export const getTwitterRequestToken: (
+ user_token: string,
+) => Promise<string> = async (user_token) => {
+ const response = await fetch(integratedEndpoints.Twitter[0], {
+ method: 'GET',
+ headers: {
+ Authorization: `Token ${user_token}`,
+ },
+ });
+ return response.url;
+};
+
+// one stop shop for handling all browser sign-in social linkings
+export const handlePressForAuthBrowser: (
+ socialType: string,
+) => Promise<boolean> = async (socialType: string) => {
+ try {
+ if (!(socialType in integratedEndpoints)) {
+ Alert.alert('Coming soon!');
+ return false;
+ }
+
+ if (!(await InAppBrowser.isAvailable())) {
+ // Okay... to open an external browser and have it link back to
+ // the app is a bit tricky, we will need to have navigation routes
+ // setup for this screen and have it hooked up.
+ // See https://github.com/proyecto26/react-native-inappbrowser#authentication-flow-using-deep-linking
+ // Though this isn't the end of the world, from the documentation,
+ // the in-app browser should be supported from iOS 11, which
+ // is about 98.5% of all iOS devices in the world.
+ // See https://support.apple.com/en-gb/HT209574
+ Alert.alert(
+ 'Sorry! Your device was unable to open a browser to let you sign-in! 😔',
+ );
+ return false;
+ }
+
+ let url = integratedEndpoints[socialType][0];
+ const user_token = await AsyncStorage.getItem('token');
+
+ if (!user_token) {
+ throw 'Unable to get user token';
+ }
+
+ // We will need to do an extra step for twitter sign-in
+ if (socialType === 'Twitter') {
+ url = await getTwitterRequestToken(user_token);
+ }
+
+ return await InAppBrowser.openAuth(url, 'taggid://callback', {
+ ephemeralWebSession: true,
+ })
+ .then(async (response) => {
+ if (response.type === 'success' && response.url) {
+ const success = await registerIntegratedSocialLink(
+ response.url,
+ user_token,
+ socialType,
+ );
+ if (!success) {
+ throw 'Unable to register with backend';
+ }
+ Alert.alert(`Successfully linked ${socialType} 🎉`);
+ return true;
+ } else {
+ throw 'Error from Oauth API';
+ }
+ })
+ .catch((error) => {
+ console.log(error);
+ Alert.alert(
+ `Something went wrong, we can't link with ${socialType} 😔`,
+ );
+ return false;
+ });
+ } catch (error) {
+ console.log(error);
+ Alert.alert(`Something went wrong, we can't link with ${socialType} 😔`);
+ }
+ return false;
+};
+
+// get all the linked socials from backend as an array
+export const getLinkedSocials: (user_id: string) => Promise<string[]> = async (
+ user_id: string,
+) => {
+ try {
+ const user_token = await AsyncStorage.getItem('token');
+ const response = await fetch(`${LINKED_SOCIALS_ENDPOINT}${user_id}/`, {
+ method: 'GET',
+ headers: {
+ Authorization: 'Token ' + user_token,
+ },
+ });
+ const body = await response.json();
+ if (response.status !== 200) {
+ console.log(body);
+ throw 'Unable to fetch from server';
+ }
+ return body.linked_socials || [];
+ } catch (error) {
+ console.log(error);
+ return [];
+ }
+};
diff --git a/src/services/UserProfileService.ts b/src/services/UserProfileService.ts
index f5523be4..31383f67 100644
--- a/src/services/UserProfileService.ts
+++ b/src/services/UserProfileService.ts
@@ -1,16 +1,18 @@
//Abstracted common profile api calls out here
+import AsyncStorage from '@react-native-community/async-storage';
import {Alert} from 'react-native';
+import RNFetchBlob from 'rn-fetch-blob';
+import {SocialAccountType} from 'src/types';
import {
- PROFILE_INFO_ENDPOINT,
AVATAR_PHOTO_ENDPOINT,
COVER_PHOTO_ENDPOINT,
+ GET_FB_POSTS_ENDPOINT,
+ GET_IG_POSTS_ENDPOINT,
+ GET_TWITTER_POSTS_ENDPOINT,
+ PROFILE_INFO_ENDPOINT,
} from '../constants';
-import AsyncStorage from '@react-native-community/async-storage';
-import RNFetchBlob from 'rn-fetch-blob';
-import {SocialAccountType} from 'src/types';
-
export const loadProfileInfo = async (
token: string,
userId: string,
@@ -83,13 +85,19 @@ export const loadCover = async (
}
};
-export const loadSocialPosts = async (
- token: string,
+const integratedSocialPostsEndpoints: {[social: string]: string} = {
+ Facebook: GET_FB_POSTS_ENDPOINT,
+ Instagram: GET_IG_POSTS_ENDPOINT,
+ Twitter: GET_TWITTER_POSTS_ENDPOINT,
+};
+
+export const loadSocialPosts: (
userId: string,
socialType: string,
- endpoint: string,
- socialAccounts: Record<string, SocialAccountType>,
-) => {
+) => Promise<SocialAccountType> = async (userId, socialType) => {
+ const token = await AsyncStorage.getItem('token');
+ const endpoint = integratedSocialPostsEndpoints[socialType];
+ const accountData: SocialAccountType = {};
try {
const response = await fetch(endpoint + `${userId}/`, {
method: 'GET',
@@ -99,16 +107,16 @@ export const loadSocialPosts = async (
});
if (response.status === 200) {
const body = await response.json();
- socialAccounts[socialType].handle = body.handle;
- socialAccounts[socialType].posts = body.posts;
- socialAccounts[socialType].profile_pic = body.profile_pic;
+ accountData.handle = body.handle;
+ accountData.posts = body.posts;
+ accountData.profile_pic = body.profile_pic;
} else {
- throw new Error(await response.json());
+ throw 'Unable to fetch posts data from ' + socialType;
}
} catch (error) {
- console.log(error);
+ console.warn(error);
}
- return socialAccounts;
+ return accountData;
};
export const loadRecentlySearchedUsers = async (callback: Function) => {
diff --git a/src/services/index.ts b/src/services/index.ts
index aa13dbe3..6d0f4314 100644
--- a/src/services/index.ts
+++ b/src/services/index.ts
@@ -1,3 +1,4 @@
export * from './UserProfileService';
+export * from './SocialLinkingService';
export * from './MomentServices';
export * from './UserFollowServices';