diff --git a/apps/mobile/src/lib/auth.ts b/apps/mobile/src/lib/auth.ts index 4d2e4389a..39b1ac5b4 100644 --- a/apps/mobile/src/lib/auth.ts +++ b/apps/mobile/src/lib/auth.ts @@ -1,11 +1,10 @@ -import { expoClient, getSetCookie, hasBetterAuthCookies } from "@better-auth/expo/client" +import { expoClient } from "@better-auth/expo/client" import { baseAuthPlugins } from "@follow/shared/auth" import { isNewUserQueryKey } from "@follow/store/user/constants" import { whoamiQueryKey } from "@follow/store/user/hooks" import { createMobileAPIHeaders } from "@follow/utils/headers" import { useQuery } from "@tanstack/react-query" import { createAuthClient } from "better-auth/react" -import { fetch as expoFetch } from "expo/fetch" import { nativeApplicationVersion } from "expo-application" import * as FileSystem from "expo-file-system/legacy" import Storage from "expo-sqlite/kv-store" @@ -73,48 +72,10 @@ const plugins = [ }), ] -const updateCookieStorage = (serializedCookie: string) => { - try { - safeSecureStore.setItem(cookieKey, serializedCookie) - } catch (error) { - console.warn("SecureStore.setItem failed during auth cookie persistence:", error) - return false - } - - const env = getEnvProfile() - try { - safeSecureStore.setItem(`${cookieKey}_${env}`, serializedCookie) - } catch { - // Keychain may be unavailable in background - } - - bumpAuthStateRevision() - queryClient.invalidateQueries({ queryKey: whoamiQueryKey }) - queryClient.invalidateQueries({ queryKey: isNewUserQueryKey }) - return true -} - -export const persistAuthCookieHeader = (setCookie: string | null | undefined) => { - if (!setCookie || !hasBetterAuthCookies(setCookie, "better-auth")) { - return false - } - - let previousCookie: string | undefined - try { - previousCookie = safeSecureStore.getItem(cookieKey) ?? undefined - } catch (error) { - console.warn("SecureStore.getItem failed during auth cookie persistence:", error) - } - - const serializedCookie = getSetCookie(setCookie, previousCookie) - return updateCookieStorage(serializedCookie) -} - export const authClient = createAuthClient({ baseURL: `${proxyEnv.API_URL}/better-auth`, fetchOptions: { cache: "no-store", - customFetchImpl: async (input, init) => expoFetch(input.toString(), init as any) as any, // Learn more: https://better-fetch.vercel.app/docs/hooks onRequest: async (ctx) => { const headers = createMobileAPIHeaders({ diff --git a/apps/mobile/src/modules/login/email.tsx b/apps/mobile/src/modules/login/email.tsx index 2a6f56762..2529412cd 100644 --- a/apps/mobile/src/modules/login/email.tsx +++ b/apps/mobile/src/modules/login/email.tsx @@ -1,4 +1,3 @@ -import { userSyncService } from "@follow/store/user/store" import { tracker } from "@follow/tracker" import { zodResolver } from "@hookform/resolvers/zod" import { useMutation } from "@tanstack/react-query" @@ -15,7 +14,7 @@ import { z } from "zod" import { SubmitButton } from "@/src/components/common/SubmitButton" import { PlainTextField } from "@/src/components/ui/form/TextField" import { Text } from "@/src/components/ui/typography/Text" -import { authClient, persistAuthCookieHeader } from "@/src/lib/auth" +import { signIn, signUp } from "@/src/lib/auth" import { useNavigation } from "@/src/lib/navigation/hooks" import { Navigation } from "@/src/lib/navigation/Navigation" import { toast } from "@/src/lib/toast" @@ -30,134 +29,6 @@ const formSchema = z.object({ }) type FormValue = z.infer -const getAuthErrorMessage = (value: unknown) => { - if (!value || typeof value !== "object" || !("error" in value)) { - return - } - - const { error } = value - if (!error || typeof error !== "object" || !("message" in error)) { - return - } - - return typeof error.message === "string" ? error.message : undefined -} - -const getAuthData = (value: unknown) => { - if (!value || typeof value !== "object" || !("data" in value)) { - return - } - - return value.data && typeof value.data === "object" ? value.data : undefined -} - -const getResponseSetCookie = (response: Response) => { - const directValue = - response.headers.get("x-better-auth-set-cookie") ?? - response.headers.get("set-cookie") ?? - response.headers.get("Set-Cookie") - if (directValue) { - return directValue - } - - const rawHeaders = (response as Response & { _rawHeaders?: unknown })._rawHeaders - if (!Array.isArray(rawHeaders)) { - return null - } - - const values = rawHeaders - .filter( - (header): header is [string, string] => - Array.isArray(header) && - header.length >= 2 && - typeof header[0] === "string" && - typeof header[1] === "string", - ) - .filter(([key]) => key.toLowerCase() === "set-cookie") - .map(([, value]) => value) - - return values.length > 0 ? values.join(", ") : null -} - -const hasTwoFactorRedirect = (value: unknown) => { - const data = getAuthData(value) - if (data && "twoFactorRedirect" in data) { - return Boolean(data.twoFactorRedirect) - } - - if (!value || typeof value !== "object" || !("response" in value)) { - return false - } - - const { response } = value - if (!response || typeof response !== "object" || !("twoFactorRedirect" in response)) { - return false - } - - return Boolean(response.twoFactorRedirect) -} - -const requestCredentialAuth = async ({ - path, - body, -}: { - path: "/sign-in/email" | "/sign-up/email" - body: Record -}) => { - let setCookie: string | null = null - - const result = await authClient.$fetch(path, { - method: "POST", - body, - headers: await getTokenHeaders(), - throw: false, - onResponse(context) { - setCookie = getResponseSetCookie(context.response) - }, - }) - - const persistedCookie = setCookie ? persistAuthCookieHeader(setCookie) : false - - return { - result, - persistedCookie, - } -} - -const establishCredentialSession = async ({ - email, - password, - onTwoFactorRedirect, -}: { - email: string - password: string - onTwoFactorRedirect?: () => void -}) => { - const { result, persistedCookie } = await requestCredentialAuth({ - path: "/sign-in/email", - body: { - email, - password, - }, - }) - - const errorMessage = getAuthErrorMessage(result) - if (errorMessage) { - throw new Error(errorMessage) - } - - if (hasTwoFactorRedirect(result)) { - onTwoFactorRedirect?.() - return null - } - - const session = persistedCookie ? await userSyncService.whoami().catch(() => null) : null - if (!session?.user?.id) { - return null - } - - return session -} async function onSubmit(values: FormValue) { const result = formSchema.safeParse(values) if (!result.success) { @@ -166,29 +37,37 @@ async function onSubmit(values: FormValue) { return false } - let session = null try { - session = await establishCredentialSession({ - email: result.data.email, - password: result.data.password, - onTwoFactorRedirect: () => { - Navigation.rootNavigation.presentControllerView(TwoFactorAuthScreen) + const res = await signIn.email( + { + email: result.data.email, + password: result.data.password, }, - }) + { + headers: await getTokenHeaders(), + }, + ) + + if (res.error) { + throw new Error(res.error.message) + } + + // @ts-expect-error better-auth response type omits twoFactorRedirect + if (res.data?.twoFactorRedirect) { + Navigation.rootNavigation.presentControllerView(TwoFactorAuthScreen) + return false + } } catch (error) { Alert.alert(error instanceof Error ? error.message : "Unable to sign in") return false } - if (!session?.user?.id) { - return false - } - tracker.userLogin({ type: "email", }) return true } + export function EmailLogin() { const { t } = useTranslation() const [emailValue, setEmailValue] = useState("") @@ -314,44 +193,28 @@ export function EmailSignUp() { }) const submitMutation = useMutation({ mutationFn: async (values: SignupFormValue) => { - try { - const { result, persistedCookie } = await requestCredentialAuth({ - path: "/sign-up/email", - body: { + await signUp + .email( + { email: values.email, password: values.password, name: values.email.split("@")[0] ?? "", }, + { + headers: await getTokenHeaders(), + }, + ) + .then((res) => { + if (res.error?.message) { + toast.error(res.error.message) + } else { + toast.success(i18next.t("login.sign_up_successful")) + tracker.register({ + type: "email", + }) + Navigation.rootNavigation.back() + } }) - - const errorMessage = getAuthErrorMessage(result) - if (errorMessage) { - toast.error(errorMessage) - return false - } - - let session = persistedCookie ? await userSyncService.whoami().catch(() => null) : null - if (!session?.user?.id) { - session = await establishCredentialSession({ - email: values.email, - password: values.password, - }) - } - - if (!session?.user?.id) { - toast.error("Unable to establish session after sign up") - return false - } - - toast.success(i18next.t("login.sign_up_successful")) - tracker.register({ - type: "email", - }) - return true - } catch (error) { - toast.error(error instanceof Error ? error.message : "Unable to sign up") - return false - } }, }) const signup = handleSubmit((values) => {