Expo AuthSession Google登录首次调用报错 二次点击正常
Expo AuthSession 接入Google登录问题修复
问题现象
- 首次打开应用调用Google登录函数直接报错,第二次点击调用时登录功能可正常运行
- 抛出报错信息:
Cannot prompt to authenticate until the request has finished loading.(无法在身份验证请求加载完成前弹出授权提示)
- 用户完成登录后刷新应用,无法默认获取到已登录的用户信息,Firebase登录态丢失
原实现代码如下:
import React, { useEffect, useState, createContext, useContext, useMemo } from "react" import * as Google from "expo-auth-session/providers/google" import * as WebBrowser from "expo-web-browser" import { GoogleAuthProvider, signInWithCredential, auth, onAuthStateChanged, signOut } from "../firebase" WebBrowser.maybeCompleteAuthSession() const config = { expoClientId: xxxx webClientId: xxxx iosClientId: xxxx androidClientId: xxxx } const AuthContext = createContext({}); export const AuthProvider = ({children}) => { const [user,setUser] = useState(null) const [error,setError] = useState(null) const [req, res, login] = Google.useAuthRequest(config) const [loading, setLoading] = useState(true) useEffect( () => onAuthStateChanged(auth, user => { setLoading(true) if (user) { setUser(user) setLoading(false) } else { setUser(null) } setLoading(false) }), [] ) const signInWithGoogle = () => { setLoading(true) login({ showInRecents: true }) .then(async response => { if (response && response.type === "success") { const { accessToken, idToken } = response.authentication const credential = GoogleAuthProvider.credential(idToken, accessToken) await signInWithCredential(auth, credential) setLoading(false) } }) .catch(error => console.error(error)) .finally(() => setLoading(false)) }
问题根因
- 首次点击报错:
Google.useAuthRequest钩子初始化是异步过程,组件首次挂载阶段授权请求还未完成加载,此时直接调用login()方法就会触发对应报错。等第一次点击报错后,授权请求已经在后台完成加载,所以第二次点击可以正常运行。另外原代码仅依赖login()返回的Promise处理响应,不符合expo-auth-session官方推荐的响应监听逻辑,容易出现状态同步问题。 - 刷新丢失登录态:Expo/React Native 环境下Firebase Auth默认使用内存级临时存储保存登录态,应用刷新/重启后内存数据清空,自然读不到之前的登录信息,没有配置适配RN环境的本地持久化方案。
- 细节语法问题:原代码中
config对象的多个属性之间缺少逗号分隔,严格模式下会直接抛出语法错误;监听登录态的useEffect没有返回清理函数,存在内存泄漏风险。
修复方案
步骤1:修复首次点击报错问题
- 调用
login()方法前增加判断,只有当授权请求对象req不为空(代表初始化完成)时才触发登录流程 - 用
useEffect监听useAuthRequest返回的res响应对象,统一处理登录成功/失败/取消逻辑,替代直接在login()Promise里写业务逻辑的写法 - 补全配置对象的语法分隔符,给登录态监听事件增加卸载清理逻辑
步骤2:修复Firebase登录态持久化问题
- 安装依赖:
npx expo install @react-native-async-storage/async-storage - 初始化Firebase Auth时,显式配置持久化方案为AsyncStorage本地存储,保证应用重启后登录态不丢失
修复后完整代码
import React, { useEffect, useState, createContext, useContext } from "react" 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 { GoogleAuthProvider, signInWithCredential, initializeAuth, getReactNativePersistence, onAuthStateChanged, signOut } from "firebase/auth" // 注意:替换你原来../firebase里的auth初始化代码,参考如下 // import { initializeApp } from "firebase/app" // const firebaseConfig = {/* 你的firebase配置 */} // const app = initializeApp(firebaseConfig) // const auth = initializeAuth(app, { // persistence: getReactNativePersistence(AsyncStorage) // }) WebBrowser.maybeCompleteAuthSession() const config = { expoClientId: 'xxxx', webClientId: 'xxxx', iosClientId: 'xxxx', androidClientId: 'xxxx' } const AuthContext = createContext({}); export const AuthProvider = ({children}) => { const [user,setUser] = useState(null) const [error,setError] = useState(null) const [loading, setLoading] = useState(true) const [req, res, login] = Google.useAuthRequest(config) // 监听Google授权响应 useEffect(() => { if (res?.type === "success") { const { accessToken, idToken } = res.authentication const credential = GoogleAuthProvider.credential(idToken, accessToken) signInWithCredential(auth, credential) .catch(err => setError(err)) .finally(() => setLoading(false)) } // 处理用户取消/关闭授权页的情况 if (res?.type === "cancel" || res?.type === "dismiss") { setLoading(false) } }, [res]) // 监听Firebase全局登录态 useEffect(() => { const unsub = onAuthStateChanged(auth, user => { setLoading(true) if (user) { setUser(user) } else { setUser(null) } setLoading(false) }) // 组件卸载时清理监听 return () => unsub() }, []) const signInWithGoogle = async () => { setLoading(true) setError(null) // 核心校验:请求未初始化完成时拦截调用 if (!req) { setLoading(false) setError(new Error("登录模块初始化中,请稍后重试")) return } try { await login({ showInRecents: true }) } catch (err) { setError(err) setLoading(false) } } const value = { user, loading, error, signInWithGoogle, signOut: () => signOut(auth) } return ( <AuthContext.Provider value={value}> {children} </AuthContext.Provider> ) } export const useAuth = () => useContext(AuthContext)
额外注意点
- 开发环境下建议暂时关闭React Strict Mode,严格模式下的双执行机制会导致auth钩子重复初始化,偶发触发同类报错
- 确保Google云控制台里配置的OAuth客户端ID和config中填写的完全匹配,安卓端要正确配置包名和调试/发布版SHA签名
内容的提问来源于stack exchange,提问作者user875254
相关产品推荐
相关产品推荐

