← back to Govarbitrage
apps/mobile/lib/auth.ts
84 lines
/**
* Sign in with Apple (native iOS). Uses expo-apple-authentication to obtain an
* Apple identity token, exchanges it at the backend's /api/auth/apple for our
* own app session JWT, and stores that in expo-secure-store. Optional: the rest
* of the app works signed-out; this just creates/links a lightweight account.
*/
import * as AppleAuthentication from "expo-apple-authentication";
import {
loadSettings,
saveAppleAccount,
clearAppleAccount,
type AppleAccount,
} from "./settings";
/** True only on iOS devices/simulators that support Sign in with Apple. */
export async function isAppleSignInAvailable(): Promise<boolean> {
try {
return await AppleAuthentication.isAvailableAsync();
} catch {
return false;
}
}
export async function signInWithApple(): Promise<AppleAccount> {
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
const { identityToken, fullName, email } = credential;
if (!identityToken) {
throw new Error("Apple did not return an identity token.");
}
// Apple sends name/email only on the FIRST authorization — pass them through
// so the backend can populate the account on create.
const name =
fullName && (fullName.givenName || fullName.familyName)
? [fullName.givenName, fullName.familyName].filter(Boolean).join(" ")
: null;
const { baseUrl } = await loadSettings();
const res = await fetch(`${baseUrl.replace(/\/$/, "")}/api/auth/apple`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({
identityToken,
fullName: name || undefined,
email: email || undefined,
}),
});
if (!res.ok) {
let msg = `Sign-in failed (${res.status}).`;
try {
const body = await res.json();
if (body?.error) msg = body.error;
} catch {
// keep the status-code message
}
throw new Error(msg);
}
const data = (await res.json()) as {
token: string;
user?: { name?: string | null; email?: string | null };
};
if (!data?.token) throw new Error("Server did not return a session token.");
const account: AppleAccount = {
token: data.token,
name: data.user?.name ?? name,
email: data.user?.email ?? email ?? null,
};
await saveAppleAccount(account);
return account;
}
export async function signOut(): Promise<void> {
await clearAppleAccount();
}