diff --git a/README.md b/README.md index 25f033e..a77d973 100644 --- a/README.md +++ b/README.md @@ -287,7 +287,21 @@ without leaving the page. server-rendered root layout) specifically so `app/layout.tsx` — otherwise static/ISR-cacheable — doesn't get forced into per-request dynamic rendering just to know one icon's href; briefly shows the logged-out - state on first paint until that fetch resolves. + state on first paint until that fetch resolves. The icon itself also + gets a small brand-colored underline while logged in — same visual + language as the desktop nav links' active-state indicator — since the + icon alone doesn't otherwise signal session state at a glance. +- **Checkout registration collisions**: if the email typed into Card 1 + during inline registration already belongs to an existing account, + Payload's create call fails — `registerCustomer()` in `customerAuth.ts` + detects this specifically (`emailExists: true` on the returned + `AuthResult`, inferred from the field flagged in Payload's validation + error, since Payload's own message text doesn't distinguish "duplicate" + from other email-field failures) rather than just surfacing a generic + error. `CheckoutContent.tsx`'s `handleSubmit` reacts by switching + straight to the login toggle with that email pre-filled and + scroll-into-view, instead of leaving the customer stuck with an error + and no obvious next step. - **`/konto/profil`** edits name + the one saved default address (deliberately a single address, not a full address book — see the assistant's memory note on optionally expanding this later), changes the password, shows diff --git a/app/checkout/components/CheckoutContent.tsx b/app/checkout/components/CheckoutContent.tsx index 350af7d..4ae1352 100644 --- a/app/checkout/components/CheckoutContent.tsx +++ b/app/checkout/components/CheckoutContent.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useRef, useState } from "react"; import Link from "next/link"; import Image from "next/image"; import { useRouter } from "next/navigation"; @@ -67,6 +67,7 @@ export function CheckoutContent({ const [purchaseError, setPurchaseError] = useState(null); const [purchasing, setPurchasing] = useState(false); const [showLogin, setShowLogin] = useState(false); + const accountGateRef = useRef(null); const [loginEmail, setLoginEmail] = useState(""); const [loginPassword, setLoginPassword] = useState(""); const [loginError, setLoginError] = useState(null); @@ -156,6 +157,16 @@ export function CheckoutContent({ }); const data = await res.json(); if (!data.ok) { + // The email typed into Card 1 already belongs to an existing + // account — registering was never going to work here. Switch + // straight to the login toggle with that email pre-filled instead + // of just showing an error with no clear next step; the customer + // only needs to add their password and resubmit. + if (data.emailExists) { + setLoginEmail(body.email); + setShowLogin(true); + accountGateRef.current?.scrollIntoView({ behavior: "smooth", block: "center" }); + } setPurchaseError(data.reason || "Die Bestellung konnte nicht abgeschlossen werden."); setPurchasing(false); return; @@ -234,7 +245,10 @@ export function CheckoutContent({ {/* Account gate — an account is required to buy, so this either confirms the active session or offers "already a customer?" login inline (registration itself happens as part of the main - form submit below, via the password field in Card 1). */} + form submit below, via the password field in Card 1). Ref used + to scroll this into view if a checkout submit turns out to + collide with an existing account (see handleSubmit). */} +
{customerEmail ? (

Eingeloggt als {customerEmail}{" "} @@ -278,6 +292,7 @@ export function CheckoutContent({ {loginError &&

{loginError}

}
)} +
diff --git a/app/components/Navbar.tsx b/app/components/Navbar.tsx index b1ff2b4..352d077 100644 --- a/app/components/Navbar.tsx +++ b/app/components/Navbar.tsx @@ -107,13 +107,18 @@ function AccountLink({ variant }: { variant: "icon" | "mobile" }) { return ( + {/* Only real "am I logged in?" signal on the site outside /konto + itself — same brand-colored underline language as the desktop + nav links' active-state indicator, so it reads as consistent + rather than a new visual idiom. */} + {loggedIn && } ); } diff --git a/app/lib/customerAuth.ts b/app/lib/customerAuth.ts index 72419cb..7f5abb1 100644 --- a/app/lib/customerAuth.ts +++ b/app/lib/customerAuth.ts @@ -39,7 +39,9 @@ export type CustomerSummary = { emailVerified: boolean; }; -export type AuthResult = { ok: true; token: string; customer: CustomerSummary } | { ok: false; reason: string }; +export type AuthResult = + | { ok: true; token: string; customer: CustomerSummary } + | { ok: false; reason: string; emailExists?: boolean }; export async function registerCustomer(input: { firstName: string; @@ -58,7 +60,23 @@ export async function registerCustomer(input: { if (!res.ok) { const data = await res.json().catch(() => null); const message: string | undefined = data?.errors?.[0]?.message; - return { ok: false, reason: message ?? "Diese E-Mail-Adresse ist bereits registriert." }; + // Payload's own message for a duplicate unique field is a generic + // "The following field is invalid: email" — not distinguishable from + // any other email-field validation failure by content alone, but at + // this point the client's own type="email" + required already ruled + // out a malformed/missing address, so "email" being the flagged field + // here in practice only ever means one thing: this address is already + // registered. emailExists lets the checkout UI react to that + // specifically (switch to the login toggle) instead of just showing + // an error the customer has no clear next step for. + const emailExists = Boolean(message?.toLowerCase().includes("email")); + return { + ok: false, + reason: emailExists + ? "Diese E-Mail-Adresse ist bereits registriert. Bitte logge dich stattdessen ein." + : (message ?? "Registrierung ist gerade nicht möglich."), + emailExists, + }; } return loginCustomer(input);