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));