Setzt Teilprojekte 1-5 der Spec 2026-10-16 um: - Urkunden-Engine (lib/certificate): ein Layout fuer PDF, Vorschau und 3D-Textur, Vorlage classic@1, eingebettete OFL-Schriften, Testmatrix - Einmal-Patenschaft ueber Traegerprodukte (Kategorie PATENSCHAFT), Paket wird im Checkout in Patenschaft (Regelsatz) und Honig aufgeteilt, Kontopflicht, rein digitale Bestellungen (FulfillmentMethod DIGITAL) - Ausstellung nach Zahlung idempotent, Wiederholung im App-Prozess (instrumentation + background-jobs), Ablage in CERTIFICATE_DIR - Verkaufsseite /patenschaft mit Konfigurator und Live-Vorschau, Warenkorb mit Schluessel je Position - Kundenkonto: 3D-Ansicht von Urkunde und Medaille, Downloads, ZIP, Uebertragung per befristetem Einmal-Link, Treuestufen bis VIP - Admin: Angebote, Patenschaften, Druck, Korrektur mit Revisionen, Abos, Erntefenster, Deckung, SEPA-Frist, Treuestufen - Abo ueber Stripe Billing: Lieferung erst nach bezahlten Monaten, SEPA-Neukunden-Frist und Rueckbuchungsschutz, Honigreservierung, Kuendigungsbutton, Monatsrechnungen, Stripe-Einrichtungsskript Startangebote werden inaktiv angelegt; Livebetrieb erst nach Freigabe. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
61 lines
2.5 KiB
TypeScript
61 lines
2.5 KiB
TypeScript
import type { Metadata } from "next";
|
|
import { Container, LinkButton, SectionHeading } from "@/components/ui";
|
|
import { noIndexPrivate } from "@/lib/seo";
|
|
import { getCurrentCustomer } from "@/lib/auth";
|
|
import { resolveSubscriptionReturn } from "@/lib/sponsorship-subscription/checkout-return";
|
|
import { getLocale } from "@/i18n/locale";
|
|
import { getAboStrings } from "@/i18n/sponsorship/abo";
|
|
|
|
// Fragt bei jedem Aufruf den aktuellen Stand bei Stripe ab — nie aus einem Cache.
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export async function generateMetadata(): Promise<Metadata> {
|
|
return { title: getAboStrings(await getLocale()).thanks.metaTitle, robots: noIndexPrivate };
|
|
}
|
|
|
|
type Props = { searchParams: Promise<Record<string, string | string[] | undefined>> };
|
|
|
|
/**
|
|
* Rueckkehrseite nach dem Abo-Checkout (return_url in checkout.ts). Ueber
|
|
* "abgeschlossen" entscheidet die serverseitige Nachfrage bei Stripe, nicht
|
|
* die URL; angelegt wird hier nichts — Abo, Zahlung und Urkunde entstehen
|
|
* ausschliesslich ueber den Webhook.
|
|
*/
|
|
export default async function AboThanksPage({ searchParams }: Props) {
|
|
const t = getAboStrings(await getLocale()).thanks;
|
|
const { session_id: sessionId } = await searchParams;
|
|
const customer = await getCurrentCustomer();
|
|
const result =
|
|
typeof sessionId === "string" && customer ? await resolveSubscriptionReturn(sessionId, customer.id) : { kind: "notCompleted" as const };
|
|
|
|
const view =
|
|
result.kind === "active"
|
|
? { title: t.activeTitle, text: t.activeText }
|
|
: result.kind === "processing"
|
|
? { title: t.processingTitle, text: t.processingText }
|
|
: { title: t.notCompletedTitle, text: t.notCompletedText };
|
|
|
|
return (
|
|
<div className="py-24">
|
|
<Container className="max-w-xl text-center">
|
|
<SectionHeading title={view.title} align="center" level="h1" className="mx-auto" />
|
|
<p role="status" className="mt-6 text-ink/70">
|
|
{view.text}
|
|
</p>
|
|
<div className="mt-10 flex flex-wrap items-center justify-center gap-4">
|
|
{result.kind === "notCompleted" ? (
|
|
<LinkButton href="/patenschaft">{t.backToOffer}</LinkButton>
|
|
) : (
|
|
<>
|
|
<LinkButton href={`/konto/patenschaft/abo/${result.subscriptionId}`}>{t.toSubscription}</LinkButton>
|
|
<LinkButton href="/konto/patenschaft" variant="secondary">
|
|
{t.toAccount}
|
|
</LinkButton>
|
|
</>
|
|
)}
|
|
</div>
|
|
</Container>
|
|
</div>
|
|
);
|
|
}
|