Create flow: session UI + sign out

This commit is contained in:
adilallo
2026-04-06 19:22:50 -06:00
parent 4b14510dde
commit 759f5f1555
47 changed files with 1383 additions and 370 deletions
@@ -14,6 +14,7 @@ const LoginContainer = memo<LoginProps>(
ariaLabel,
ariaLabelledBy,
usePortal = true,
backdropVariant = "blurredYellow",
}) => {
const dialogRef = useRef<HTMLDivElement>(null);
const backdropRef = useRef<HTMLDivElement>(null);
@@ -126,6 +127,7 @@ const LoginContainer = memo<LoginProps>(
backdropRef={backdropRef}
portalReady={portalReady}
usePortal={usePortal}
backdropVariant={backdropVariant}
>
{children}
</LoginView>
@@ -1,3 +1,5 @@
export type LoginBackdropVariant = "solid" | "blurredYellow";
export interface LoginProps {
isOpen: boolean;
onClose: () => void;
@@ -13,6 +15,8 @@ export interface LoginProps {
* without waiting for a portal gate (more reliable across engines).
*/
usePortal?: boolean;
/** `solid` = full-page marketing yellow; `blurredYellow` = blur + translucent yellow over underlying UI */
backdropVariant?: LoginBackdropVariant;
}
export interface LoginViewProps {
@@ -28,4 +32,5 @@ export interface LoginViewProps {
/** False until client mount — avoids SSR/client HTML mismatch for createPortal. */
portalReady: boolean;
usePortal: boolean;
backdropVariant: LoginBackdropVariant;
}
+10 -2
View File
@@ -2,7 +2,14 @@
import { createPortal } from "react-dom";
import ModalHeader from "../../utility/ModalHeader";
import type { LoginViewProps } from "./Login.types";
import type { LoginBackdropVariant, LoginViewProps } from "./Login.types";
const backdropClasses: Record<LoginBackdropVariant, string> = {
solid:
"bg-[var(--color-surface-inverse-brand-primary)]",
blurredYellow:
"bg-[var(--color-surface-inverse-brand-primary)]/85 backdrop-blur-md supports-[backdrop-filter]:bg-[var(--color-surface-inverse-brand-primary)]/75",
};
export function LoginView({
isOpen,
@@ -16,6 +23,7 @@ export function LoginView({
backdropRef,
portalReady,
usePortal,
backdropVariant,
}: LoginViewProps) {
if (!isOpen) return null;
if (usePortal && !portalReady) return null;
@@ -23,7 +31,7 @@ export function LoginView({
const content = (
<div
ref={backdropRef}
className="fixed inset-0 z-[9998] flex flex-col items-center justify-center gap-6 overflow-y-auto bg-[var(--color-surface-inverse-brand-primary)] px-4 py-8"
className={`fixed inset-0 z-[9998] flex flex-col items-center justify-center gap-6 overflow-y-auto px-4 py-8 ${backdropClasses[backdropVariant]}`}
onClick={onClose}
role="presentation"
>
+52 -7
View File
@@ -9,6 +9,7 @@ import TextInput from "../../controls/TextInput";
import ContentLockup from "../../type/ContentLockup";
import { requestMagicLink } from "../../../../lib/create/api";
import { safeInternalPath } from "../../../../lib/safeInternalPath";
import { setTransferPendingFlag } from "../../../create/anonymousDraftStorage";
/** Mail icon for login modal (inline SVG; same pattern as InfoMessageBox ExclamationIconInline). */
function MailIconInline() {
@@ -37,7 +38,18 @@ function MailIconInline() {
const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
export default function LoginForm() {
export type LoginFormVariant = "default" | "saveProgress";
export type LoginFormProps = {
variant?: LoginFormVariant;
/** Overrides URL `next` for `requestMagicLink` (e.g. create-flow exit modal). */
magicLinkNextPath?: string;
};
export default function LoginForm({
variant = "default",
magicLinkNextPath,
}: LoginFormProps) {
const t = useTranslation("pages.login");
const tFooter = useTranslation("footer");
const router = useRouter();
@@ -55,6 +67,8 @@ export default function LoginForm() {
const nextParam = searchParams.get("next");
const errorParam = searchParams.get("error");
const isSaveProgress = variant === "saveProgress";
/** Drop `error` from the URL so URL-driven messages dont linger after a new attempt. */
const stripErrorQuery = useCallback(() => {
if (!searchParams.get("error")) return;
@@ -75,7 +89,8 @@ export default function LoginForm() {
}
setSubmitting(true);
try {
const nextPath = safeInternalPath(nextParam);
const rawNext = magicLinkNextPath ?? nextParam;
const nextPath = safeInternalPath(rawNext);
const result = await requestMagicLink(trimmed, nextPath);
if (result.ok === false) {
if (result.retryAfterMs != null && result.retryAfterMs > 0) {
@@ -88,6 +103,9 @@ export default function LoginForm() {
}
return;
}
if (isSaveProgress) {
setTransferPendingFlag();
}
setEmail(trimmed);
setSent(true);
} catch {
@@ -95,7 +113,14 @@ export default function LoginForm() {
} finally {
setSubmitting(false);
}
}, [email, nextParam, stripErrorQuery, t]);
}, [
email,
isSaveProgress,
magicLinkNextPath,
nextParam,
stripErrorQuery,
t,
]);
const urlErrorMessage =
errorParam === "expired_link"
@@ -106,16 +131,36 @@ export default function LoginForm() {
: t("errors.invalidLink")
: "";
const titleId = "login-modal-heading";
return (
<div className="flex flex-col gap-6 pt-2">
<div className="flex flex-col gap-3">
<div className="relative flex h-12 w-12 shrink-0 items-center justify-center rounded-full bg-[var(--color-surface-inverse-brand-primary)]">
<div
className={`relative flex h-12 w-12 shrink-0 items-center justify-center rounded-full ${
isSaveProgress
? "bg-[#fefcc9]"
: "bg-[var(--color-surface-inverse-brand-primary)]"
}`}
>
<MailIconInline />
</div>
<ContentLockup
titleId="login-modal-heading"
title={sent ? t("successTitle") : t("title")}
description={sent ? t("successBody") : t("subtitle")}
titleId={titleId}
title={
sent
? t("successTitle")
: isSaveProgress
? t("saveProgressTitle")
: t("title")
}
description={
sent
? t("successBody")
: isSaveProgress
? t("saveProgressSubtitle")
: t("subtitle")
}
variant="login"
alignment="left"
/>