From e48a8f4f2eda3934c067414dc690c0980de8f628 Mon Sep 17 00:00:00 2001 From: DIYgod Date: Fri, 3 Jul 2026 17:41:58 +0800 Subject: [PATCH] fix(mobile): use folo callback for social auth --- .../src/lib/auth-cookie-migration.test.ts | 92 +++++++++++++++++++ apps/mobile/src/lib/auth-cookie-migration.ts | 2 +- apps/mobile/src/lib/auth.ts | 2 +- .../src/modules/login/social-login.test.ts | 17 +++- apps/mobile/src/modules/login/social-login.ts | 9 +- apps/mobile/src/modules/login/social.tsx | 4 +- 6 files changed, 119 insertions(+), 7 deletions(-) create mode 100644 apps/mobile/src/lib/auth-cookie-migration.test.ts diff --git a/apps/mobile/src/lib/auth-cookie-migration.test.ts b/apps/mobile/src/lib/auth-cookie-migration.test.ts new file mode 100644 index 000000000..b4603e3fe --- /dev/null +++ b/apps/mobile/src/lib/auth-cookie-migration.test.ts @@ -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 => { + 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" }) + }) +}) diff --git a/apps/mobile/src/lib/auth-cookie-migration.ts b/apps/mobile/src/lib/auth-cookie-migration.ts index 96a6e06b0..cf5841c79 100644 --- a/apps/mobile/src/lib/auth-cookie-migration.ts +++ b/apps/mobile/src/lib/auth-cookie-migration.ts @@ -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", } } diff --git a/apps/mobile/src/lib/auth.ts b/apps/mobile/src/lib/auth.ts index d25b0cead..fb5da25e7 100644 --- a/apps/mobile/src/lib/auth.ts +++ b/apps/mobile/src/lib/auth.ts @@ -54,7 +54,7 @@ const refreshSessionQueries = () => const plugins = [ ...baseAuthPlugins, expoClient({ - scheme: "follow", + scheme: "folo", storagePrefix, storage: { setItem(key: string, value: string) { diff --git a/apps/mobile/src/modules/login/social-login.test.ts b/apps/mobile/src/modules/login/social-login.test.ts index 88c061be4..bd31d975b 100644 --- a/apps/mobile/src/modules/login/social-login.test.ts +++ b/apps/mobile/src/modules/login/social-login.test.ts @@ -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 () => {}) diff --git a/apps/mobile/src/modules/login/social-login.ts b/apps/mobile/src/modules/login/social-login.ts index 7b037d14c..d5cedb1f2 100644 --- a/apps/mobile/src/modules/login/social-login.ts +++ b/apps/mobile/src/modules/login/social-login.ts @@ -1,7 +1,12 @@ +export const nativeOAuthCallbackURL = "folo://" + type LoginWithSocialProviderOptions = { providerId: string setPendingProviderId: (providerId: string | null) => void - signInWithProvider: (providerId: string) => Promise + signInWithProvider: ( + providerId: string, + options: { callbackURL: typeof nativeOAuthCallbackURL }, + ) => Promise signInWithAppleIdentityToken: () => Promise syncSession: () => Promise 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() diff --git a/apps/mobile/src/modules/login/social.tsx b/apps/mobile/src/modules/login/social.tsx index 94dc3c898..4fbba6594 100644 --- a/apps/mobile/src/modules/login/social.tsx +++ b/apps/mobile/src/modules/login/social.tsx @@ -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 () => {