Skip to content
Auth

用 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)登录。

Supabase Social Auth example

项目设置 #

🌐 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

  1. 在 Supabase 仪表板中创建一个新项目
  2. 输入你的项目详情。
  3. 等新数据库上线。

设置数据库模式 #

🌐 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.

  1. 在仪表板中转到SQL 编辑器页面。
  2. 点击 社区 > 快速入门 标签下的 用户管理入门
  3. 点击运行

获取 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.

Project URL
Publishable key

正在构建应用 #

🌐 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:

1
npx create-expo-app@latest
2
3
cd expo-social-auth

安装其他依赖:

🌐 Install the additional dependencies:

1
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.

lib/supabase.web.ts
1
import AsyncStorage from '@react-native-async-storage/async-storage'
2
import { createClient } from '@supabase/supabase-js'
3
import 'react-native-url-polyfill/auto'
4
5
const isSSR = typeof window === 'undefined'
6
7
const ExpoWebSecureStoreAdapter = {
8
getItem: (key: string) => {
9
if (isSSR) return null
10
console.debug('getItem', { key })
11
return AsyncStorage.getItem(key)
12
},
13
setItem: (key: string, value: string) => {
14
if (isSSR) return
15
return AsyncStorage.setItem(key, value)
16
},
17
removeItem: (key: string) => {
18
if (isSSR) return
19
return AsyncStorage.removeItem(key)
20
},
21
}
22
23
export const supabase = createClient(
24
process.env.EXPO_PUBLIC_SUPABASE_URL ?? '',
25
process.env.EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY ?? '',
26
{
27
auth: {
28
storage: ExpoWebSecureStoreAdapter,
29
autoRefreshToken: true,
30
persistSession: true,
31
detectSessionInUrl: false,
32
},
33
}
34
)
View source

设置环境变量 #

🌐 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:

.env
1
EXPO_PUBLIC_SUPABASE_URL=""
2
EXPO_PUBLIC_SUPABASE_PUBLISHABLE_KEY=""
3
EXPO_PUBLIC_APPLE_AUTH_SERVICE_ID=""
4
EXPO_PUBLIC_APPLE_AUTH_REDIRECT_URI=""
5
EXPO_PUBLIC_GOOGLE_AUTH_WEB_CLIENT_ID=""
View source

设置受保护的导航 #

🌐 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:

hooks/use-auth-context.tsx
1
import { createContext, useContext } from 'react'
2
3
export type AuthData = {
4
claims?: Record<string, any> | null
5
profile?: any | null
6
isLoading: boolean
7
isLoggedIn: boolean
8
}
9
10
export const AuthContext = createContext<AuthData>({
11
claims: undefined,
12
profile: undefined,
13
isLoading: true,
14
isLoggedIn: false,
15
})
16
17
export const useAuthContext = () => useContext(AuthContext)
View source

创建 AuthProvider#

🌐 Create the AuthProvider

接下来,创建一个提供者组件来在整个应用中管理认证会话:

🌐 Next, create a provider component to manage the authentication session throughout the app:

providers/auth-provider.tsx
1
import { AuthContext } from '@/hooks/use-auth-context'
2
import { supabase } from '@/lib/supabase'
3
import { PropsWithChildren, useEffect, useState } from 'react'
4
5
export default function AuthProvider({ children }: PropsWithChildren) {
6
const [claims, setClaims] = useState<Record<string, any> | undefined | null>()
7
const [profile, setProfile] = useState<any>()
8
const [isLoading, setIsLoading] = useState<boolean>(true)
9
10
// Fetch the claims once, and subscribe to auth state changes
11
useEffect(() => {
12
const fetchClaims = async () => {
13
setIsLoading(true)
14
15
const { data, error } = await supabase.auth.getClaims()
16
17
if (error) {
18
console.error('Error fetching claims:', error)
19
}
20
21
setClaims(data?.claims ?? null)
22
setIsLoading(false)
23
}
24
25
fetchClaims()
26
27
const {
28
data: { subscription },
29
} = supabase.auth.onAuthStateChange(async (_event, _session) => {
30
console.log('Auth state changed:', { event: _event })
31
const { data } = await supabase.auth.getClaims()
32
setClaims(data?.claims ?? null)
33
})
34
35
// Cleanup subscription on unmount
36
return () => {
37
subscription.unsubscribe()
38
}
39
}, [])
40
41
// Fetch the profile when the claims change
42
useEffect(() => {
43
const fetchProfile = async () => {
44
setIsLoading(true)
45
46
if (claims) {
47
const { data } = await supabase.from('profiles').select('*').eq('id', claims.sub).single()
48
49
setProfile(data)
50
} else {
51
setProfile(null)
52
}
53
54
setIsLoading(false)
55
}
56
57
fetchProfile()
58
}, [claims])
59
60
return (
61
<AuthContext.Provider
62
value={{
63
claims,
64
isLoading,
65
profile,
66
isLoggedIn: claims != undefined,
67
}}
68
>
69
{children}
70
</AuthContext.Provider>
71
)
72
}
View source

