diff options
author | George Rusu <56009869+grusu6928@users.noreply.github.com> | 2020-10-25 15:21:38 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-10-25 18:21:38 -0400 |
commit | 9da19cdcb6c7596d60afde6d0d559f06a24a0627 (patch) | |
tree | 8f11b6e0a5fcdc2eb983d498fa7b016d4daf44ba /src/components | |
parent | 44a25bfabd356f5eee5ec4f580452407a7e40246 (diff) |
[TMA-237] Added modal for user registration and redirect (#61)
* move async-storage
* removed lock files
* added lock files to gitignore
* added the wrong file to gitignore
* added modal for user registration and redirect
* api call to get list of linked socials for each user to display appropriate icon
* fixed indentation and linting
* refactored modal and browser sign-in
* now dynamically adding linked and unlinked taggs, added a bunch of TODOs for tomorrow
* added svg icons
* done? finished taggs bar UI and all the navigations including modal
* fixed some bugs and added more TODOs
* fixed some bugs and refactored posts fetching logic
* fixed taggs bar bug
* done, it will update everything correctly
* added comments
* added tiktok
Co-authored-by: hsalhab <husam_salhab@brown.edu>
Co-authored-by: george <grus6928@gmail.com>
Co-authored-by: Ivan Chen <ivan@thetaggid.com>
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/common/SocialIcon.tsx | 3 | ||||
-rw-r--r-- | src/components/common/SocialLinkModal.tsx | 118 | ||||
-rw-r--r-- | src/components/common/index.ts | 1 | ||||
-rw-r--r-- | src/components/onboarding/SocialMediaLinker.tsx | 112 | ||||
-rw-r--r-- | src/components/taggs/Tagg.tsx | 146 | ||||
-rw-r--r-- | src/components/taggs/TaggsBar.tsx | 101 |
6 files changed, 307 insertions, 174 deletions
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; |