Expo 集成

Expo 是一个使用 React Native 构建跨平台应用的流行框架。Better Auth 支持 Expo 原生应用和网页应用。

【Expo is a popular framework for building cross-platform apps with React Native. Better Auth supports both Expo native and web apps.】

安装

【Installation】

配置更好的认证后端

在使用 Better Auth 与 Expo 之前,确保你已经设置了一个 Better Auth 后端。你可以使用独立的服务器,或者利用 Expo 的新 API Routes 功能来托管你的 Better Auth 实例。

要开始使用,请查看我们的安装指南,了解如何在你的服务器上设置 Better Auth。如果你想查看完整示例,可以在这里找到它。

要在 Expo 中使用新的 API 路由功能来托管你的 Better Auth 实例,你可以在你的 Expo 应用中创建一个新的 API 路由,并挂载 Better Auth 处理程序。

app/api/auth/[...auth]+api.ts
import { auth } from "@/lib/auth"; // import Better Auth handler

const handler = auth.handler;
export { handler as GET, handler as POST }; // export handler for both GET and POST requests

Install Server Dependencies

将 Better Auth 包和 Expo 插件安装到你的服务器应用中。

npm install better-auth @better-auth/expo

If you're using Expo's API Routes, you can follow the step below instead.

安装客户端依赖

  • 你还需要将 Better Auth 包和 Expo 插件都安装到你的 Expo 应用中。
npm install better-auth @better-auth/expo
  • 你需要安装 expo-network 来检测网络状态。
npm install expo-network
  • (可选) 如果你使用的是默认的 Expo 模板,这些依赖已经包含在内,所以你可以跳过这一步。否则,如果你打算使用我们的社交提供商(例如 Google、Apple),你的 Expo 应用需要一些额外的依赖。
npm install expo-linking expo-web-browser expo-constants

在你的服务器上添加 Expo 插件

将 Expo 插件添加到你的 Better Auth 服务器。

lib/auth.ts
import { betterAuth } from "better-auth";
import { expo } from "@better-auth/expo";

export const auth = betterAuth({
    plugins: [expo()],
    emailAndPassword: { 
        enabled: true, // Enable authentication using email and password.
      }, 
});

初始化更好的认证客户端

要在你的 Expo 应用中初始化 Better Auth,你需要使用 Better Auth 后端的基本 URL 调用 createAuthClient。确保从 /react 导入客户端。

确保在你的 Expo 应用中安装 expo-secure-store 包。它用于安全地存储会话数据和 cookies。

npm install expo-secure-store

你还需要从 @better-auth/expo/client 导入客户端插件,并在初始化身份验证客户端时将其传入 plugins 数组。

这是重要的,因为:

  • 社交认证支持: 通过在 Expo 网页浏览器中处理授权 URL 和回调,来实现社交认证流程。
  • 安全的 Cookie 管理: 安全地存储 Cookie,并自动将其添加到你的身份验证请求的头部。
lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";
import * as SecureStore from "expo-secure-store";

export const authClient = createAuthClient({
    baseURL: "http://localhost:8081", // Base URL of your Better Auth backend.
    plugins: [
        expoClient({
            scheme: "myapp",
            storagePrefix: "myapp",
            storage: SecureStore,
        })
    ]
});

Be sure to include the full URL, including the path, if you've changed the default path from /api/auth.

方案与可信来源

Better Auth 使用深度链接在认证后将用户重定向回你的应用。要启用此功能,你需要将应用的 scheme 添加到 Better Auth 配置中的 trustedOrigins 列表中。

首先,确保你在 app.json 文件中定义了一个 scheme。

app.json
{
    "expo": {
        "scheme": "myapp"
    }
}

然后,更新你的 Better Auth 配置,将该方案包含在 trustedOrigins 列表中。

auth.ts
export const auth = betterAuth({
    trustedOrigins: ["myapp://"]
})

如果你有多个方案或需要支持包含各种路径的深度链接,你可以使用特定的模式或通配符:

auth.ts
export const auth = betterAuth({
    trustedOrigins: [
        // Basic scheme
        "myapp://", 
        
        // Production & staging schemes
        "myapp-prod://",
        "myapp-staging://",
        
        // Wildcard support for all paths following the scheme
        "myapp://*"
    ]
})

开发模式

在开发过程中,Expo 会使用带有你设备本地 IP 地址的 exp:// 方案。为了支持这一点,你可以使用通配符来匹配常见的本地 IP 范围:

auth.ts
export const auth = betterAuth({
    trustedOrigins: [
        "myapp://",
        
        // Development mode - Expo's exp:// scheme with local IP ranges
        ...(process.env.NODE_ENV === "development" ? [
            "exp://",                      // Trust all Expo URLs (prefix matching)
            "exp://**",                    // Trust all Expo URLs (wildcard matching)
            "exp://192.168.*.*:*/**",      // Trust 192.168.x.x IP range with any port and path
        ] : [])
    ]
})