创建 SplashScreenController#

🌐 Create the SplashScreenController

创建一个 SplashScreenController 组件来显示认证会话加载时的 Expo SplashScreen:“}

🌐 Create a SplashScreenController component to display the Expo SplashScreen while the authentication session is loading:

components/splash-screen-controller.tsx
1
import { useAuthContext } from '@/hooks/use-auth-context'
2
import { SplashScreen } from 'expo-router'
3
4
SplashScreen.preventAutoHideAsync()
5
6
export function SplashScreenController() {
7
const { isLoading } = useAuthContext()
8
9
if (!isLoading) {
10
SplashScreen.hideAsync()
11
}
12
13
return null
14
}
View source

创建一个登出组件 #

🌐 Create a logout component

创建一个登出按钮组件来处理用户退出:

🌐 Create a logout button component to handle user sign-out:

components/social-auth-buttons/sign-out-button.tsx
1
import { supabase } from '@/lib/supabase'
2
import React from 'react'
3
import { Button } from 'react-native'
4
5
async function onSignOutButtonPress() {
6
const { error } = await supabase.auth.signOut()
7
8
if (error) {
9
console.error('Error signing out:', error)
10
}
11
}
12
13
export default function SignOutButton() {
14
return <Button title="Sign out" onPress={onSignOutButtonPress} />
15
}
View source

然后把它添加到用于显示用户资料数据和注销按钮的 app/(tabs)/index.tsx 文件中:

🌐 And add it to the app/(tabs)/index.tsx file used to display the user profile data and the logout button:

app/(tabs)/index.tsx
1
import { Image } from 'expo-image'
2
import { StyleSheet } from 'react-native'
3
4
import { HelloWave } from '@/components/hello-wave'
5
import ParallaxScrollView from '@/components/parallax-scroll-view'
6
import { ThemedText } from '@/components/themed-text'
7
import { ThemedView } from '@/components/themed-view'
8
import SignOutButton from '@/components/social-auth-buttons/sign-out-button'
9
import { useAuthContext } from '@/hooks/use-auth-context'
10
11
export default function HomeScreen() {
12
const { profile } = useAuthContext()
13
14
return (
15
<ParallaxScrollView
16
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
17
headerImage={
18
<Image
19
source={require('@/assets/images/partial-react-logo.png')}
20
style={styles.reactLogo}
21
/>
22
}
23
>
24
<ThemedView style={styles.titleContainer}>
25
<ThemedText type="title">Welcome!</ThemedText>
26
<HelloWave />
27
</ThemedView>
28
<ThemedView style={styles.stepContainer}>
29
<ThemedText type="subtitle">Username</ThemedText>
30
<ThemedText>{profile?.username}</ThemedText>
31
<ThemedText type="subtitle">Full name</ThemedText>
32
<ThemedText>{profile?.full_name}</ThemedText>
33
</ThemedView>
34
<SignOutButton />
35
</ParallaxScrollView>
36
)
37
}
38
39
const styles = StyleSheet.create({
40
titleContainer: {
41
flexDirection: 'row',
42
alignItems: 'center',
43
gap: 8,
44
},
45
stepContainer: {
46
gap: 8,
47
marginBottom: 8,
48
},
49
reactLogo: {
50
height: 178,
51
width: 290,
52
bottom: 0,
53
left: 0,
54
position: 'absolute',
55
},
56
})
View source

创建一个登录界面 #

🌐 Create a login screen

接下来,创建一个基本的登录界面组件:

🌐 Next, create a basic login screen component:

app/login.tsx
1
import { Link, Stack } from 'expo-router'
2
import { StyleSheet } from 'react-native'
3
4
import { ThemedText } from '@/components/themed-text'
5
import { ThemedView } from '@/components/themed-view'
6
7
export default function LoginScreen() {
8
return (
9
<>
10
<Stack.Screen options={{ title: 'Login' }} />
11
<ThemedView style={styles.container}>
12
<ThemedText type="title">Login</ThemedText>
13
<Link href="/" style={styles.link}>
14
<ThemedText type="link">Try to navigate to home screen!</ThemedText>
15
</Link>
16
</ThemedView>
17
</>
18
)
19
}
20
21
const styles = StyleSheet.create({
22
container: {
23
flex: 1,
24
alignItems: 'center',
25
justifyContent: 'center',
26
padding: 20,
27
},
28
link: {
29
marginTop: 15,
30
paddingVertical: 15,
31
},
32
})
View source

