Files
Wabenhain/src/app/vertrag-widerrufen/page.tsx
T
Nick SzaunigandClaude Opus 5.5 dcfad4e011 feat(legal): electronic withdrawal function and remaining compliance fixes
Withdrawal function (§ 356a BGB, mandatory since 19 June 2026):
- /vertrag-widerrufen: two-step flow "Vertrag widerrufen" ->
  "Widerruf bestätigen", no login, only name, order number and e-mail
  required. Prominent footer button on every page, prefilled link per
  order in the customer account.
- /api/widerruf stores every declaration (matched to the order only if
  order number and e-mail fit), mails a receipt with the declaration and
  the exact time of receipt, notifies ADMIN_EMAIL. Same response for
  matching and non-matching orders (no enumeration), origin check and
  rate limit instead of a CAPTCHA that could block a statutory right.
  User input is HTML-escaped and stripped of line breaks in headers.
- /admin/widerrufe lists declarations, open ones first, with an
  open-count badge and "mark as done".
- Withdrawal notice gains the statutory sentence on the online function.

Other fixes:
- Newsletter stores confirmedAt as proof of consent (Art. 7(1) GDPR).
- "Recently viewed" is only persisted with consent (§ 25(1) TDDDG);
  choosing "essential only" or withdrawing consent deletes it. The
  banner no longer promises audience-measurement cookies that do not
  exist.
- Admin rejects an invalid company e-mail; an obfuscated stored value
  falls back to the configured address so the legal notice stays valid.
- Privacy policy: Microsoft 365 as mail processor, log retention of at
  most 14 days, withdrawal-function processing, consent timestamps.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 11:14:02 +02:00

36 lines
1.5 KiB
TypeScript

import type { Metadata } from "next";
import { Container, SectionHeading, FormPanel } from "@/components/ui";
import { WithdrawalForm } from "@/components/withdrawal-form";
import { getLocale } from "@/i18n/locale";
import { getDictionary } from "@/i18n/get-dictionary";
export const metadata: Metadata = { title: "Vertrag widerrufen", alternates: { canonical: "/vertrag-widerrufen" } };
/** Nur Zeichen, die in Bestellnummern vorkommen — alles andere wird nicht vorausgefuellt. */
const ORDER_NUMBER_PREFILL = /^[A-Za-z0-9-]{1,40}$/;
/**
* Elektronische Widerrufsfunktion (§ 356a BGB). Ohne Anmeldung erreichbar —
* auch Gastbestellungen muessen hier widerrufen werden koennen. Aus dem
* Kundenkonto kommt man mit ?bestellung=… und vorausgefuellter Nummer.
*/
export default async function VertragWiderrufenPage(props: PageProps<"/vertrag-widerrufen">) {
const t = getDictionary(await getLocale());
const { bestellung } = await props.searchParams;
const prefill = typeof bestellung === "string" && ORDER_NUMBER_PREFILL.test(bestellung) ? bestellung : "";
return (
<div className="py-20">
<Container className="max-w-xl">
<FormPanel>
<SectionHeading title={t.withdrawalPage.title} level="h1" />
<p className="mt-6 text-sm leading-relaxed text-ink/75">{t.withdrawalPage.intro}</p>
<div className="mt-8">
<WithdrawalForm defaultOrderNumber={prefill} />
</div>
</FormPanel>
</Container>
</div>
);
}