有关受信任来源的更多信息,请参见此处

exp:// 的通配符模式应仅在开发中使用。在生产环境中,请使用你应用的特定方案(例如,myapp://)。

配置 Metro Bundler

要解决 Better Auth 导出问题,你需要在 metro 配置中启用 unstable_enablePackageExports

metro.config.js
const { getDefaultConfig } = require("expo/metro-config");

const config = getDefaultConfig(__dirname)

config.resolver.unstable_enablePackageExports = true; 

module.exports = config;
如果你的项目中没有 metro.config.js 文件,请运行 npx expo customize metro.config.js

如果你无法启用 unstable_enablePackageExports 选项,你可以使用 babel-plugin-module-resolver 来手动解析路径。

babel.config.js
module.exports = function (api) {
    api.cache(true);
    return {
        presets: ["babel-preset-expo"],
        plugins: [
            [
                "module-resolver",
                {
                    alias: {
                        "better-auth/react": "./node_modules/better-auth/dist/client/react/index.mjs",
                        "better-auth/client/plugins": "./node_modules/better-auth/dist/client/plugins/index.mjs",
                        "@better-auth/expo/client": "./node_modules/@better-auth/expo/dist/client.mjs",
                    },
                },
            ],
        ],
    }
}
如果你的项目中没有 babel.config.js 文件,请运行 npx expo customize babel.config.js

在进行更改后别忘了清除缓存。

npx expo start --clear

用法

【Usage】

用户认证

【Authenticating Users】

初始化 Better Auth 后,你现在可以使用 authClient 在你的 Expo 应用中进行用户认证。

【With Better Auth initialized, you can now use the authClient to authenticate users in your Expo app.】

app/sign-in.tsx
import { useState } from "react"; 
import { View, TextInput, Button } from "react-native";
import { authClient } from "@/lib/auth-client";

export default function SignIn() {
    const [email, setEmail] = useState("");
    const [password, setPassword] = useState("");

    const handleLogin = async () => {
        await authClient.signIn.email({
            email,
            password,
        })
    };

    return (
        <View>
            <TextInput
                placeholder="Email"
                value={email}
                onChangeText={setEmail}
            />
            <TextInput
                placeholder="Password"
                value={password}
                onChangeText={setPassword}
            />
            <Button title="Login" onPress={handleLogin} />
        </View>
    );
}
app/sign-up.tsx
import { useState } from "react";
import { View, TextInput, Button } from "react-native";
import { authClient } from "@/lib/auth-client";

export default function SignUp() {
    const [email, setEmail] = useState("");
    const [name, setName] = useState("");
    const [password, setPassword] = useState("");

    const handleLogin = async () => {
        await authClient.signUp.email({
                email,
                password,
                name
        })
    };

    return (
        <View>
            <TextInput
                placeholder="Name"
                value={name}
                onChangeText={setName}
            />
            <TextInput
                placeholder="Email"
                value={email}
                onChangeText={setEmail}
            />
            <TextInput
                placeholder="Password"
                value={password}
                onChangeText={setPassword}
            />
            <Button title="Login" onPress={handleLogin} />
        </View>
    );
}

社交登录

【Social Sign-In】

对于社交登录,你可以使用 authClient.signIn.social 方法,提供提供商名称和回调 URL。

【For social sign-in, you can use the authClient.signIn.social method with the provider name and a callback URL.】

app/social-sign-in.tsx
import { Button } from "react-native";

export default function SocialSignIn() {
    const handleLogin = async () => {
        await authClient.signIn.social({
            provider: "google",
            callbackURL: "/dashboard" // this will be converted to a deep link (eg. `myapp://dashboard`) on native
        })
    };
    return <Button title="Login with Google" onPress={handleLogin} />;
}

IdToken 登录

【IdToken Sign-In】

如果你想在移动设备上发起提供者请求,然后在服务器上验证 ID 令牌,你可以使用带有 idToken 选项的 authClient.signIn.social 方法。

【If you want to make provider request on the mobile device and then verify the ID token on the server, you can use the authClient.signIn.social method with the idToken option.】

app/social-sign-in.tsx
import { Button } from "react-native";

export default function SocialSignIn() {
    const handleLogin = async () => {
        await authClient.signIn.social({
            provider: "google", // only google, apple and facebook are supported for idToken signIn
            idToken: {
                token: "...", // ID token from provider
                nonce: "...", // nonce from provider (optional)
            }
            callbackURL: "/dashboard" // this will be converted to a deep link (eg. `myapp://dashboard`) on native
        })
    };
    return <Button title="Login with Google" onPress={handleLogin} />;
}

会话

【Session】

Better Auth 提供了一个 useSession 钩子,用于在你的应用中访问当前用户的会话。

【Better Auth provides a useSession hook to access the current user's session in your app.】