实现受保护的路由 #

🌐 Implement protected routes

AuthProviderSplashScreenController 封装导航。

🌐 Wrap the navigation with the AuthProvider and SplashScreenController.

使用 Expo Router 的受保护路由,你可以保护导航:

🌐 Using Expo Router's protected routes, you can secure navigation:

app/\_layout.tsx
1
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'
2
import { Stack } from 'expo-router'
3
import { StatusBar } from 'expo-status-bar'
4
import 'react-native-reanimated'
5
6
import { SplashScreenController } from '@/components/splash-screen-controller'
7
8
import { useAuthContext } from '@/hooks/use-auth-context'
9
import { useColorScheme } from '@/hooks/use-color-scheme'
10
import AuthProvider from '@/providers/auth-provider'
11
12
// Separate RootNavigator so we can access the AuthContext
13
function RootNavigator() {
14
const { isLoggedIn } = useAuthContext()
15
16
return (
17
<Stack>
18
<Stack.Protected guard={isLoggedIn}>
19
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
20
</Stack.Protected>
21
<Stack.Protected guard={!isLoggedIn}>
22
<Stack.Screen name="login" options={{ headerShown: false }} />
23
</Stack.Protected>
24
<Stack.Screen name="+not-found" />
25
</Stack>
26
)
27
}
28
29
export default function RootLayout() {
30
const colorScheme = useColorScheme()
31
32
return (
33
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
34
<AuthProvider>
35
<SplashScreenController />
36
<RootNavigator />
37
<StatusBar style="auto" />
38
</AuthProvider>
39
</ThemeProvider>
40
)
41
}
View source

你现在可以通过运行以下命令来测试这个应用:

🌐 You can now test the app by running:

1
npx expo prebuild
2
npx expo start --clear

确认应用按预期工作。启动画面在获取用户资料时显示,即使尝试使用 Link 按钮导航到主页,登录页面仍然会出现。

🌐 Verify that the app works as expected. The splash screen displays while fetching the user profile, and the login page appears even when attempting to navigate to the home screen using the Link button.

整合社交认证 #

🌐 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 login screen:

1
2
import AppleSignInButton from '@/components/social-auth-buttons/apple/apple-sign-in-button';
3
4
export default function LoginScreen() {
5
return (
6
<>
7
<Stack.Screen options={{ title: 'Login' }} />
8
<ThemedView style={styles.container}>
9
10
<AppleSignInButton />
11
12
</ThemedView>
13
</>
14
);
15
}
16

对于苹果认证,你可以选择以下方式:

🌐 For Apple authentication, you can choose between:

无论选择哪种方式,你都需要从 Apple 开发者控制台 获取一个服务 ID。

🌐 For either option, you need to obtain a Service ID from the Apple Developer Console.

前提条件

在继续之前,确保你已经按照 Invertase 初始设置指南Invertase 安卓设置指南 中的要求完成了必要的准备工作。

你需要在 .env 文件中添加两个新的环境变量:

1
EXPO_PUBLIC_APPLE_AUTH_SERVICE_ID="YOUR_APPLE_AUTH_SERVICE_ID"
2
EXPO_PUBLIC_APPLE_AUTH_REDIRECT_URI="YOUR_APPLE_AUTH_REDIRECT_URI"

iOS#

安装 @invertase/react-native-apple-authentication 库:

1
npx expo install @invertase/react-native-apple-authentication

然后创建 iOS 特定的按钮组件 AppleSignInButton

