The token was only ever consumed inside Welcome.tsx, which App renders ONLY when there is no session. Opening the emailed link in a browser that already held a Siftlode session posted nothing, the fragment was then stripped, and the user got no signal at all -- the link simply did nothing. Incognito worked, which is why it survived. Confirmation now happens in App, above that branch: signed out the outcome still drives the auth card's banners, signed in it drives a top-of-app bar. The entry URL is snapshotted once at import time (lib/entryUrl) instead of per component, so no future URL writer can strip a token before whoever needs it has read it -- that ordering race is what broke this in the first place. POST /auth/verify now names the address it confirmed, which matters when the browser holds a session for a different account; the same reasoning as password_reset_required already covers the disclosure. Also here, from the 2026-09-02 review tail: the dead GET /auth/verify goes (both senders emit the fragment form, and the last query-token link expired 24h after the 0.78.7 deploy), a spent #verify=/#reset= fragment is cleared while signed in, and the auth-card banners get role=status. The verifyOkReset copy no longer blames admin approval, which E9.5 S1 made false.
42 lines
1.6 KiB
TypeScript
42 lines
1.6 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import { CheckCircle2, AlertTriangle } from "lucide-react";
|
|
import Banner from "./Banner";
|
|
import type { VerifyEntry } from "../lib/useVerifyEntry";
|
|
|
|
// The top-of-app outcome of an emailed verification link opened in a browser that ALREADY holds a
|
|
// session. That case used to be silent: the token was only ever consumed inside Welcome.tsx, which a
|
|
// signed-in visit never reaches, so the link opened a tab, the fragment was stripped, and nothing
|
|
// happened. Signed OUT, the same outcome is rendered by the auth card's own banners instead — this
|
|
// bar is only for the signed-in half.
|
|
//
|
|
// No "send a new link" action here, unlike the auth card: this viewer is signed in, so the sign-in
|
|
// screen (where resending lives) is not where they are, and the message is a report, not a task. A
|
|
// wrong-account case is why the address is named.
|
|
export default function VerifyBanner({ entry }: { entry: VerifyEntry }) {
|
|
const { t } = useTranslation();
|
|
if (!entry.outcome) return null;
|
|
|
|
const ok = entry.outcome !== "invalid";
|
|
const text =
|
|
entry.outcome === "invalid"
|
|
? t("header.verify.invalid")
|
|
: entry.email
|
|
? t("header.verify.confirmedNamed", { email: entry.email })
|
|
: t("header.verify.confirmed");
|
|
|
|
return (
|
|
<Banner
|
|
tone={ok ? "accent" : "warning"}
|
|
icon={ok ? CheckCircle2 : AlertTriangle}
|
|
role="status"
|
|
onDismiss={entry.dismiss}
|
|
dismissTitle={t("header.banner.dismiss")}
|
|
>
|
|
{text}
|
|
{entry.outcome === "ok_reset" && (
|
|
<span className="text-muted"> {t("header.verify.reset")}</span>
|
|
)}
|
|
</Banner>
|
|
);
|
|
}
|