Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion src/hooks/__tests__/useQuantumAuth.test.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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(<AuthHarness />);

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",
Expand Down
6 changes: 6 additions & 0 deletions src/hooks/useQuantumAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function useQuantumAuth() {
const [authStatus, setAuthStatus] = useState<AuthStatus>("checking");
const [authNotice, setAuthNotice] = useState("");
const operationRef = useRef(0);
const authPromptActiveRef = useRef(false);

const restoreStoredSession = useCallback(async () => {
const operationId = nextAuthOperation(operationRef);
Expand Down Expand Up @@ -73,6 +74,8 @@ export function useQuantumAuth() {

useEffect(() => {
const subscription = AppState.addEventListener("change", (nextState) => {
if (authPromptActiveRef.current) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore after canceled OAuth prompts

When sign-in is started while the initial restoreStoredSession() is still pending (the app opens login for any status other than authenticated) and the user cancels or dismisses the browser, this early return also suppresses the active transition that used to reload SecureStore. The cancel path then falls back to the stale session captured before restore, often null, leaving an existing valid stored session shown as guest until another app-state cycle or restart.

Useful? React with 👍 / 👎.


if (nextState === "active") {
void restoreStoredSession();
return;
Expand Down Expand Up @@ -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) {
Expand All @@ -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));
Expand Down
Loading