From 792115326fc6af583f422082537885bc8061d051 Mon Sep 17 00:00:00 2001 From: Ivan Chen Date: Tue, 8 Dec 2020 23:17:23 -0500 Subject: [TMA-410] Edit Non-Integrated Socials (#126) * removed refs, added KB avoidance, UI done * missed some things * now parsing response correctly from backend * fixed naming * now allow empty strings to un-link a social * hide field if empty, allow empty string, fixed taggs bar not updating bug * Fix bug where non integrated socials do not show up on edit profile page Co-authored-by: Ashm Walia <40498934+ashmgarv@users.noreply.github.com> Co-authored-by: Ashm Walia --- src/components/taggs/Tagg.tsx | 5 +- src/components/taggs/TaggsBar.tsx | 11 +- src/screens/profile/EditProfile.tsx | 348 ++++++++++++++++++++---------------- src/services/UserProfileService.ts | 9 +- src/store/actions/user.ts | 24 ++- src/store/initialStates.ts | 2 + src/store/reducers/userReducer.ts | 18 +- src/types/types.ts | 2 + 8 files changed, 253 insertions(+), 166 deletions(-) (limited to 'src') diff --git a/src/components/taggs/Tagg.tsx b/src/components/taggs/Tagg.tsx index 086b3c87..8158cbac 100644 --- a/src/components/taggs/Tagg.tsx +++ b/src/components/taggs/Tagg.tsx @@ -26,7 +26,7 @@ interface TaggProps { isLinked: boolean; isIntegrated: boolean; setTaggsNeedUpdate: (_: boolean) => void; - setSocialDataNeedUpdate: (_: string) => void; + setSocialDataNeedUpdate: (social: string, username: string) => void; userXId: string; screenType: ScreenType; } @@ -84,7 +84,7 @@ const Tagg: React.FC = ({ if (isIntegrated) { handlePressForAuthBrowser(social).then((success) => { setTaggsNeedUpdate(success); - if (success) setSocialDataNeedUpdate(social); + if (success) setSocialDataNeedUpdate(social, ''); }); } else { setModalVisible(true); @@ -112,6 +112,7 @@ const Tagg: React.FC = ({ if (await registerNonIntegratedSocialLink(social, username)) { Alert.alert(`Successfully linked ${social} 🎉`); setTaggsNeedUpdate(true); + setSocialDataNeedUpdate(social, username); } else { // If we display too fast the alert will get dismissed with the modal setTimeout(() => { diff --git a/src/components/taggs/TaggsBar.tsx b/src/components/taggs/TaggsBar.tsx index 12e4b93a..082743d0 100644 --- a/src/components/taggs/TaggsBar.tsx +++ b/src/components/taggs/TaggsBar.tsx @@ -12,7 +12,7 @@ import {StatusBarHeight} from '../../utils'; import Tagg from './Tagg'; import {RootState} from '../../store/rootReducer'; import {ScreenType} from '../../types'; -import {loadIndividualSocial} from '../../store/actions'; +import {loadIndividualSocial, updateSocial} from '../../store/actions'; const {View, ScrollView, interpolate, Extrapolate} = Animated; interface TaggsBarProps { @@ -38,10 +38,15 @@ const TaggsBar: React.FC = ({ /** * Updates the individual social that needs update + * If username is empty, update nonintegrated socials like Snapchat and TikTok * @param socialType Type of the social that needs update */ - const handleSocialUpdate = (socialType: string) => { - dispatch(loadIndividualSocial(user.userId, socialType)); + const handleSocialUpdate = (socialType: string, username: string) => { + if (username !== '') { + dispatch(updateSocial(socialType, username)); + } else { + dispatch(loadIndividualSocial(user.userId, socialType)); + } }; /** diff --git a/src/screens/profile/EditProfile.tsx b/src/screens/profile/EditProfile.tsx index ab58db41..97c58177 100644 --- a/src/screens/profile/EditProfile.tsx +++ b/src/screens/profile/EditProfile.tsx @@ -11,6 +11,9 @@ import { Alert, View, SafeAreaView, + KeyboardAvoidingView, + Platform, + Keyboard, } from 'react-native'; import {Button} from 'react-native-elements'; import { @@ -21,7 +24,6 @@ import { BirthDatePicker, TabsGradient, } from '../../components'; -import {OnboardingStackParams} from '../../routes/onboarding'; import ImagePicker from 'react-native-image-crop-picker'; import { EDIT_PROFILE_ENDPOINT, @@ -30,23 +32,21 @@ import { genderRegex, } from '../../constants'; import AsyncStorage from '@react-native-community/async-storage'; +import {ProfileStackParams} from '../../routes'; import Animated from 'react-native-reanimated'; import {HeaderHeight, SCREEN_HEIGHT} from '../../utils'; import {RootState} from '../../store/rootReducer'; import {useDispatch, useSelector} from 'react-redux'; import {loadUserData} from '../../store/actions'; -type ProfileOnboardingScreenRouteProp = RouteProp< - OnboardingStackParams, - 'ProfileOnboarding' +type EditProfileNavigationProp = StackNavigationProp< + ProfileStackParams, + 'EditProfile' >; -type ProfileOnboardingScreenNavigationProp = StackNavigationProp< - OnboardingStackParams, - 'ProfileOnboarding' ->; -interface ProfileOnboardingProps { - route: ProfileOnboardingScreenRouteProp; - navigation: ProfileOnboardingScreenNavigationProp; + +interface EditProfileProps { + route: RouteProp; + navigation: EditProfileNavigationProp; } /** @@ -54,14 +54,11 @@ interface ProfileOnboardingProps { * @param navigation react-navigation navigation object */ -const ProfileOnboarding: React.FC = ({ - route, - navigation, -}) => { +const EditProfile: React.FC = ({route, navigation}) => { const y: Animated.Value = Animated.useValue(0); const {userId, username} = route.params; const { - profile: {website, biography, birthday, gender}, + profile: {website, biography, birthday, gender, snapchat, tiktok}, avatar, cover, } = useSelector((state: RootState) => state.user); @@ -73,7 +70,8 @@ const ProfileOnboarding: React.FC = ({ if (needsUpdate) { dispatch(loadUserData({userId, username})); } - }, [loadUserData, needsUpdate]); + + }, [dispatch, needsUpdate, userId, username]); const [isCustomGender, setIsCustomGender] = React.useState( gender !== '' && gender !== 'female' && gender !== 'male', @@ -87,42 +85,15 @@ const ProfileOnboarding: React.FC = ({ birthdate: birthday && moment(birthday).format('YYYY-MM-DD'), gender: isCustomGender ? 'custom' : gender, customGenderText: isCustomGender ? gender : '', + snapchat: snapchat, + tiktok: tiktok, isValidWebsite: true, isValidBio: true, isValidGender: true, + isValidSnapchat: true, + isValidTiktok: true, attemptedSubmit: false, }); - // refs for changing focus - const bioRef = React.useRef(); - const birthdateRef = React.useRef(); - const genderRef = React.useRef(); - const customGenderRef = React.useRef(); - /** - * Handles focus change to the next input field. - * @param field key for field to move focus onto - */ - const handleFocusChange = (field: string): void => { - switch (field) { - case 'bio': - const bioField: any = bioRef.current; - bioField.focus(); - break; - case 'birthdate': - const birthdateField: any = birthdateRef.current; - birthdateField.focus(); - break; - case 'gender': - const genderField: any = genderRef.current; - genderField.focus(); - break; - case 'customGender': - const customGenderField: any = customGenderRef.current; - customGenderField.focus(); - break; - default: - return; - } - }; /** * Profile screen "Add header image" button @@ -256,6 +227,24 @@ const ProfileOnboarding: React.FC = ({ }); }; + const handleSnapchatUpdate = (newUsername: string) => { + // Allow any username, empty means to "un-link" it + // TODO: refresh taggs bar after + setForm({ + ...form, + snapchat: newUsername, + }); + }; + + const handleTikTokUpdate = (newUsername: string) => { + // Allow any username, empty means to "un-link" it + // TODO: refresh taggs bar after + setForm({ + ...form, + tiktok: newUsername, + }); + }; + const handleSubmit = useCallback(async () => { if (!form.largePic) { Alert.alert('Please select a Header image!'); @@ -325,6 +314,22 @@ const ProfileOnboarding: React.FC = ({ } } + if (form.isValidSnapchat) { + request.append('snapchat', form.snapchat); + } else { + setForm({...form, attemptedSubmit: false}); + setTimeout(() => setForm({...form, attemptedSubmit: true})); + invalidFields = true; + } + + if (form.isValidTiktok) { + request.append('tiktok', form.tiktok); + } else { + setForm({...form, attemptedSubmit: false}); + setTimeout(() => setForm({...form, attemptedSubmit: true})); + invalidFields = true; + } + if (invalidFields) { return; } @@ -357,10 +362,7 @@ const ProfileOnboarding: React.FC = ({ ); } } catch (error) { - Alert.alert( - 'Profile creation failed 😓', - 'Please double-check your network connection and retry.', - ); + Alert.alert('Please double-check your network connection and retry.'); return { name: 'Profile creation error', description: error, @@ -384,118 +386,158 @@ const ProfileOnboarding: React.FC = ({ return ( - y.setValue(e.nativeEvent.contentOffset.y)} - showsHorizontalScrollIndicator={false} - showsVerticalScrollIndicator={true} - scrollEventThrottle={1} - alwaysBounceVertical - contentContainerStyle={{paddingBottom: SCREEN_HEIGHT / 15}}> - - - - - - - - - handleFocusChange('bio')} - blurOnSubmit={false} - valid={form.isValidWebsite} - attemptedSubmit={form.attemptedSubmit} - invalidWarning={ - 'Website must be a valid link to your website' - } - width={280} - value={form.website} - /> - handleFocusChange('bio')} - blurOnSubmit={false} - ref={bioRef} - valid={form.isValidBio} - attemptedSubmit={form.attemptedSubmit} - invalidWarning={ - 'Bio must be less than 150 characters and must contain valid characters' - } - width={280} - value={form.bio} - /> - - - handleGenderUpdate(value)} - items={[ - {label: 'Male', value: 'male'}, - {label: 'Female', value: 'female'}, - {label: 'Custom', value: 'custom'}, - ]} - placeholder={{ - label: 'Gender', - value: null, - color: '#fff', - }} - /> - {isCustomGender && ( + + y.setValue(e.nativeEvent.contentOffset.y)} + showsHorizontalScrollIndicator={false} + showsVerticalScrollIndicator={false} + scrollEventThrottle={1} + alwaysBounceVertical + contentContainerStyle={{paddingBottom: SCREEN_HEIGHT / 15}}> + + + + + + + + + handleSubmit()} - valid={form.isValidGender} + valid={form.isValidBio} attemptedSubmit={form.attemptedSubmit} invalidWarning={ - 'Custom field can only contain letters and hyphens' + 'Bio must be less than 150 characters and must contain valid characters' } + width={280} + value={form.bio} + /> + + + handleGenderUpdate(value)} + items={[ + {label: 'Male', value: 'male'}, + {label: 'Female', value: 'female'}, + {label: 'Custom', value: 'custom'}, + ]} + placeholder={{ + label: 'Gender', + value: null, + color: '#fff', + }} /> - )} + {isCustomGender && ( + handleSubmit()} + placeholder="Enter your gender" + returnKeyType="done" + style={styles.customGenderInput} + textContentType="none" + valid={form.isValidGender} + value={form.customGenderText} + /> + )} + {snapchat !== '' && ( + + )} + {tiktok !== '' && ( + + )} + - - + + @@ -585,4 +627,4 @@ const styles = StyleSheet.create({ }, }); -export default ProfileOnboarding; +export default EditProfile; diff --git a/src/services/UserProfileService.ts b/src/services/UserProfileService.ts index e69e4103..c8dbcdd1 100644 --- a/src/services/UserProfileService.ts +++ b/src/services/UserProfileService.ts @@ -1,10 +1,8 @@ -//Abstracted common profile api calls out here - import AsyncStorage from '@react-native-community/async-storage'; import moment from 'moment'; import {Alert} from 'react-native'; import RNFetchBlob from 'rn-fetch-blob'; -import {SocialAccountType, MomentType} from 'src/types'; +import {SocialAccountType} from 'src/types'; import { AVATAR_PHOTO_ENDPOINT, COVER_PHOTO_ENDPOINT, @@ -25,10 +23,9 @@ export const loadProfileInfo = async (token: string, userId: string) => { const status = response.status; if (status === 200) { const info = await response.json(); - let {name, biography, website, birthday, gender} = info; - // user should always have a birthday, but a safety check here + let {name, biography, website, birthday, gender, snapchat, tiktok} = info; birthday = birthday && moment(birthday).format('YYYY-MM-DD'); - return {name, biography, website, birthday, gender}; + return {name, biography, website, birthday, gender, snapchat, tiktok}; } } catch (error) { Alert.alert( diff --git a/src/store/actions/user.ts b/src/store/actions/user.ts index 09ec8abf..e77b8513 100644 --- a/src/store/actions/user.ts +++ b/src/store/actions/user.ts @@ -2,7 +2,7 @@ import {RootState} from '../rootReducer'; import {UserType} from '../../types/types'; import {loadProfileInfo, loadAvatar, loadCover} from '../../services'; import {Action, ThunkAction} from '@reduxjs/toolkit'; -import {userLoggedIn, userDetailsFetched} from '../reducers'; +import {userLoggedIn, userDetailsFetched, socialEdited} from '../reducers'; import {getTokenOrLogout} from '../../utils'; /** @@ -38,6 +38,28 @@ export const loadUserData = ( } }; +/** + * To update editable socials + * @param social social to be updated + * @param value username of social to be updated + */ +export const updateSocial = ( + social: string, + value: string, +): ThunkAction, RootState, unknown, Action> => async ( + dispatch, +) => { + try { + console.log(social); + dispatch({ + type: socialEdited.type, + payload: {social, value}, + }); + } catch (error) { + console.log(error); + } +}; + export const logout = (): ThunkAction< Promise, RootState, diff --git a/src/store/initialStates.ts b/src/store/initialStates.ts index 4087b97c..817af86b 100644 --- a/src/store/initialStates.ts +++ b/src/store/initialStates.ts @@ -14,6 +14,8 @@ export const NO_PROFILE: ProfileType = { name: '', gender: '', birthday: undefined, + snapchat: '', + tiktok: '', }; export const EMPTY_MOMENTS_LIST = []; diff --git a/src/store/reducers/userReducer.ts b/src/store/reducers/userReducer.ts index f43bd0bc..2fd5c462 100644 --- a/src/store/reducers/userReducer.ts +++ b/src/store/reducers/userReducer.ts @@ -29,8 +29,24 @@ const userDataSlice = createSlice({ state.avatar = action.payload.avatar; state.cover = action.payload.cover; }, + + socialEdited: (state, action) => { + const {social, value} = action.payload; + switch (social) { + case 'Snapchat': + state.profile.snapchat = value; + break; + case 'TikTok': + state.profile.tiktok = value; + break; + } + }, }, }); -export const {userLoggedIn, userDetailsFetched} = userDataSlice.actions; +export const { + userLoggedIn, + userDetailsFetched, + socialEdited, +} = userDataSlice.actions; export const userDataReducer = userDataSlice.reducer; diff --git a/src/types/types.ts b/src/types/types.ts index 51837a91..79f15ae9 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -19,6 +19,8 @@ export interface ProfileType { website: string; gender: string; birthday: Date | undefined; + snapchat: string; + tiktok: string; } export interface SocialAccountType { -- cgit v1.2.3-70-g09d2