fix(mobile): use folo callback for social auth

This commit is contained in:
DIYgod 2026-07-03 17:41:58 +08:00
parent dfcfb0ddff
commit e48a8f4f2e
6 changed files with 119 additions and 7 deletions

View File

@ -0,0 +1,92 @@
import { afterEach, describe, expect, it, vi } from "vitest"
const mocks = vi.hoisted(() => ({
getCookie: vi.fn(() => "better-auth.session_token=session"),
getInstallerPackageName: vi.fn(async () => "com.android.vending"),
getUserAgent: vi.fn(async () => "Folo/0.5.5"),
oneTimeTokenApply: vi.fn(async () => {}),
}))
vi.mock("@follow/utils/headers", () => ({
createMobileAPIHeaders: vi.fn(() => ({
"x-app-version": "0.5.5",
})),
}))
vi.mock("expo-application", () => ({
nativeApplicationVersion: "0.5.5",
}))
vi.mock("react-native", () => ({
Platform: {
OS: "android",
isPad: false,
},
}))
vi.mock("react-native-device-info", () => ({
default: {
getInstallerPackageName: mocks.getInstallerPackageName,
},
}))
vi.mock("./auth", () => ({
getCookie: mocks.getCookie,
oneTimeToken: {
apply: mocks.oneTimeTokenApply,
},
}))
vi.mock("./client-session", () => ({
getClientId: () => "client-id",
getSessionId: () => "session-id",
}))
vi.mock("./native/user-agent", () => ({
getUserAgent: mocks.getUserAgent,
}))
vi.mock("./proxy-env", () => ({
proxyEnv: {
API_URL: "https://api.folo.is",
},
}))
const { migrateLegacyApiSession } = await import("./auth-cookie-migration")
describe("migrateLegacyApiSession", () => {
afterEach(() => {
vi.unstubAllGlobals()
vi.clearAllMocks()
})
it("uses the Folo native app scheme as the migration auth origin", async () => {
const fetchMock = vi.fn(async (input: string, _options?: RequestInit): Promise<Response> => {
if (input === "https://api.folo.is/better-auth/get-session") {
return new Response(null, { status: 401 })
}
if (input === "https://api.follow.is/better-auth/get-session") {
return Response.json({ user: { id: "user-id" } })
}
if (input === "https://api.follow.is/better-auth/one-time-token/generate") {
return Response.json({ token: "one-time-token" })
}
return new Response(null, { status: 404 })
})
vi.stubGlobal("fetch", fetchMock)
await migrateLegacyApiSession()
expect(fetchMock).toHaveBeenCalledTimes(3)
for (const [, options] of fetchMock.mock.calls) {
expect(options?.headers).toMatchObject({
"expo-origin": "folo://",
})
}
expect(mocks.oneTimeTokenApply).toHaveBeenCalledWith({ token: "one-time-token" })
})
})

View File

@ -46,7 +46,7 @@ const createMigrationHeaders = async () => {
"X-Client-Id": getClientId(),
"X-Session-Id": getSessionId(),
"User-Agent": await getUserAgent(),
"expo-origin": "follow://",
"expo-origin": "folo://",
"x-skip-oauth-proxy": "true",
}
}

View File

@ -54,7 +54,7 @@ const refreshSessionQueries = () =>
const plugins = [
...baseAuthPlugins,
expoClient({
scheme: "follow",
scheme: "folo",
storagePrefix,
storage: {
setItem(key: string, value: string) {

View File

@ -32,7 +32,7 @@ describe("loginWithSocialProvider", () => {
})
expect(result).toBe(true)
expect(signInWithProvider).toHaveBeenCalledWith("google")
expect(signInWithProvider).toHaveBeenCalledWith("google", { callbackURL: "folo://" })
expect(trackLogin).toHaveBeenCalledTimes(1)
expect(sequence).toEqual(["pending:google", "sign-in:google", "sync", "track", "pending:none"])
})
@ -58,6 +58,21 @@ describe("loginWithSocialProvider", () => {
expect(setPendingProviderId).toHaveBeenLastCalledWith(null)
})
it("uses the Folo native app scheme as the OAuth callback URL", async () => {
const signInWithProvider = vi.fn(async () => {})
await loginWithSocialProvider({
providerId: "github",
setPendingProviderId: vi.fn(),
signInWithProvider,
signInWithAppleIdentityToken: vi.fn(async () => {}),
syncSession: async () => false,
trackLogin: vi.fn(),
})
expect(signInWithProvider).toHaveBeenCalledWith("github", { callbackURL: "folo://" })
})
it("uses the Apple token flow for Apple sign in", async () => {
const signInWithAppleIdentityToken = vi.fn(async () => {})
const signInWithProvider = vi.fn(async () => {})

View File

@ -1,7 +1,12 @@
export const nativeOAuthCallbackURL = "folo://"
type LoginWithSocialProviderOptions = {
providerId: string
setPendingProviderId: (providerId: string | null) => void
signInWithProvider: (providerId: string) => Promise<void>
signInWithProvider: (
providerId: string,
options: { callbackURL: typeof nativeOAuthCallbackURL },
) => Promise<void>
signInWithAppleIdentityToken: () => Promise<void>
syncSession: () => Promise<boolean>
trackLogin: () => void
@ -23,7 +28,7 @@ export async function loginWithSocialProvider({
if (providerId === "apple") {
await signInWithAppleIdentityToken()
} else {
await signInWithProvider(providerId)
await signInWithProvider(providerId, { callbackURL: nativeOAuthCallbackURL })
}
const hasSession = await syncSession()

View File

@ -78,10 +78,10 @@ export function SocialLogin({ onPressEmail }: { isRegister: boolean; onPressEmai
void loginWithSocialProvider({
providerId: provider.id,
setPendingProviderId,
signInWithProvider: async (providerId) => {
signInWithProvider: async (providerId, { callbackURL }) => {
await signIn.social({
provider: providerId as SocialProviderSignInInput["provider"],
callbackURL: "/",
callbackURL,
})
},
signInWithAppleIdentityToken: async () => {