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
12 changes: 10 additions & 2 deletions apps/web/src/app/mobile-auth/MobileAuthClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import CircularProgress from "@mui/material/CircularProgress";
import Typography from "@mui/material/Typography";
import { useSession } from "@openmapx/core";
import { useTranslations } from "next-intl";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { AuthDialog } from "@/components/auth/AuthDialog";
import { useEnv } from "@/integration-api/runtime/EnvProvider";

Expand Down Expand Up @@ -49,6 +49,14 @@ export function MobileAuthClient({
const { data: session, isPending } = useSession();
const [phase, setPhase] = useState<Phase>("idle");
const authenticated = Boolean(session);
// better-auth reports every refetch of an empty session as pending, and the
// password step triggers one just before it asks for a two-factor code.
// Hiding the dialog then would remount it on its password form, so only the
// first session check keeps it hidden.
const [sessionChecked, setSessionChecked] = useState(!isPending);
useEffect(() => {
if (!isPending) setSessionChecked(true);
}, [isPending]);
// Signing in happens here, in the real browser, where OAuth providers and
// platform passkeys work. Every sign-in route returns to this exact handoff
// so the app's state and challenge survive a provider round trip.
Expand Down Expand Up @@ -112,7 +120,7 @@ export function MobileAuthClient({
</Typography>
<Typography sx={{ mb: 3, color: "text.secondary" }}>{purposeText}</Typography>

{!authenticated && !isPending && (
{!authenticated && sessionChecked && (
<>
<Alert severity="info" sx={{ mb: 3, textAlign: "left" }}>
{t("signInFirst")}
Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/app/mobile-auth/MobileAuthPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,37 @@ describe("MobileAuthClient", () => {
expect(view.queryByTestId("auth-dialog")).toBeNull();
});

it("keeps the sign-in dialog mounted while a signed-out session refetches", () => {
// better-auth reports a refetch of an empty session as pending. The password
// step triggers one before it asks for the two-factor code, so unmounting
// here would reset the dialog to its password form.
sessionState.data = null;
const view = renderClient();
const dialog = view.getByTestId("auth-dialog");

sessionState.isPending = true;
view.rerender(
<NextIntlClientProvider locale="en" messages={en} timeZone="Europe/Berlin">
<MobileAuthClient
purpose="sign-in"
state={STATE}
codeChallenge={CHALLENGE}
callbackScheme="openmapx"
/>
</NextIntlClientProvider>,
);

expect(view.getByTestId("auth-dialog")).toBe(dialog);
});

it("shows no sign-in dialog before the first session check finishes", () => {
sessionState.data = null;
sessionState.isPending = true;
const view = renderClient();

expect(view.queryByTestId("auth-dialog")).toBeNull();
});

it("issues nothing when the server refuses", async () => {
stubIssue({ ok: false });
const view = renderClient();
Expand Down
Loading