diff --git a/src/hooks/__tests__/useQuantumAuth.test.tsx b/src/hooks/__tests__/useQuantumAuth.test.tsx
index a738bcb..ec1d9a7 100644
--- a/src/hooks/__tests__/useQuantumAuth.test.tsx
+++ b/src/hooks/__tests__/useQuantumAuth.test.tsx
@@ -1,5 +1,5 @@
import { fireEvent, render, waitFor } from "@testing-library/react-native";
-import { Pressable, Text, View } from "react-native";
+import { AppState, Pressable, Text, View } from "react-native";
import {
clearStoredAuthSession,
@@ -95,6 +95,52 @@ describe("useQuantumAuth", () => {
);
});
+ it("keeps the OAuth operation alive while the auth browser backgrounds the app", async () => {
+ const signedInSession: StoredAuthSession = {
+ accessToken: "access-token",
+ expiresAt: Math.floor(Date.now() / 1000) + 3600,
+ idToken: "id-token",
+ issuedAt: Math.floor(Date.now() / 1000),
+ refreshToken: "refresh-token",
+ user: {
+ email: "user@chefuinc.com",
+ uid: "user-1",
+ },
+ };
+ let appStateListener: ((state: string) => void) | undefined;
+ jest
+ .spyOn(AppState, "addEventListener")
+ .mockImplementation((_event, listener) => {
+ appStateListener = listener as (state: string) => void;
+ return { remove: jest.fn() } as never;
+ });
+ let finishSignIn: ((session: StoredAuthSession) => void) | undefined;
+ jest.mocked(startQuantumSignIn).mockImplementation(
+ () =>
+ new Promise(resolve => {
+ finishSignIn = resolve;
+ }),
+ );
+
+ const view = await render();
+
+ await waitFor(() =>
+ expect(view.getByTestId("status").props.children).toBe("guest"),
+ );
+ fireEvent.press(view.getByText("Sign in"));
+ appStateListener?.("background");
+ finishSignIn?.(signedInSession);
+
+ await waitFor(() =>
+ expect(view.getByTestId("status").props.children).toBe("authenticated"),
+ );
+ expect(saveStoredAuthSession).toHaveBeenCalledWith(signedInSession);
+ expect(view.getByTestId("notice").props.children).toBe(
+ "Signed in to CheFu Account.",
+ );
+ view.unmount();
+ });
+
it("clears expired sessions and prompts the user to sign in again", async () => {
const expiredSession: StoredAuthSession = {
accessToken: "expired-access",
diff --git a/src/hooks/useQuantumAuth.ts b/src/hooks/useQuantumAuth.ts
index a56aa14..6b02a0b 100644
--- a/src/hooks/useQuantumAuth.ts
+++ b/src/hooks/useQuantumAuth.ts
@@ -30,6 +30,7 @@ export function useQuantumAuth() {
const [authStatus, setAuthStatus] = useState("checking");
const [authNotice, setAuthNotice] = useState("");
const operationRef = useRef(0);
+ const authPromptActiveRef = useRef(false);
const restoreStoredSession = useCallback(async () => {
const operationId = nextAuthOperation(operationRef);
@@ -73,6 +74,8 @@ export function useQuantumAuth() {
useEffect(() => {
const subscription = AppState.addEventListener("change", (nextState) => {
+ if (authPromptActiveRef.current) return;
+
if (nextState === "active") {
void restoreStoredSession();
return;
@@ -124,7 +127,9 @@ export function useQuantumAuth() {
setAuthStatus("checking");
try {
+ authPromptActiveRef.current = true;
const nextSession = await startQuantumSignIn();
+ authPromptActiveRef.current = false;
if (!isCurrentAuthOperation(operationRef, operationId)) return;
if (!nextSession) {
@@ -137,6 +142,7 @@ export function useQuantumAuth() {
setAuthStatus("authenticated");
setAuthNotice("Signed in to CheFu Account.");
} catch (error) {
+ authPromptActiveRef.current = false;
if (!isCurrentAuthOperation(operationRef, operationId)) return;
setAuthStatus(session ? "authenticated" : "guest");
setAuthNotice(authErrorMessage(error));