I'm implementing Google authentication in my React Native project using the expo-auth-session
library.
The issue is that after selecting the user and accepting the consent screen, when the redirectBack happens to return the Google response token, the AuthContext that manages the authentication seems to be misconfigured. I believe the redirect causes the view to reload, and the response isn't processed correctly.
Why do I think the issue is with the AuthContext? Because if I move all the login logic into the main _layout.tsx file and remove the AuthContext, it works and correctly returns the token.
The curious thing is that the login works correctly... (with my back-end), i think it might be something related to Google's redirect
This is my app directory structure:
Alright, this is the app/_layout.tsx
(entry point)
function RootLayoutNavigation() {
const tamaguiConfig = createTamagui(config);
return (
<TamaguiProvider config={tamaguiConfig}>
<Theme name="light">
<AuthProvider>
<SafeAreaView style={{ flex: 1 }}>
<StatusBar barStyle="default" backgroundColor="#f2f2f2" translucent />
<Slot />
</SafeAreaView>
</AuthProvider>
</Theme>
</TamaguiProvider>
);
}
To manage authentication, I have a Context created (which I suspect is misconfigured and might be the source of the issue)
AuthContext.tsx
import React, {useContext, createContext, type PropsWithChildren, useEffect, useState} from 'react';
import {loadUser, login, logout} from "~/services/AuthService";
import {useRouter, useSegments} from "expo-router";
import {getToken} from "~/services/TokenService";
import {ActivityIndicator} from "react-native";
import {View} from "tamagui";
type User = any;
interface AuthContextType {
user: User | null;
signIn: (userData: User) => void;
signOut: () => Promise<void>;
isLoading: boolean;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType>({
user: null,
signIn: () => {},
signOut: async () => {},
isLoading: true,
isAuthenticated: false,
});
function useProtectedRoute(user: User | null) {
const segments = useSegments();
const router = useRouter();
console.log("segments", segments)
useEffect(() => {
if (segments.length === 0) return;
const inAuthGroup = segments[0] === '(auth)';
if (!user && !inAuthGroup) {
console.log("entra 1")
router.replace('/(auth)/login');
} else if (user && inAuthGroup) {
console.log("entra 2")
router.replace('/(drawer)');
}
}, [user, segments]);
}
export function AuthProvider({ children }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
useProtectedRoute(user);
useEffect(() => {
async function loadSession() {
try {
const token = await getToken();
if (token) {
const userData = await loadUser();
setUser(userData);
setIsAuthenticated(true);
}
} catch (error) {
console.error('Error loading session:', error);
} finally {
setIsLoading(false);
}
}
loadSession();
}, []);
const signIn = (userData: User) => {
setUser(userData);
setIsAuthenticated(true);
};
const signOut = async () => {
try {
await logout();
setUser(null);
setIsAuthenticated(false);
} catch (error) {
console.error('Error during logout:', error);
}
};
return (
<AuthContext.Provider value={{ user, signIn, signOut, isLoading, isAuthenticated }}>
{!isLoading ? children : (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
</View>
)}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
And the login page login.tsx
const Login = () => {
const { signIn } = useAuth();
const router = useRouter()
const [isLoadingLogin, setIsLoadingLogin] = useState(false)
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [errors, setErrors] = useState({})
const config = {
androidClientId: "client-id",
webClientId: "client-id 2",
expoClientId: "client-id 3"
}
const [request, response, promptAsync] = Google.useAuthRequest(config);
React.useEffect(() => {
handleEffect()
}, [response])
async function handleEffect(){
// Here is always null.
console.warn("handle Effect() - token " + response)
}
return (
<SafeAreaView style={{flex: 1, backgroundColor: "#e3e3e3"}}>
<H4>Log-in</H4>
<Text>{JSON.stringify(response)}</Text> // Also there is always null.
<View minWidth={350} marginVertical={12} flex={1} gap={10}>
<View marginBottom={10}>
<Input
placeholder="Email"
value={email}
onChangeText={(text) => {
setEmail(text);
}}
keyboardType="email-address"
autoCapitalize="none"
/>
</View>
<View>
<Input
placeholder="Contraseña"
value={password}
onChangeText={setPassword}
secureTextEntry={true}
autoCapitalize="none"
/>
</View>
<View>
<Pressable
onPress={() => promptAsync()}
style={({ pressed }) => ({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
padding: 10,
borderRadius: 30,
backgroundColor: pressed ? '#f0f0f0' : '#ffffff',
borderWidth: 1,
borderColor: '#dcdcdc',
})}
>
<Text paddingRight={9} fontWeight={600} fontSize={15.5}>
Entrar con Google
</Text>
<Image
source={require('~/assets/images/google-logo.png')}
style={{
width: 22,
height: 22,
}}
/>
</Pressable>
</View>
</View>
</SafeAreaView>
)
}
I'm implementing Google authentication in my React Native project using the expo-auth-session
library.
The issue is that after selecting the user and accepting the consent screen, when the redirectBack happens to return the Google response token, the AuthContext that manages the authentication seems to be misconfigured. I believe the redirect causes the view to reload, and the response isn't processed correctly.
Why do I think the issue is with the AuthContext? Because if I move all the login logic into the main _layout.tsx file and remove the AuthContext, it works and correctly returns the token.
The curious thing is that the login works correctly... (with my back-end), i think it might be something related to Google's redirect
This is my app directory structure:
Alright, this is the app/_layout.tsx
(entry point)
function RootLayoutNavigation() {
const tamaguiConfig = createTamagui(config);
return (
<TamaguiProvider config={tamaguiConfig}>
<Theme name="light">
<AuthProvider>
<SafeAreaView style={{ flex: 1 }}>
<StatusBar barStyle="default" backgroundColor="#f2f2f2" translucent />
<Slot />
</SafeAreaView>
</AuthProvider>
</Theme>
</TamaguiProvider>
);
}
To manage authentication, I have a Context created (which I suspect is misconfigured and might be the source of the issue)
AuthContext.tsx
import React, {useContext, createContext, type PropsWithChildren, useEffect, useState} from 'react';
import {loadUser, login, logout} from "~/services/AuthService";
import {useRouter, useSegments} from "expo-router";
import {getToken} from "~/services/TokenService";
import {ActivityIndicator} from "react-native";
import {View} from "tamagui";
type User = any;
interface AuthContextType {
user: User | null;
signIn: (userData: User) => void;
signOut: () => Promise<void>;
isLoading: boolean;
isAuthenticated: boolean;
}
const AuthContext = createContext<AuthContextType>({
user: null,
signIn: () => {},
signOut: async () => {},
isLoading: true,
isAuthenticated: false,
});
function useProtectedRoute(user: User | null) {
const segments = useSegments();
const router = useRouter();
console.log("segments", segments)
useEffect(() => {
if (segments.length === 0) return;
const inAuthGroup = segments[0] === '(auth)';
if (!user && !inAuthGroup) {
console.log("entra 1")
router.replace('/(auth)/login');
} else if (user && inAuthGroup) {
console.log("entra 2")
router.replace('/(drawer)');
}
}, [user, segments]);
}
export function AuthProvider({ children }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
useProtectedRoute(user);
useEffect(() => {
async function loadSession() {
try {
const token = await getToken();
if (token) {
const userData = await loadUser();
setUser(userData);
setIsAuthenticated(true);
}
} catch (error) {
console.error('Error loading session:', error);
} finally {
setIsLoading(false);
}
}
loadSession();
}, []);
const signIn = (userData: User) => {
setUser(userData);
setIsAuthenticated(true);
};
const signOut = async () => {
try {
await logout();
setUser(null);
setIsAuthenticated(false);
} catch (error) {
console.error('Error during logout:', error);
}
};
return (
<AuthContext.Provider value={{ user, signIn, signOut, isLoading, isAuthenticated }}>
{!isLoading ? children : (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
</View>
)}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
And the login page login.tsx
const Login = () => {
const { signIn } = useAuth();
const router = useRouter()
const [isLoadingLogin, setIsLoadingLogin] = useState(false)
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [errors, setErrors] = useState({})
const config = {
androidClientId: "client-id",
webClientId: "client-id 2",
expoClientId: "client-id 3"
}
const [request, response, promptAsync] = Google.useAuthRequest(config);
React.useEffect(() => {
handleEffect()
}, [response])
async function handleEffect(){
// Here is always null.
console.warn("handle Effect() - token " + response)
}
return (
<SafeAreaView style={{flex: 1, backgroundColor: "#e3e3e3"}}>
<H4>Log-in</H4>
<Text>{JSON.stringify(response)}</Text> // Also there is always null.
<View minWidth={350} marginVertical={12} flex={1} gap={10}>
<View marginBottom={10}>
<Input
placeholder="Email"
value={email}
onChangeText={(text) => {
setEmail(text);
}}
keyboardType="email-address"
autoCapitalize="none"
/>
</View>
<View>
<Input
placeholder="Contraseña"
value={password}
onChangeText={setPassword}
secureTextEntry={true}
autoCapitalize="none"
/>
</View>
<View>
<Pressable
onPress={() => promptAsync()}
style={({ pressed }) => ({
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
padding: 10,
borderRadius: 30,
backgroundColor: pressed ? '#f0f0f0' : '#ffffff',
borderWidth: 1,
borderColor: '#dcdcdc',
})}
>
<Text paddingRight={9} fontWeight={600} fontSize={15.5}>
Entrar con Google
</Text>
<Image
source={require('~/assets/images/google-logo.png')}
style={{
width: 22,
height: 22,
}}
/>
</Pressable>
</View>
</View>
</SafeAreaView>
)
}
The problem came because I didn't have the redirectUri parameter in Google.useAuthRequest.
const config = {
androidClientId: "client-id",
webClientId: "client-id 2",
expoClientId: "client-id 3",
redirectUri: "schema:///login" - Need this line of code.
This way the token returns to the view once the redirection is done to your app.
In my case, despite having everything well configured, what happened is that when doing the redirect back, because of how I had configured my AuthContext, the token did not return to the log-in and did not capture it.
The curious thing about this is that if I implemented this logic directly in the entry point of the app app/_layout.tsx
it worked correctly, but it must be that by some move of the AuthContext that I have created, it was necessary to manage well the redirect issue.
Expo Auth Session Library Doc: https://docs.expo.dev/versions/latest/sdk/auth-session/