Skip to content

Commit

Permalink
Cwz/ocf 33 auth context (#19)
Browse files Browse the repository at this point in the history
* Added auth context for login

---------

Co-authored-by: Chris Torres <[email protected]>
  • Loading branch information
carolynzhuang and christophertorres1 authored Dec 11, 2024
1 parent 6d4bb48 commit fbff3e7
Show file tree
Hide file tree
Showing 10 changed files with 1,311 additions and 1,055 deletions.
5 changes: 4 additions & 1 deletion App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { useFonts } from 'expo-font';
import { DMSans_400Regular, DMSans_700Bold } from '@expo-google-fonts/dm-sans';
import { DefaultTheme } from '@react-navigation/native';
import { AuthContextProvider } from '@/context/AuthContext';
import AppNavigator from '@/navigation/AppNavigator';
import colors from '@/styles/colors';

Expand All @@ -25,7 +26,9 @@ export default function App() {

return (
<GestureHandlerRootView style={{ flex: 1 }}>
<AppNavigator />
<AuthContextProvider>
<AppNavigator />
</AuthContextProvider>
</GestureHandlerRootView>
);
}
2,133 changes: 1,154 additions & 979 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
"@expo-google-fonts/dm-sans": "^0.2.3",
"@expo/metro-runtime": "~4.0.0",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-navigation/bottom-tabs": "^7.0.6",
"@react-navigation/elements": "^2.1.0",
"@react-navigation/native": "^7.0.3",
"@react-navigation/native-stack": "^7.1.1",
"@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/elements": "^1.3.31",
"@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0",
"@supabase/supabase-js": "^2.45.6",
"@svgr/cli": "^8.1.0",
"@types/react-navigation": "^3.0.8",
Expand Down Expand Up @@ -75,8 +75,8 @@
"@eslint/object-schema": "^2.1.4",
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
"@types/react": "~18.3.12",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
"babel-plugin-module-resolver": "^5.0.2",
"eslint": "^8.57.1",
"eslint-config-expo": "~8.0.1",
Expand Down
74 changes: 24 additions & 50 deletions src/components/GoogleSignInButton/GoogleSignInButton.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { Text, TouchableOpacity } from 'react-native';
import { makeRedirectUri } from 'expo-auth-session';
import * as Google from 'expo-auth-session/providers/google';
import * as WebBrowser from 'expo-web-browser';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { CompositeScreenProps } from '@react-navigation/native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { useAuth } from '@/context/AuthContext';
import { LoginStackParamList, RootStackParamList } from '@/types/navigation';
import { styles } from './styles';

Expand All @@ -16,76 +16,50 @@ type GoogleSignInButtonProps = CompositeScreenProps<
NativeStackScreenProps<RootStackParamList, 'BottomTabs'>
>;

type UserInfo = {
email: string;
name: string;
};

export default function GoogleSignInButton({
navigation,
}: GoogleSignInButtonProps) {
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
const [request, response, promptAsync] = Google.useAuthRequest({
const { setAuthenticated } = useAuth();
const [, response, promptAsync] = Google.useAuthRequest({
webClientId: process.env.EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID,
androidClientId: process.env.EXPO_PUBLIC_GOOGLE_ANDROID_CLIENT_ID,
iosClientId: process.env.EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID,
redirectUri: makeRedirectUri({ scheme: 'org.calblueprint.ourcityforest' }),
});

console.log(request);
console.log(userInfo);

useEffect(() => {
async function handleSignInWithGoogle() {
const handleSignInWithGoogle = async (token: string) => {
try {
const userJSON = await AsyncStorage.getItem('@user');
if (userJSON) {
setUserInfo(JSON.parse(userJSON));
} else if (
response?.type === 'success' &&
response.authentication?.accessToken
) {
await getUserInfo(response.authentication.accessToken);
const userResponse = await fetch(
'https://www.googleapis.com/userinfo/v2/me',
{
headers: { Authorization: `Bearer ${token}` },
},
);

if (userResponse.ok) {
await setAuthenticated(true);
navigation.navigate('BottomTabs', {
screen: 'Home',
params: { screen: 'TreeSearch' },
});
} else {
console.error('Authentication failed');
await setAuthenticated(false);
}
} catch (error) {
console.error('Error retrieving user data from AsyncStorage:', error);
console.error('Failed to sign in with Google:', error);
await setAuthenticated(false);
}
}

handleSignInWithGoogle();
}, [navigation, response]);
};

const getUserInfo = async (token: string) => {
if (!token) return;
try {
const userResponse = await fetch(
'https://www.googleapis.com/userinfo/v2/me',
{
headers: { Authorization: `Bearer ${token}` },
},
);
const user = await userResponse.json();
await AsyncStorage.setItem('@user', JSON.stringify(user));
setUserInfo(user);
} catch (error) {
console.error('Failed to fetch user data:', error);
if (response?.type === 'success' && response.authentication?.accessToken) {
handleSignInWithGoogle(response.authentication.accessToken);
}
};
}, [response, navigation, setAuthenticated]);

return (
<TouchableOpacity
onPress={() => {
promptAsync();
navigation.navigate('BottomTabs', {
screen: 'Home',
params: { screen: 'TreeSearch' },
});
}}
>
<TouchableOpacity onPress={() => promptAsync()}>
<Text style={styles.adminLoginLinkText}>Login Here</Text>
</TouchableOpacity>
);
Expand Down
37 changes: 30 additions & 7 deletions src/components/GoogleSignOutButton/GoogleSignOutButton.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,38 @@
import { Text, TouchableOpacity } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { CommonActions } from '@react-navigation/native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { useAuth } from '@/context/AuthContext';
import { ContactStackParamList } from '@/types/navigation';
import { styles } from './styles';

export default function GoogleSignOutButton() {
type GoogleSignOutButtonProps = NativeStackScreenProps<
ContactStackParamList,
'Contact'
>;

export default function GoogleSignOutButton({
navigation,
}: GoogleSignOutButtonProps) {
const { setAuthenticated } = useAuth();

const handleSignOut = async () => {
try {
await AsyncStorage.setItem('authenticated', 'false');
await setAuthenticated(false);
navigation.dispatch(
CommonActions.reset({
index: 0,
routes: [{ name: 'LoginStack' }],
}),
);
} catch (error) {
console.error('Error signing out:', error);
}
};

return (
<TouchableOpacity
style={styles.button}
onPress={() => {
AsyncStorage.removeItem('@user');
}}
>
<TouchableOpacity style={styles.button} onPress={handleSignOut}>
<Text style={styles.buttonText}>Sign out</Text>
</TouchableOpacity>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/SpeciesDisplay/SpeciesDisplay.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Fragment } from 'react';
import React from 'react';
import { Text, TextInput, View } from 'react-native';
import SvgBear from '@/icons/Bear';
import SvgFlash from '@/icons/Flash';
Expand Down
56 changes: 56 additions & 0 deletions src/context/AuthContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, {
createContext,
ReactNode,
useContext,
useEffect,
useState,
} from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';

type AuthState = {
isAuthenticated: boolean;
setAuthenticated: (value: boolean) => Promise<void>;
};

const AuthContext = createContext<AuthState | undefined>(undefined);

export const AuthContextProvider = ({ children }: { children: ReactNode }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);

useEffect(() => {
const syncAuthState = async () => {
try {
const storedAuth = await AsyncStorage.getItem('authenticated');
console.log('Auth state from storage:', storedAuth);
setIsAuthenticated(storedAuth === 'true');
} catch (error) {
console.error('Error loading authentication state:', error);
}
};

syncAuthState();
}, []);

const setAuthenticated = async (value: boolean) => {
try {
await AsyncStorage.setItem('authenticated', value ? 'true' : 'false');
setIsAuthenticated(value);
} catch (error) {
console.error('Failed to update authentication state:', error);
}
};

return (
<AuthContext.Provider value={{ isAuthenticated, setAuthenticated }}>
{children}
</AuthContext.Provider>
);
};

export const useAuth = (): AuthState => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
27 changes: 19 additions & 8 deletions src/navigation/AppNavigator.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import QRCodeScanner from '@/components/QRCodeScanner/QRCodeScanner';
import { useAuth } from '@/context/AuthContext';
import SvgContactSelected from '@/icons/ContactSelected';
import SvgContactUnselected from '@/icons/ContactUnselected';
import SvgHomeSelected from '@/icons/HomeSelected';
Expand Down Expand Up @@ -104,15 +104,26 @@ function BottomTabNavigator() {

// Root Navigator
export default function AppNavigator() {
const { isAuthenticated } = useAuth();

return (
<NavigationContainer>
<RootStack.Navigator
initialRouteName="LoginStack"
screenOptions={{ headerShown: false }}
>
<RootStack.Screen name="LoginStack" component={LoginStackNavigator} />
<RootStack.Screen name="BottomTabs" component={BottomTabNavigator} />
</RootStack.Navigator>
{isAuthenticated ? (
<RootStack.Navigator
initialRouteName="BottomTabs"
screenOptions={{ headerShown: false }}
>
<RootStack.Screen name="BottomTabs" component={BottomTabNavigator} />
</RootStack.Navigator>
) : (
<RootStack.Navigator
initialRouteName="LoginStack"
screenOptions={{ headerShown: false }}
>
<RootStack.Screen name="LoginStack" component={LoginStackNavigator} />
<RootStack.Screen name="BottomTabs" component={BottomTabNavigator} />
</RootStack.Navigator>
)}
</NavigationContainer>
);
}
16 changes: 15 additions & 1 deletion src/screens/Contact/Contact.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import React from 'react';
import { Image, ScrollView, Text, View } from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import GoogleSignOutButton from '@/components/GoogleSignOutButton/GoogleSignOutButton';
import { ContactStackParamList } from '@/types/navigation';
import { styles } from './styles';

export default function ContactScreen() {
type ContactScreenProps = NativeStackScreenProps<
ContactStackParamList,
'Contact'
>;

export default function ContactScreen({
navigation,
route,
}: ContactScreenProps) {
return (
<ScrollView style={styles.backgroundContainer}>
<View style={styles.imageContainer}>
Expand All @@ -17,6 +28,9 @@ export default function ContactScreen() {
</View>

<View style={styles.contactInfo}>
{/* temporary button */}
<GoogleSignOutButton navigation={navigation} route={route} />

<View>
<Text style={styles.contactHeader}>Contact Us</Text>
</View>
Expand Down
4 changes: 2 additions & 2 deletions src/screens/Login/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export const styles = StyleSheet.create({

logo: {
height: 200,
marginTop: 40,
marginBottom: 40,
marginTop: 20,
marginBottom: 70,
},

button: {
Expand Down

0 comments on commit fbff3e7

Please sign in to comment.