用 Expo React Native 构建一个社交登录应用
本教程演示了如何使用 Expo 构建一个实现社交认证的 React Native 应用。该应用展示了完整的认证流程,并使用以下方式实现受保护的导航:
🌐 This tutorial demonstrates how to build a React Native app with Expo that implements social authentication. The app showcases a complete authentication flow with protected navigation using:
- Supabase 数据库 - 一个用于存储用户数据的 Postgres 数据库,带有 行级安全,确保数据受到保护,用户只能访问自己的信息。
- Supabase Auth - 让用户可以通过社交认证提供商(Apple 和 Google)登录。

如果你在使用本指南的过程中遇到困难,可以参考 GitHub 上的完整示例。
🌐 If you get stuck while working through this guide, refer to the full example on GitHub.
项目设置 #
🌐 Project setup
在你开始构建之前,你需要先设置数据库和 API。你可以通过在 Supabase 中启动一个新项目,然后在数据库中创建一个“架构”来完成这一步。
🌐 Before you start building you need to set up the Database and API. You can do this by starting a new Project in Supabase and then creating a "schema" inside the database.
创建一个项目 #
🌐 Create a project
- 在 Supabase 仪表板中创建一个新项目。
- 输入你的项目详情。
- 等新数据库上线。
设置数据库模式 #
🌐 Set up the database schema
现在设置数据库模式。你可以在 SQL 编辑器中使用“用户管理入门”快速开始,也可以复制粘贴下面的 SQL 并运行。
🌐 Now set up the database schema. You can use the "User Management Starter" quickstart in the SQL Editor, or you can copy/paste the SQL from below and run it.
- 在仪表板中转到SQL 编辑器页面。
- 在 参考 > 示例 选项卡下点击 用户管理入门。
- 点击运行。
你可以通过运行 db pull 命令将数据库架构拉到本地项目。查看本地开发文档获取详细说明。
🌐 You can pull the database schema down to your local project by running the db pull command. Read the local development docs for detailed instructions.
supabase link --project-ref <project-id># You can get <project-id> from your project's dashboard URL: https://supabase.nodejs.cn/dashboard/project/<project-id>supabase db pull获取 API 详情 #
🌐 Get API details
要与数据库表中的数据进行交互,你可以使用封装了自动生成的数据 API 端点的客户端库,并使用来自项目 Connect 对话框的项目 URL 和密钥进行认证。
🌐 To interact with data in database tables, you use the client libraries that wrap the auto-generated Data API endpoints, authenticating using the Project URL and key from the project Connect dialog.
正在构建应用 #
🌐 Building the app
先从零开始搭建 React Native 应用吧。
🌐 Start by building the React Native app from scratch.
初始化一个 React Native 应用 #
🌐 Initialize a React Native app
使用 Expo 初始化一个名为 expo-social-auth 的应用,使用 标准模板:
🌐 Use Expo to initialize an app called expo-social-auth with the standard template:
npx create-expo-app@latestcd expo-social-auth安装其他依赖:
🌐 Install the additional dependencies:
- supabase-js
- @react-native-async-storage/async-storage - React Native 的一个键值存储库。
- expo-secure-store - 提供了一种在设备上本地安全存储键值对的方法。
- expo-splash-screen - 提供了一种以编程方式管理启动屏的方法。
npx expo install @supabase/supabase-js @react-native-async-storage/async-storage expo-secure-store expo-splash-screen现在,创建一个辅助文件来初始化 Supabase 客户端,用于网页和 React Native 平台,使用特定平台的存储适配器:移动端使用Expo SecureStore,网页端使用AsyncStorage 。
🌐 Now, create a helper file to initialize the Supabase client for both web and React Native platforms using platform-specific storage adapters: Expo SecureStore for mobile and AsyncStorage for web.
import AsyncStorage from '@react-native-async-storage/async-storage'import { createClient } from '@supabase/supabase-js'import 'react-native-url-polyfill/auto'const isSSR = typeof window === 'undefined'const ExpoWebSecureStoreAdapter = { getItem: (key: string) => { if (isSSR) return null console.debug('getItem', { key }) return AsyncStorage.getItem(key) }, setItem: (key: string, value: string) => { if (isSSR) return return AsyncStorage.setItem(key, value) }, removeItem: (key: string) => { if (isSSR) return return AsyncStorage.removeItem(key) },}export const supabase = createClient( process.env.EXPO_PUBLIC_SUPABASE_URL ?? '', process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY ?? '', { auth: { storage: ExpoWebSecureStoreAdapter, autoRefreshToken: true, persistSession: true, detectSessionInUrl: false, }, })设置环境变量 #
🌐 Set up environment variables
你需要之前复制的 API URL 和 publishable 密钥 在这里。这些变量在你的 Expo 应用中是安全的,因为 Supabase 在你的数据库上启用了 行级安全。
🌐 You need the API URL and the publishable key copied earlier.
These variables are safe to expose in your Expo app since Supabase has Row Level Security enabled on your database.
创建一个包含这些变量的 .env 文件:
🌐 Create a .env file containing these variables:
EXPO_PUBLIC_SUPABASE_URL=""EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY=""EXPO_PUBLIC_APPLE_AUTH_SERVICE_ID=""EXPO_PUBLIC_APPLE_AUTH_REDIRECT_URI=""EXPO_PUBLIC_GOOGLE_AUTH_WEB_CLIENT_ID=""设置受保护的导航 #
🌐 Set up protected navigation
接下来,你需要保护应用导航,防止未认证的用户访问受保护的路由。使用 Expo SplashScreen 在获取用户资料和验证身份状态时显示加载屏幕。
🌐 Next, you need to protect app navigation to prevent unauthenticated users from accessing protected routes. Use the Expo SplashScreen to display a loading screen while fetching the user profile and verifying authentication status.
创建 AuthContext#
🌐 Create the AuthContext
创建一个 React 上下文 来管理身份验证会话,使其可以从任何组件访问:
🌐 Create a React context to manage the authentication session, making it accessible from any component:
import { createContext, useContext } from 'react'export type AuthData = { claims?: Record<string, any> | null profile?: any | null isLoading: boolean isLoggedIn: boolean}export const AuthContext = createContext<AuthData>({ claims: undefined, profile: undefined, isLoading: true, isLoggedIn: false,})export const useAuthContext = () => useContext(AuthContext)创建 AuthProvider#
🌐 Create the AuthProvider
接下来,创建一个提供者组件来在整个应用中管理认证会话:
🌐 Next, create a provider component to manage the authentication session throughout the app:
import { AuthContext } from '@/hooks/use-auth-context'import { supabase } from '@/lib/supabase'import { PropsWithChildren, useEffect, useState } from 'react'export default function AuthProvider({ children }: PropsWithChildren) { const [claims, setClaims] = useState<Record<string, any> | undefined | null>() const [profile, setProfile] = useState<any>() const [isLoading, setIsLoading] = useState<boolean>(true) // Fetch the claims once, and subscribe to auth state changes useEffect(() => { const fetchClaims = async () => { setIsLoading(true) const { data, error } = await supabase.auth.getClaims() if (error) { console.error('Error fetching claims:', error) } setClaims(data?.claims ?? null) setIsLoading(false) } fetchClaims() const { data: { subscription }, } = supabase.auth.onAuthStateChange(async (_event, _session) => { console.log('Auth state changed:', { event: _event }) const { data } = await supabase.auth.getClaims() setClaims(data?.claims ?? null) }) // Cleanup subscription on unmount return () => { subscription.unsubscribe() } }, []) // Fetch the profile when the claims change useEffect(() => { const fetchProfile = async () => { setIsLoading(true) if (claims) { const { data } = await supabase.from('profiles').select('*').eq('id', claims.sub).single() setProfile(data) } else { setProfile(null) } setIsLoading(false) } fetchProfile() }, [claims]) return ( <AuthContext.Provider value={{ claims, isLoading, profile, isLoggedIn: claims != undefined, }} > {children} </AuthContext.Provider> )}创建 SplashScreenController#
🌐 Create the SplashScreenController
创建一个 SplashScreenController 组件来显示认证会话加载时的 Expo SplashScreen:“}
🌐 Create a SplashScreenController component to display the Expo SplashScreen while the authentication session is loading:
import { useAuthContext } from '@/hooks/use-auth-context'import { SplashScreen } from 'expo-router'SplashScreen.preventAutoHideAsync()export function SplashScreenController() { const { isLoading } = useAuthContext() if (!isLoading) { SplashScreen.hideAsync() } return null}创建一个登出组件 #
🌐 Create a sign-out component
创建一个登出按钮组件来处理用户登出:
🌐 Create a sign-out button component to handle user sign-out:
import { supabase } from '@/lib/supabase'import React from 'react'import { Button } from 'react-native'async function onSignOutButtonPress() { const { error } = await supabase.auth.signOut() if (error) { console.error('Error signing out:', error) }}export default function SignOutButton() { return <Button title="Sign out" onPress={onSignOutButtonPress} />}然后把它添加到用于显示用户资料数据和注销按钮的 app/(tabs)/index.tsx 文件中:
🌐 And add it to the app/(tabs)/index.tsx file used to display the user profile data and the sign-out button:
import { Image } from 'expo-image'import { StyleSheet } from 'react-native'import { HelloWave } from '@/components/hello-wave'import ParallaxScrollView from '@/components/parallax-scroll-view'import { ThemedText } from '@/components/themed-text'import { ThemedView } from '@/components/themed-view'import SignOutButton from '@/components/social-auth-buttons/sign-out-button'import { useAuthContext } from '@/hooks/use-auth-context'export default function HomeScreen() { const { profile } = useAuthContext() return ( <ParallaxScrollView headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }} headerImage={ <Image source={require('@/assets/images/partial-react-logo.png')} style={styles.reactLogo} /> } > <ThemedView style={styles.titleContainer}> <ThemedText type="title">Welcome!</ThemedText> <HelloWave /> </ThemedView> <ThemedView style={styles.stepContainer}> <ThemedText type="subtitle">Username</ThemedText> <ThemedText>{profile?.username}</ThemedText> <ThemedText type="subtitle">Full name</ThemedText> <ThemedText>{profile?.full_name}</ThemedText> </ThemedView> <SignOutButton /> </ParallaxScrollView> )}const styles = StyleSheet.create({ titleContainer: { flexDirection: 'row', alignItems: 'center', gap: 8, }, stepContainer: { gap: 8, marginBottom: 8, }, reactLogo: { height: 178, width: 290, bottom: 0, left: 0, position: 'absolute', },})创建一个登录界面 #
🌐 Create a sign-in screen
接下来,创建一个基本的登录界面组件:
🌐 Next, create a basic sign-in screen component:
import { Link, Stack } from 'expo-router'import { StyleSheet } from 'react-native'import { ThemedText } from '@/components/themed-text'import { ThemedView } from '@/components/themed-view'export default function LoginScreen() { return ( <> <Stack.Screen options={{ title: 'Login' }} /> <ThemedView style={styles.container}> <ThemedText type="title">Login</ThemedText> <Link href="/" style={styles.link}> <ThemedText type="link">Try to navigate to home screen!</ThemedText> </Link> </ThemedView> </> )}const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 20, }, link: { marginTop: 15, paddingVertical: 15, },})实现受保护的路由 #
🌐 Implement protected routes
用 AuthProvider 和 SplashScreenController 封装导航。
🌐 Wrap the navigation with the AuthProvider and SplashScreenController.
使用 Expo Router 的受保护路由,你可以保护导航:
🌐 Using Expo Router's protected routes, you can secure navigation:
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'import { Stack } from 'expo-router'import { StatusBar } from 'expo-status-bar'import 'react-native-reanimated'import { SplashScreenController } from '@/components/splash-screen-controller'import { useAuthContext } from '@/hooks/use-auth-context'import { useColorScheme } from '@/hooks/use-color-scheme'import AuthProvider from '@/providers/auth-provider'// Separate RootNavigator so we can access the AuthContextfunction RootNavigator() { const { isLoggedIn } = useAuthContext() return ( <Stack> <Stack.Protected guard={isLoggedIn}> <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> </Stack.Protected> <Stack.Protected guard={!isLoggedIn}> <Stack.Screen name="login" options={{ headerShown: false }} /> </Stack.Protected> <Stack.Screen name="+not-found" /> </Stack> )}export default function RootLayout() { const colorScheme = useColorScheme() return ( <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> <AuthProvider> <SplashScreenController /> <RootNavigator /> <StatusBar style="auto" /> </AuthProvider> </ThemeProvider> )}你现在可以通过运行以下命令来测试这个应用:
🌐 You can now test the app by running:
npx expo prebuildnpx expo start --clear确认应用按预期工作。启动画面在获取用户资料时显示,即使尝试使用 Link 按钮导航到主页,登录页面仍然会出现。
🌐 Verify that the app works as expected. The splash screen displays while fetching the user profile, and the sign-in page appears even when attempting to navigate to the home screen using the Link button.
默认情况下,Supabase Auth 在为用户创建会话之前要求进行邮箱验证。要支持邮箱验证,你需要实现深度链接处理!
🌐 By default Supabase Auth requires email verification before a session is created for the user. To support email verification you need to implement deep link handling!
在测试时,你可以在你的项目的邮箱认证提供商设置中禁用邮箱确认。
🌐 While testing, you can disable email confirmation in your project's email auth provider settings.
整合社交认证 #
🌐 Integrate social authentication
现在将社交认证与 Supabase Auth 集成,先从 Apple 认证开始。如果你只需要实现 Google 认证,可以直接跳到Google 认证部分。
🌐 Now integrate social authentication with Supabase Auth, starting with Apple authentication. If you only need to implement Google authentication, you can skip to the Google authentication section.
苹果认证 #
🌐 Apple authentication
先在登录界面里加上这个按钮:
🌐 Start by adding the button inside the sign-in screen:
…import AppleSignInButton from '@/components/social-auth-buttons/apple/apple-sign-in-button';…export default function LoginScreen() { return ( <> <Stack.Screen options={{ title: 'Login' }} /> <ThemedView style={styles.container}> … <AppleSignInButton /> … </ThemedView> </> );}…对于苹果认证,你可以选择以下方式:
🌐 For Apple authentication, you can choose between:
- Invertase 的 React Native 苹果认证库 - 支持 iOS 和 Android
- react-apple-signin-auth - 支持网页端,也被 Invertase 推荐
- Expo 的 AppleAuthentication 库 —— 仅支持 iOS
无论选择哪种方式,你都需要从 Apple 开发者控制台 获取一个服务 ID。
🌐 For either option, you need to obtain a Service ID from the Apple Developer Console.
要在 Android 和网络上启用 Apple 注册,你还需要注册通过运行得到的隧道 URL(例如 https://arnrer1-anonymous-8081.exp.direct):
🌐 To enable Apple sign-up on Android and Web, you also need to register the tunnelled URL (e.g., https://arnrer1-anonymous-8081.exp.direct) obtained by running:
npx expo start --tunnel然后把它添加到 你的 Supabase 仪表板认证配置 的 重定向 URL 字段里。
🌐 And add it to the Redirect URLs field in your Supabase dashboard Authentication configuration.
更多信息,请参考 Supabase 使用 Apple 登录 指南。
🌐 For more information, follow the Supabase Sign in with Apple guide.
前提条件
在继续之前,确保你已经按照 Invertase 初始设置指南 和 Invertase 安卓设置指南 中的要求完成了必要的准备工作。
你需要在 .env 文件中添加两个新的环境变量:
EXPO_PUBLIC_APPLE_AUTH_SERVICE_ID="YOUR_APPLE_AUTH_SERVICE_ID"EXPO_PUBLIC_APPLE_AUTH_REDIRECT_URI="YOUR_APPLE_AUTH_REDIRECT_URI"iOS#
安装 @invertase/react-native-apple-authentication 库:
npx expo install @invertase/react-native-apple-authentication然后创建 iOS 特定的按钮组件 AppleSignInButton:
import { supabase } from '@/lib/supabase'import { AppleButton, appleAuth } from '@invertase/react-native-apple-authentication'import type { SignInWithIdTokenCredentials } from '@supabase/supabase-js'import { router } from 'expo-router'import { Platform } from 'react-native'async function onAppleButtonPress() { // Performs login request const appleAuthRequestResponse = await appleAuth.performRequest({ requestedOperation: appleAuth.Operation.LOGIN, // Note: it appears putting FULL_NAME first is important, see issue #293 requestedScopes: [appleAuth.Scope.FULL_NAME, appleAuth.Scope.EMAIL], }) // Get the current authentication state for user // Note: This method must be tested on a real device. On the iOS simulator it always throws an error. const credentialState = await appleAuth.getCredentialStateForUser(appleAuthRequestResponse.user) console.log('Apple sign in successful:', { credentialState, appleAuthRequestResponse }) if ( credentialState === appleAuth.State.AUTHORIZED && appleAuthRequestResponse.identityToken && appleAuthRequestResponse.authorizationCode ) { const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = { provider: 'apple', token: appleAuthRequestResponse.identityToken, nonce: appleAuthRequestResponse.nonce, access_token: appleAuthRequestResponse.authorizationCode, } const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials) if (error) { console.error('Error signing in with Apple:', error) } if (data) { console.log('Apple sign in successful:', data) router.navigate('/(tabs)') } }}export default function AppleSignInButton() { if (Platform.OS !== 'ios') { return <></> } return ( <AppleButton buttonStyle={AppleButton.Style.BLACK} buttonType={AppleButton.Type.SIGN_IN} style={{ width: 160, height: 45 }} onPress={() => onAppleButtonPress()} /> )}要在模拟器上测试功能,去掉 getCredentialStateForUser 检查:
在 iOS 中启用 Apple 验证功能:
{ "expo": { … "ios": { … "usesAppleSignIn": true … }, … }}按照 Expo 文档 的说明,将功能添加到 Info.plist 文件中。
在测试应用之前,如果你已经构建了 iOS 应用,请先清理项目生成的文件:
npx react-native-clean-project clean-project-auto如果问题仍然存在,尝试完全清除缓存,正如许多用户在这个已关闭的问题中报告的那样。
最后,通过安装 Pod 库并运行 Expo prebuild 命令来更新 iOS 项目:
cd iospod installcd ..npx expo prebuild现在在真实设备上测试这个应用吧:
npx expo run:ios --no-build-cache --device你应该能看到带有 Apple 认证按钮的登录界面。
如果你在使用本指南的过程中遇到困难,可以参考GitHub 上的完整 Invertase 示例。
安卓
安装所需的库:
npx expo install @invertase/react-native-apple-authentication react-native-get-random-values uuid接下来,创建 Android 专用的 AppleSignInButton 组件:
import { supabase } from '@/lib/supabase'import { appleAuthAndroid, AppleButton } from '@invertase/react-native-apple-authentication'import { SignInWithIdTokenCredentials } from '@supabase/supabase-js'import { Platform } from 'react-native'import 'react-native-get-random-values'import { v4 as uuid } from 'uuid'async function onAppleButtonPress() { // Generate secure, random values for state and nonce const rawNonce = uuid() const state = uuid() // Configure the request appleAuthAndroid.configure({ // The Service ID you registered with Apple clientId: process.env.EXPO_PUBLIC_APPLE_AUTH_SERVICE_ID ?? '', // Return URL added to your Apple dev console. We intercept this redirect, but it must still match // the URL you provided to Apple. It can be an empty route on your backend as it's never called. redirectUri: process.env.EXPO_PUBLIC_APPLE_AUTH_REDIRECT_URI ?? '', // The type of response requested - code, id_token, or both. responseType: appleAuthAndroid.ResponseType.ALL, // The amount of user information requested from Apple. scope: appleAuthAndroid.Scope.ALL, // Random nonce value that will be SHA256 hashed before sending to Apple. nonce: rawNonce, // Unique state value used to prevent CSRF attacks. A UUID will be generated if nothing is provided. state, }) // Open the browser window for user sign in const credentialState = await appleAuthAndroid.signIn() console.log('Apple sign in successful:', credentialState) if (credentialState.id_token && credentialState.code && credentialState.nonce) { const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = { provider: 'apple', token: credentialState.id_token, nonce: credentialState.nonce, access_token: credentialState.code, } const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials) if (error) { console.error('Error signing in with Apple:', error) } if (data) { console.log('Apple sign in successful:', data) } }}export default function AppleSignInButton() { if (Platform.OS !== 'android' || appleAuthAndroid.isSupported !== true) { return <></> } return ( <AppleButton buttonStyle={AppleButton.Style.BLACK} buttonType={AppleButton.Type.SIGN_IN} onPress={() => onAppleButtonPress()} /> )}你现在应该可以通过在实体设备或模拟器上运行来测试身份验证了:
npx expo run:android --no-build-cacheGoogle 验证 #
🌐 Google authentication
先在登录界面添加按钮:
🌐 Start by adding the button to the sign-in screen:
…import GoogleSignInButton from '@/components/social-auth-buttons/google/google-sign-in-button';…export default function LoginScreen() { return ( <> <Stack.Screen options={{ title: 'Login' }} /> <ThemedView style={styles.container}> … <GoogleSignInButton /> … </ThemedView> </> );}…对于谷歌认证,你可以在以下选项中进行选择:
🌐 For Google authentication, you can choose between the following options:
- GN Google 登录高级版 - 支持 iOS、Android 和网页,使用最新的 Google 一键登录(但 需要订阅)
- @react-oauth/google - 支持网页(所以对移动端来说不是很合适,但它能用)
- 依赖 Supabase Auth 的
signInWithOAuth功能——它也支持 iOS、Android 和网页(还可以用来管理其他任何 OAuth 提供商)
GN Google 免费登录 不支持 iOS 或 Android,因为它不允许在登录请求中传递自定义 nonce。
🌐 The GN Google Sign In Free doesn't support iOS or Android, as it doesn't allow to pass a custom nonce to the sign-in request.
无论选择哪种方式,你都需要从谷歌云引擎获取一个 Web 客户端 ID,具体操作可参考 Google 登录 指南。
🌐 For either option, you need to obtain a Web Client ID from the Google Cloud Engine, as explained in the Google Sign In guide.
本指南只在网页端使用 @react-oauth/google@latest 选项,在移动平台使用 signInWithOAuth 。
🌐 This guide only uses the @react-oauth/google@latest option for the Web, and the signInWithOAuth for the mobile platforms.
在继续之前,先在 .env 文件中添加一个新的环境变量:
🌐 Before proceeding, add a new environment variable to the .env file:
EXPO_PUBLIC_GOOGLE_AUTH_WEB_CLIENT_ID="YOUR_GOOGLE_AUTH_WEB_CLIENT_ID"安装 @react-oauth/google 库:
npx expo install @react-oauth/google在 app.json 中启用 expo-web-browser 插件:
{ "expo": { … "plugins": [ … [ "expo-web-browser", { "experimentalLauncherActivity": false } ] … ], … }}然后创建 iOS 特定的按钮组件 GoogleSignInButton:
import { supabase } from '@/lib/supabase'import { CredentialResponse, GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google'import { SignInWithIdTokenCredentials } from '@supabase/supabase-js'import { useEffect, useState } from 'react'import 'react-native-get-random-values'export default function GoogleSignInButton() { // Generate secure, random values for state and nonce const [nonce, setNonce] = useState('') const [sha256Nonce, setSha256Nonce] = useState('') async function onGoogleButtonSuccess(authRequestResponse: CredentialResponse) { console.debug('Google sign in successful:', { authRequestResponse }) if (authRequestResponse.clientId && authRequestResponse.credential) { const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = { provider: 'google', token: authRequestResponse.credential, nonce: nonce, } const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials) if (error) { console.error('Error signing in with Google:', error) } if (data) { console.log('Google sign in successful:', data) } } } function onGoogleButtonFailure() { console.error('Error signing in with Google') } useEffect(() => { function generateNonce(): string { const array = new Uint32Array(1) window.crypto.getRandomValues(array) return array[0].toString() } async function generateSha256Nonce(nonce: string): Promise<string> { const buffer = await window.crypto.subtle.digest('sha-256', new TextEncoder().encode(nonce)) const array = Array.from(new Uint8Array(buffer)) return array.map((b) => b.toString(16).padStart(2, '0')).join('') } let nonce = generateNonce() setNonce(nonce) generateSha256Nonce(nonce).then((sha256Nonce) => { setSha256Nonce(sha256Nonce) }) }, []) return ( <GoogleOAuthProvider clientId={process.env.EXPO_PUBLIC_GOOGLE_AUTH_WEB_CLIENT_ID ?? ''} nonce={sha256Nonce} > <GoogleLogin nonce={sha256Nonce} onSuccess={onGoogleButtonSuccess} onError={onGoogleButtonFailure} useOneTap={true} auto_select={true} /> </GoogleOAuthProvider> )}使用隧道 HTTPS URL 在你的浏览器中测试身份验证:
npx expo start --tunnel要让 Google 登录正常工作,就像你之前为 Apple 做的那样,你需要将获取的隧道 URL(例如 https://arnrer1-anonymous-8081.exp.direct)注册到你的 Google Cloud 控制台的 OAuth 2.0 客户端 ID 配置中的授权 JavaScript 来源列表里。