components/social-auth-buttons/apple/apple-sign-in-button.ios.tsx
1
import { supabase } from '@/lib/supabase'
2
import { AppleButton, appleAuth } from '@invertase/react-native-apple-authentication'
3
import type { SignInWithIdTokenCredentials } from '@supabase/supabase-js'
4
import { router } from 'expo-router'
5
import { Platform } from 'react-native'
6
7
async function onAppleButtonPress() {
8
// Performs login request
9
const appleAuthRequestResponse = await appleAuth.performRequest({
10
requestedOperation: appleAuth.Operation.LOGIN,
11
// Note: it appears putting FULL_NAME first is important, see issue #293
12
requestedScopes: [appleAuth.Scope.FULL_NAME, appleAuth.Scope.EMAIL],
13
})
14
15
// Get the current authentication state for user
16
// Note: This method must be tested on a real device. On the iOS simulator it always throws an error.
17
const credentialState = await appleAuth.getCredentialStateForUser(appleAuthRequestResponse.user)
18
19
console.log('Apple sign in successful:', { credentialState, appleAuthRequestResponse })
20
21
if (
22
credentialState === appleAuth.State.AUTHORIZED &&
23
appleAuthRequestResponse.identityToken &&
24
appleAuthRequestResponse.authorizationCode
25
) {
26
const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = {
27
provider: 'apple',
28
token: appleAuthRequestResponse.identityToken,
29
nonce: appleAuthRequestResponse.nonce,
30
access_token: appleAuthRequestResponse.authorizationCode,
31
}
32
33
const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials)
34
35
if (error) {
36
console.error('Error signing in with Apple:', error)
37
}
38
39
if (data) {
40
console.log('Apple sign in successful:', data)
41
router.navigate('/(tabs)')
42
}
43
}
44
}
45
46
export default function AppleSignInButton() {
47
if (Platform.OS !== 'ios') {
48
return <></>
49
}
50
51
return (
52
<AppleButton
53
buttonStyle={AppleButton.Style.BLACK}
54
buttonType={AppleButton.Type.SIGN_IN}
55
style={{ width: 160, height: 45 }}
56
onPress={() => onAppleButtonPress()}
57
/>
58
)
59
}
View source

在 iOS 中启用 Apple 验证功能:

1
{
2
"expo": {
3
4
"ios": {
5
6
"usesAppleSignIn": true
7
8
},
9
10
}
11
}

按照 Expo 文档 的说明,将功能添加到 Info.plist 文件中。

最后,通过安装 Pod 库并运行 Expo prebuild 命令来更新 iOS 项目:

1
cd ios
2
pod install
3
cd ..
4
npx expo prebuild

现在在真实设备上测试这个应用吧:

1
npx expo run:ios --no-build-cache --device

你应该能看到带有 Apple 验证按钮的登录界面。

安卓

安装所需的库:

1
npx expo install @invertase/react-native-apple-authentication react-native-get-random-values uuid

接下来,创建 Android 专用的 AppleSignInButton 组件:

components/social-auth-buttons/apple/apple-sign-in-button.android.tsx
1
import { supabase } from '@/lib/supabase'
2
import { appleAuthAndroid, AppleButton } from '@invertase/react-native-apple-authentication'
3
import { SignInWithIdTokenCredentials } from '@supabase/supabase-js'
4
import { Platform } from 'react-native'
5
import 'react-native-get-random-values'
6
import { v4 as uuid } from 'uuid'
7
8
async function onAppleButtonPress() {
9
// Generate secure, random values for state and nonce
10
const rawNonce = uuid()
11
const state = uuid()
12
13
// Configure the request
14
appleAuthAndroid.configure({
15
// The Service ID you registered with Apple
16
clientId: process.env.EXPO_PUBLIC_APPLE_AUTH_SERVICE_ID ?? '',
17
18
// Return URL added to your Apple dev console. We intercept this redirect, but it must still match
19
// the URL you provided to Apple. It can be an empty route on your backend as it's never called.
20
redirectUri: process.env.EXPO_PUBLIC_APPLE_AUTH_REDIRECT_URI ?? '',
21
22
// The type of response requested - code, id_token, or both.
23
responseType: appleAuthAndroid.ResponseType.ALL,
24
25
// The amount of user information requested from Apple.
26
scope: appleAuthAndroid.Scope.ALL,
27
28
// Random nonce value that will be SHA256 hashed before sending to Apple.
29
nonce: rawNonce,
30
31
// Unique state value used to prevent CSRF attacks. A UUID will be generated if nothing is provided.
32
state,
33
})
34
35
// Open the browser window for user sign in
36
const credentialState = await appleAuthAndroid.signIn()
37
console.log('Apple sign in successful:', credentialState)
38
39
if (credentialState.id_token && credentialState.code && credentialState.nonce) {
40
const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = {
41
provider: 'apple',
42
token: credentialState.id_token,
43
nonce: credentialState.nonce,
44
access_token: credentialState.code,
45
}
46
47
const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials)
48
49
if (error) {
50
console.error('Error signing in with Apple:', error)
51
}
52
53
if (data) {
54
console.log('Apple sign in successful:', data)
55
}
56
}
57
}
58
59
export default function AppleSignInButton() {
60
if (Platform.OS !== 'android' || appleAuthAndroid.isSupported !== true) {
61
return <></>
62
}
63
64
return (
65
<AppleButton
66
buttonStyle={AppleButton.Style.BLACK}
67
buttonType={AppleButton.Type.SIGN_IN}
68
onPress={() => onAppleButtonPress()}
69
/>
70
)
71
}
View source

