diff options
-rw-r--r-- | src/assets/images/logo-purple.png | bin | 0 -> 12702 bytes | |||
-rw-r--r-- | src/components/common/SocialLinkModal.tsx | 80 | ||||
-rw-r--r-- | src/components/common/TaggSquareButton.tsx | 41 | ||||
-rw-r--r-- | src/components/taggs/TwitterTaggPost.tsx | 11 | ||||
-rw-r--r-- | src/constants/api.ts | 2 | ||||
-rw-r--r-- | src/routes/Routes.tsx | 34 | ||||
-rw-r--r-- | src/screens/onboarding/Login.tsx | 24 | ||||
-rw-r--r-- | src/screens/onboarding/UpdateRequired.tsx | 84 | ||||
-rw-r--r-- | src/screens/onboarding/WelcomeScreen.tsx | 5 | ||||
-rw-r--r-- | src/screens/onboarding/index.ts | 1 | ||||
-rw-r--r-- | src/services/CommonService.ts | 11 | ||||
-rw-r--r-- | src/store/actions/user.ts | 32 | ||||
-rw-r--r-- | src/store/initialStates.ts | 1 | ||||
-rw-r--r-- | src/store/reducers/userReducer.ts | 5 |
14 files changed, 237 insertions, 94 deletions
diff --git a/src/assets/images/logo-purple.png b/src/assets/images/logo-purple.png Binary files differnew file mode 100644 index 00000000..48768f67 --- /dev/null +++ b/src/assets/images/logo-purple.png diff --git a/src/components/common/SocialLinkModal.tsx b/src/components/common/SocialLinkModal.tsx index d3bc3945..67a3f074 100644 --- a/src/components/common/SocialLinkModal.tsx +++ b/src/components/common/SocialLinkModal.tsx @@ -4,6 +4,7 @@ import {TextInput} from 'react-native-gesture-handler'; import {SocialIcon} from '.'; import CloseIcon from '../../assets/ionicons/close-outline.svg'; import {normalize, SCREEN_WIDTH} from '../../utils'; +import CenteredView from './CenteredView'; import TaggSquareButton from './TaggSquareButton'; interface SocialLinkModalProps { @@ -34,55 +35,46 @@ const SocialLinkModal: React.FC<SocialLinkModalProps> = ({ }; return ( - <> - <View style={styles.centeredView}> - <Modal - animationType="slide" - transparent={true} - visible={modalVisible} - onRequestClose={() => {}}> - <View style={styles.centeredView}> - <View style={styles.modalView}> - <TouchableOpacity - style={styles.closeButton} - onPress={onClosePress}> - <CloseIcon height={'100%'} width={'100%'} color={'grey'} /> - </TouchableOpacity> - <SocialIcon style={styles.icon} social={social} /> - <Text style={styles.titleLabel}>{social}</Text> - <Text style={styles.descriptionLabel}> - Insert your {social.toLowerCase()} username to link your{' '} - {social.toLowerCase()} account to your profile! - </Text> - <TextInput - autoCapitalize={'none'} - autoCorrect={false} - placeholder={'Username'} - style={styles.textInput} - onChangeText={setUsername} - selectionColor={'grey'} - value={username} - /> - <TaggSquareButton - title={'Submit'} - onPress={onSubmit} - mode={'gradient'} - color={'white'} - /> - </View> + <CenteredView> + <Modal + animationType="slide" + transparent={true} + visible={modalVisible} + onRequestClose={() => {}}> + <CenteredView> + <View style={styles.modalView}> + <TouchableOpacity style={styles.closeButton} onPress={onClosePress}> + <CloseIcon height={'100%'} width={'100%'} color={'grey'} /> + </TouchableOpacity> + <SocialIcon style={styles.icon} social={social} /> + <Text style={styles.titleLabel}>{social}</Text> + <Text style={styles.descriptionLabel}> + Insert your {social.toLowerCase()} username to link your{' '} + {social.toLowerCase()} account to your profile! + </Text> + <TextInput + autoCapitalize={'none'} + autoCorrect={false} + placeholder={'Username'} + style={styles.textInput} + onChangeText={setUsername} + selectionColor={'grey'} + value={username} + /> + <TaggSquareButton + title={'Submit'} + onPress={onSubmit} + mode={'gradient'} + color={'white'} + /> </View> - </Modal> - </View> - </> + </CenteredView> + </Modal> + </CenteredView> ); }; const styles = StyleSheet.create({ - centeredView: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - }, modalView: { width: SCREEN_WIDTH * 0.8, backgroundColor: 'white', diff --git a/src/components/common/TaggSquareButton.tsx b/src/components/common/TaggSquareButton.tsx index 78a90554..817a2690 100644 --- a/src/components/common/TaggSquareButton.tsx +++ b/src/components/common/TaggSquareButton.tsx @@ -3,6 +3,7 @@ import { GestureResponderEvent, StyleSheet, Text, + TextStyle, TouchableOpacity, ViewProps, ViewStyle, @@ -14,14 +15,16 @@ import {normalize, SCREEN_WIDTH} from '../../utils'; interface TaggSquareButtonProps extends ViewProps { onPress: (event: GestureResponderEvent) => void; title: string; - mode: 'normal' | 'large' | 'gradient'; - color: 'purple' | 'white'; + buttonStyle: 'normal' | 'large' | 'gradient'; + buttonColor: 'purple' | 'white'; + labelColor: 'white' | 'blue'; style?: ViewStyle; + labelStyle?: TextStyle; } const TaggSquareButton: React.FC<TaggSquareButtonProps> = (props) => { - const buttonStyles = (() => { - switch (props.color) { + const buttonColor = (() => { + switch (props.buttonColor) { case 'purple': return {backgroundColor: TAGG_PURPLE}; case 'white': @@ -29,24 +32,37 @@ const TaggSquareButton: React.FC<TaggSquareButtonProps> = (props) => { return {backgroundColor: 'white'}; } })(); - switch (props.mode) { + const labelColor = (() => { + switch (props.labelColor) { + case 'white': + return {color: 'white'}; + case 'blue': + default: + return {color: '#78A0EF'}; + } + })(); + switch (props.buttonStyle) { case 'large': return ( <TouchableOpacity onPress={props.onPress} - style={[styles.largeButton, buttonStyles, props.style]}> - <Text style={styles.largeLabel}>{props.title}</Text> + style={[styles.largeButton, buttonColor, props.style]}> + <Text style={[styles.largeLabel, labelColor, props.labelStyle]}> + {props.title} + </Text> </TouchableOpacity> ); case 'gradient': return ( - <TouchableOpacity onPress={props.onPress}> + <TouchableOpacity onPress={props.onPress} style={props.style}> <LinearGradient style={styles.gradientButton} colors={BACKGROUND_GRADIENT_MAP[0]} useAngle angle={90}> - <Text style={styles.gradientLabel}>{props.title}</Text> + <Text style={[styles.gradientLabel, props.labelStyle]}> + {props.title} + </Text> </LinearGradient> </TouchableOpacity> ); @@ -55,8 +71,10 @@ const TaggSquareButton: React.FC<TaggSquareButtonProps> = (props) => { return ( <TouchableOpacity onPress={props.onPress} - style={[styles.normalButton, buttonStyles, props.style]}> - <Text style={styles.normalLabel}>{props.title}</Text> + style={[styles.normalButton, buttonColor, props.style]}> + <Text style={[styles.normalLabel, labelColor, props.labelStyle]}> + {props.title} + </Text> </TouchableOpacity> ); } @@ -86,7 +104,6 @@ const styles = StyleSheet.create({ normalLabel: { fontSize: normalize(20), fontWeight: '500', - color: '#78A0EF', }, gradientButton: { marginTop: '8%', diff --git a/src/components/taggs/TwitterTaggPost.tsx b/src/components/taggs/TwitterTaggPost.tsx index 0cfde857..834e32ef 100644 --- a/src/components/taggs/TwitterTaggPost.tsx +++ b/src/components/taggs/TwitterTaggPost.tsx @@ -3,11 +3,7 @@ import {Image, Linking, StyleSheet, View} from 'react-native'; import {Text} from 'react-native-animatable'; import Hyperlink from 'react-native-hyperlink'; import LinearGradient from 'react-native-linear-gradient'; -import { - AVATAR_DIM, - TAGGS_GRADIENT, - TAGG_LIGHT_BLUE, -} from '../../constants'; +import {AVATAR_DIM, TAGGS_GRADIENT, TAGG_LIGHT_BLUE} from '../../constants'; import {TwitterPostType} from '../../types'; import {handleOpenSocialUrlOnBrowser, SCREEN_WIDTH} from '../../utils'; import {DateLabel, PostCarousel} from '../common'; @@ -94,7 +90,10 @@ const TwitterTaggPost: React.FC<TwitterTaggPostProps> = ({ <Text style={styles.replyHandleText} onPress={() => - openTwitterProfileLink(post.in_reply_to?.handle) + handleOpenSocialUrlOnBrowser( + post.in_reply_to?.handle, + 'Twitter', + ) }> @{post.in_reply_to.handle} </Text> diff --git a/src/constants/api.ts b/src/constants/api.ts index 32631be0..165bd550 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -3,7 +3,7 @@ const BASE_URL: string = 'http://127.0.0.1:8000/'; // local server const API_URL: string = BASE_URL + 'api/'; export const LOGIN_ENDPOINT: string = API_URL + 'login/'; -export const LOGOUT_ENDPOINT: string = API_URL + 'logout/'; +export const VERSION_ENDPOINT: string = API_URL + 'version/'; export const REGISTER_ENDPOINT: string = API_URL + 'register/'; export const EDIT_PROFILE_ENDPOINT: string = API_URL + 'edit-profile/'; export const SEND_OTP_ENDPOINT: string = API_URL + 'send-otp/'; diff --git a/src/routes/Routes.tsx b/src/routes/Routes.tsx index a5383a47..1cbc9bc5 100644 --- a/src/routes/Routes.tsx +++ b/src/routes/Routes.tsx @@ -1,18 +1,23 @@ -import React, {useEffect} from 'react'; -import NavigationBar from './tabs'; -import Onboarding from './onboarding'; -import {useSelector, useDispatch} from 'react-redux'; +import messaging from '@react-native-firebase/messaging'; +import React, {useEffect, useState} from 'react'; +import DeviceInfo from 'react-native-device-info'; +import SplashScreen from 'react-native-splash-screen'; +import {useDispatch, useSelector} from 'react-redux'; +import {fcmService, getLiveVersion} from '../services'; +import { + updateNewNotificationReceived, + updateNewVersionAvailable, +} from '../store/actions'; import {RootState} from '../store/rootReducer'; import {userLogin} from '../utils'; -import SplashScreen from 'react-native-splash-screen'; -import messaging from '@react-native-firebase/messaging'; -import {updateNewNotificationReceived} from '../store/actions'; -import {fcmService} from '../services'; +import Onboarding from './onboarding'; +import NavigationBar from './tabs'; const Routes: React.FC = () => { const { user: {userId}, } = useSelector((state: RootState) => state.user); + const [newVersionAvailable, setNewVersionAvailable] = useState(false); const dispatch = useDispatch(); /** @@ -47,7 +52,18 @@ const Routes: React.FC = () => { } }); - return userId ? <NavigationBar /> : <Onboarding />; + useEffect(() => { + const checkVersion = async () => { + const liveVersion = await getLiveVersion(); + if (liveVersion && liveVersion !== DeviceInfo.getVersion()) { + setNewVersionAvailable(true); + dispatch(updateNewVersionAvailable(true)); + } + }; + checkVersion(); + }); + + return userId && !newVersionAvailable ? <NavigationBar /> : <Onboarding />; }; export default Routes; diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx index 2db039c1..450c5039 100644 --- a/src/screens/onboarding/Login.tsx +++ b/src/screens/onboarding/Login.tsx @@ -13,7 +13,7 @@ import { TouchableOpacity, } from 'react-native'; import SplashScreen from 'react-native-splash-screen'; -import {useDispatch} from 'react-redux'; +import {useDispatch, useSelector} from 'react-redux'; import {Background, TaggInput, TaggSquareButton} from '../../components'; import {LOGIN_ENDPOINT, usernameRegex} from '../../constants'; import { @@ -25,8 +25,10 @@ import { } from '../../constants/strings'; import {OnboardingStackParams} from '../../routes/onboarding'; import {fcmService} from '../../services'; +import {RootState} from '../../store/rootReducer'; import {BackgroundGradientType, UserType} from '../../types'; import {normalize, userLogin} from '../../utils'; +import UpdateRequired from './UpdateRequired'; type VerificationScreenRouteProp = RouteProp<OnboardingStackParams, 'Login'>; type VerificationScreenNavigationProp = StackNavigationProp< @@ -45,11 +47,6 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { // ref for focusing on input fields const inputRef = useRef(); - const NO_USER: UserType = { - userId: '', - username: '', - }; - // login form state const [form, setForm] = React.useState({ username: '', @@ -59,7 +56,7 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { attemptedSubmit: false, token: '', }); - const [user, setUser] = useState<UserType>(NO_USER); + const {newVersionAvailable} = useSelector((state: RootState) => state.user); /** * Redux Store stuff @@ -167,7 +164,6 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { userLogin(dispatch, {userId: data.UserID, username}); fcmService.sendFcmTokenToServer(); } catch (err) { - setUser(NO_USER); console.log(data); Alert.alert(ERROR_INVALID_LOGIN); } @@ -216,6 +212,8 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { style={styles.container} gradientType={BackgroundGradientType.Light}> <StatusBar barStyle="light-content" /> + {/* <Modal visible={newVersionAvailable} /> */} + <UpdateRequired visible={newVersionAvailable} /> <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.keyboardAvoidingView}> @@ -259,14 +257,16 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { <TaggSquareButton onPress={handleLogin} title={'Login'} - mode={'normal'} - color={'white'} + buttonStyle={'normal'} + buttonColor={'white'} + labelColor={'blue'} /> <TaggSquareButton onPress={startRegistrationProcess} title={'Sign up'} - mode={'normal'} - color={'purple'} + buttonStyle={'normal'} + buttonColor={'purple'} + labelColor={'blue'} /> </KeyboardAvoidingView> </Background> diff --git a/src/screens/onboarding/UpdateRequired.tsx b/src/screens/onboarding/UpdateRequired.tsx new file mode 100644 index 00000000..adf7ba71 --- /dev/null +++ b/src/screens/onboarding/UpdateRequired.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import {Image, Linking, Modal, StyleSheet, View} from 'react-native'; +import {Text} from 'react-native-animatable'; +import {CenteredView, TaggSquareButton} from '../../components'; +import {normalize, SCREEN_WIDTH} from '../../utils'; + +interface UpdateRequiredProps { + visible: boolean; +} + +const UpdateRequired: React.FC<UpdateRequiredProps> = ({visible}) => { + return ( + <Modal animationType={'slide'} transparent={true} visible={visible}> + <CenteredView> + <View style={styles.contentContainer}> + <Image + style={styles.logo} + source={require('../../assets/images/logo-purple.png')} + /> + <Text style={styles.header}>Update Required</Text> + <Text style={styles.body}> + You have to update your app to continue using Tagg, please download + the latest version from the app store + </Text> + <TaggSquareButton + title={'Update'} + onPress={() => { + Linking.openURL( + 'https://apps.apple.com/us/app/tagg-discover-your-community/id1537853613', + ); + }} + buttonStyle={'normal'} + buttonColor={'purple'} + labelColor={'white'} + labelStyle={styles.button} + /> + </View> + </CenteredView> + </Modal> + ); +}; + +const styles = StyleSheet.create({ + contentContainer: { + marginTop: '20%', + width: SCREEN_WIDTH * 0.9, + backgroundColor: 'white', + borderRadius: 5, + padding: '10%', + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.25, + shadowRadius: 3.84, + elevation: 5, + }, + logo: { + width: normalize(60), + height: normalize(60), + marginBottom: '10%', + }, + header: { + fontSize: normalize(17), + fontWeight: '700', + lineHeight: 20, + marginBottom: '5%', + }, + body: { + fontSize: normalize(13), + color: 'grey', + lineHeight: 20, + textAlign: 'center', + width: SCREEN_WIDTH * 0.8, + marginBottom: '10%', + }, + button: { + fontWeight: '700', + }, +}); + +export default UpdateRequired; diff --git a/src/screens/onboarding/WelcomeScreen.tsx b/src/screens/onboarding/WelcomeScreen.tsx index bfb1a127..ae31f933 100644 --- a/src/screens/onboarding/WelcomeScreen.tsx +++ b/src/screens/onboarding/WelcomeScreen.tsx @@ -39,8 +39,9 @@ const WelcomeScreen: React.FC<WelcomeScreenProps> = ({navigation}) => { <TaggSquareButton onPress={handleNext} title={'Next'} - mode={'large'} - color={'purple'} + buttonStyle={'large'} + buttonColor={'purple'} + labelColor={'white'} style={styles.nextButton} /> </Background> diff --git a/src/screens/onboarding/index.ts b/src/screens/onboarding/index.ts index 14d0e405..596683e5 100644 --- a/src/screens/onboarding/index.ts +++ b/src/screens/onboarding/index.ts @@ -14,3 +14,4 @@ export {default as CategorySelection} from './CategorySelection'; export {default as AddWaitlistUserScreen} from './AddWaitlistUserScreen'; export {default as WaitlistSuccessScreen} from './WaitlistSuccessScreen'; export {default as CreateCustomCategory} from './CreateCustomCategory'; +export {default as UpdateRequired} from './UpdateRequired'; diff --git a/src/services/CommonService.ts b/src/services/CommonService.ts index dfbbf70e..9fa7417f 100644 --- a/src/services/CommonService.ts +++ b/src/services/CommonService.ts @@ -1,4 +1,5 @@ import RNFetchBlob from 'rn-fetch-blob'; +import {VERSION_ENDPOINT} from '../constants'; export const loadImageFromURL = async (url: string) => { try { @@ -20,3 +21,13 @@ export const loadImageFromURL = async (url: string) => { return undefined; } }; + +export const getLiveVersion = async () => { + try { + const response = await fetch(VERSION_ENDPOINT, {method: 'GET'}); + return response.status === 200 ? await response.json() : undefined; + } catch (error) { + console.log(error); + return undefined; + } +}; diff --git a/src/store/actions/user.ts b/src/store/actions/user.ts index 5f49a103..589e6f0d 100644 --- a/src/store/actions/user.ts +++ b/src/store/actions/user.ts @@ -1,18 +1,19 @@ -import { CommentThreadType } from './../../types/types'; -import {RootState} from '../rootReducer'; -import {UserType} from '../../types/types'; -import {loadProfileInfo, loadAvatar, loadCover} from '../../services'; import {Action, ThunkAction} from '@reduxjs/toolkit'; +import {loadAvatar, loadCover, loadProfileInfo} from '../../services'; +import {UserType} from '../../types/types'; +import {getTokenOrLogout} from '../../utils'; import { - userLoggedIn, - userDetailsFetched, - socialEdited, profileCompletionStageUpdated, setIsOnboardedUser, setNewNotificationReceived, + setNewVersionAvailable, setReplyPosted, + socialEdited, + userDetailsFetched, + userLoggedIn, } from '../reducers'; -import {getTokenOrLogout} from '../../utils'; +import {RootState} from '../rootReducer'; +import {CommentThreadType} from './../../types/types'; /** * Entry point to our store. @@ -98,6 +99,21 @@ export const updateIsOnboardedUser = ( } }; +export const updateNewVersionAvailable = ( + newVersionAvailable: boolean, +): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( + dispatch, +) => { + try { + dispatch({ + type: setNewVersionAvailable.type, + payload: {newVersionAvailable}, + }); + } catch (error) { + console.log(error); + } +}; + export const updateNewNotificationReceived = ( newNotificationReceived: boolean, ): ThunkAction<Promise<void>, RootState, unknown, Action<string>> => async ( diff --git a/src/store/initialStates.ts b/src/store/initialStates.ts index 8d137a5d..6ca133e0 100644 --- a/src/store/initialStates.ts +++ b/src/store/initialStates.ts @@ -44,6 +44,7 @@ export const NO_USER_DATA = { avatar: <string | null>'', cover: <string | null>'', isOnboardedUser: false, + newVersionAvailable: false, newNotificationReceived: false, replyPosted: <CommentThreadType | undefined>undefined, }; diff --git a/src/store/reducers/userReducer.ts b/src/store/reducers/userReducer.ts index 1e575339..29ec38cc 100644 --- a/src/store/reducers/userReducer.ts +++ b/src/store/reducers/userReducer.ts @@ -57,6 +57,10 @@ const userDataSlice = createSlice({ setReplyPosted: (state, action) => { state.replyPosted = action.payload.replyPosted; }, + + setNewVersionAvailable: (state, action) => { + state.newVersionAvailable = action.payload.newVersionAvailable; + }, }, }); @@ -66,6 +70,7 @@ export const { socialEdited, profileCompletionStageUpdated, setIsOnboardedUser, + setNewVersionAvailable, setNewNotificationReceived, setReplyPosted, } = userDataSlice.actions; |