app/index.tsx
import { Text } from "react-native";
import { authClient } from "@/lib/auth-client";

export default function Index() {
    const { data: session } = authClient.useSession();

    return <Text>Welcome, {session?.user.name}</Text>;
}

在本地,会话数据将缓存到 SecureStore 中。这将允许在应用重新加载时无需加载旋转图标。你可以通过向客户端传递 disableCache 选项来禁用此行为。

【On native, the session data will be cached in SecureStore. This will allow you to remove the need for a loading spinner when the app is reloaded. You can disable this behavior by passing the disableCache option to the client.】

向你的服务器发起认证请求

【Making Authenticated Requests to Your Server】

要向你的服务器发起需要用户会话的认证请求,你必须从 SecureStore 中获取会话 cookie 并手动将其添加到请求头中。

【To make authenticated requests to your server that require the user's session, you have to retrieve the session cookie from SecureStore and manually add it to your request headers.】

import { authClient } from "@/lib/auth-client";

const makeAuthenticatedRequest = async () => {
  const cookies = authClient.getCookie(); 
  const headers = {
    "Cookie": cookies, 
  };
  const response = await fetch("http://localhost:8081/api/secure-endpoint", { 
    headers,
    // 'include' can interfere with the cookies we just set manually in the headers
    credentials: "omit"
  });
  const data = await response.json();
  return data;
};

示例:TRPC 使用方法

lib/trpc-provider.tsx
//...other imports
import { authClient } from "@/lib/auth-client"; 

export const api = createTRPCReact<AppRouter>();

export function TRPCProvider(props: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    api.createClient({
      links: [
        httpBatchLink({
          //...your other options
          headers() {
            const headers = new Map<string, string>(); 
            const cookies = authClient.getCookie(); 
            if (cookies) { 
              headers.set("Cookie", cookies); 
            } 
            return Object.fromEntries(headers); 
          },
        }),
      ],
    }),
  );

  return (
    <api.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {props.children}
      </QueryClientProvider>
    </api.Provider>
  );
}

选项

【Options】

Expo 客户端

【Expo Client】

存储:用于缓存会话数据和 Cookie 的存储机制。

lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";
import SecureStorage from "expo-secure-store";

const authClient = createAuthClient({
    baseURL: "http://localhost:8081",
    plugins: [
        expoClient({
            storage: SecureStorage,
            // ...
        })
    ],
});

scheme:scheme 用于在用户通过 oAuth 提供商身份验证后,深度链接返回到你的应用。默认情况下,Better Auth 会尝试从 app.json 文件中读取 scheme。如果你需要覆盖此设置,可以将 scheme 选项传递给客户端。

lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";

const authClient = createAuthClient({
    baseURL: "http://localhost:8081",
    plugins: [
        expoClient({
            scheme: "myapp",
            // ...
        }),
    ],
});

disableCache:默认情况下,客户端会将会话数据缓存到 SecureStore 中。你可以通过向客户端传入 disableCache 选项来禁用此行为。

lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";

const authClient = createAuthClient({
    baseURL: "http://localhost:8081",
    plugins: [
        expoClient({
            disableCache: true,
            // ...
        }),
    ],
});

cookiePrefix:服务器 Cookie 名称的前缀,用于识别哪些 Cookie 属于 better-auth。这防止了在设置第三方 Cookie 时无限次重新获取。可以是单个字符串,也可以是字符串数组以匹配多个前缀。默认为“更好认证”。

lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { expoClient } from "@better-auth/expo/client";
import * as SecureStore from "expo-secure-store";

const authClient = createAuthClient({
    baseURL: "http://localhost:8081",
    plugins: [
        expoClient({
            storage: SecureStore,
            // Single prefix
            cookiePrefix: "better-auth"
        })
    ]
});

你还可以提供多个前缀,以匹配来自不同认证系统的 Cookie:

【You can also provide multiple prefixes to match cookies from different authentication systems:】

lib/auth-client.ts
const authClient = createAuthClient({
    baseURL: "http://localhost:8081",
    plugins: [
        expoClient({
            storage: SecureStore,
            // Multiple prefixes
            cookiePrefix: ["better-auth", "my-app", "custom-auth"]
        })
    ]
});

重要提示: 如果你使用像 Passkey 这样的插件,并且使用了自定义的 webAuthnChallengeCookie 选项,请确保在 cookiePrefix 数组中包含 cookie 前缀。例如,如果你设置 webAuthnChallengeCookie: "my-app-passkey",请在你的 cookiePrefix 中包含 "my-app"。更多详情请参阅 Passkey 插件文档

Expo 服务器

【Expo Servers】

服务器插件选项:

【Server plugin options:】

disableOriginOverride:为 Expo API 路由覆盖源(默认值:false)。如果你在使用 Expo API 路由时遇到 CORS 源问题,请启用此选项。