你现在应该可以通过在实体设备或模拟器上运行来测试身份验证了:

1
npx expo run:android --no-build-cache

Google 验证 #

🌐 Google authentication

先在登录界面添加按钮:

🌐 Start by adding the button to the login screen:

1
2
import GoogleSignInButton from '@/components/social-auth-buttons/google/google-sign-in-button';
3
4
export default function LoginScreen() {
5
return (
6
<>
7
<Stack.Screen options={{ title: 'Login' }} />
8
<ThemedView style={styles.container}>
9
10
<GoogleSignInButton />
11
12
</ThemedView>
13
</>
14
);
15
}
16

对于谷歌认证,你可以在以下选项中进行选择:

🌐 For Google authentication, you can choose between the following options:

  • GN Google 登录高级版 - 支持 iOS、Android 和网页,使用最新的 Google 一键登录(但 需要订阅
  • @react-oauth/google - 支持网页(所以对移动端来说不是很合适,但它能用)
  • 依赖 Supabase Auth 的 signInWithOAuth 功能——它也支持 iOS、Android 和网页(还可以用来管理其他任何 OAuth 提供商)

无论选择哪种方式,你都需要从谷歌云引擎获取一个 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:

1
EXPO_PUBLIC_GOOGLE_AUTH_WEB_CLIENT_ID="YOUR_GOOGLE_AUTH_WEB_CLIENT_ID"

安装 @react-oauth/google 库:

1
npx expo install @react-oauth/google

app.json 中启用 expo-web-browser 插件:

1
{
2
"expo": {
3
4
"plugins": [
5
6
[
7
"expo-web-browser",
8
{
9
"experimentalLauncherActivity": false
10
}
11
]
12
13
],
14
15
}
16
}

然后创建 iOS 特定的按钮组件 GoogleSignInButton

components/social-auth-buttons/google/google-sign-in-button.web.tsx
1
import { supabase } from '@/lib/supabase'
2
import { CredentialResponse, GoogleLogin, GoogleOAuthProvider } from '@react-oauth/google'
3
import { SignInWithIdTokenCredentials } from '@supabase/supabase-js'
4
import { useEffect, useState } from 'react'
5
6
import 'react-native-get-random-values'
7
8
export default function GoogleSignInButton() {
9
// Generate secure, random values for state and nonce
10
const [nonce, setNonce] = useState('')
11
const [sha256Nonce, setSha256Nonce] = useState('')
12
13
async function onGoogleButtonSuccess(authRequestResponse: CredentialResponse) {
14
console.debug('Google sign in successful:', { authRequestResponse })
15
if (authRequestResponse.clientId && authRequestResponse.credential) {
16
const signInWithIdTokenCredentials: SignInWithIdTokenCredentials = {
17
provider: 'google',
18
token: authRequestResponse.credential,
19
nonce: nonce,
20
}
21
22
const { data, error } = await supabase.auth.signInWithIdToken(signInWithIdTokenCredentials)
23
24
if (error) {
25
console.error('Error signing in with Google:', error)
26
}
27
28
if (data) {
29
console.log('Google sign in successful:', data)
30
}
31
}
32
}
33
34
function onGoogleButtonFailure() {
35
console.error('Error signing in with Google')
36
}
37
38
useEffect(() => {
39
function generateNonce(): string {
40
const array = new Uint32Array(1)
41
window.crypto.getRandomValues(array)
42
return array[0].toString()
43
}
44
45
async function generateSha256Nonce(nonce: string): Promise<string> {
46
const buffer = await window.crypto.subtle.digest('sha-256', new TextEncoder().encode(nonce))
47
const array = Array.from(new Uint8Array(buffer))
48
return array.map((b) => b.toString(16).padStart(2, '0')).join('')
49
}
50
51
let nonce = generateNonce()
52
setNonce(nonce)
53
54
generateSha256Nonce(nonce).then((sha256Nonce) => {
55
setSha256Nonce(sha256Nonce)
56
})
57
}, [])
58
59
return (
60
<GoogleOAuthProvider
61
clientId={process.env.EXPO_PUBLIC_GOOGLE_AUTH_WEB_CLIENT_ID ?? ''}
62
nonce={sha256Nonce}
63
>
64
<GoogleLogin
65
nonce={sha256Nonce}
66
onSuccess={onGoogleButtonSuccess}
67
onError={onGoogleButtonFailure}
68
useOneTap={true}
69
auto_select={true}
70
/>
71
</GoogleOAuthProvider>
72
)
73
}
View source

使用隧道 HTTPS URL 在你的浏览器中测试身份验证:

1
npx expo start --tunnel