248 lines
8.8 KiB
TypeScript
248 lines
8.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, useRef } from "react";
|
|
import Link from "next/link";
|
|
import { useCart } from "@/components/cart/CartContext";
|
|
import type { MedusaOrder } from "@/lib/medusa";
|
|
|
|
function formatPrice(amount: number, currencyCode: string) {
|
|
return new Intl.NumberFormat("en-US", {
|
|
style: "currency",
|
|
currency: currencyCode,
|
|
minimumFractionDigits: 2,
|
|
}).format(amount);
|
|
}
|
|
|
|
interface DownloadItem {
|
|
product_title: string;
|
|
variant_title: string;
|
|
sku: string;
|
|
download_url: string;
|
|
}
|
|
|
|
interface UpcomingItem {
|
|
product_title: string;
|
|
variant_title: string;
|
|
sku: string;
|
|
release_date: string;
|
|
}
|
|
|
|
export default function CheckoutReturnPage() {
|
|
const { cart, resetCart } = useCart();
|
|
const [order, setOrder] = useState<MedusaOrder | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [downloads, setDownloads] = useState<DownloadItem[]>([]);
|
|
const [upcoming, setUpcoming] = useState<UpcomingItem[]>([]);
|
|
const attempted = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (attempted.current) return;
|
|
if (!cart?.id) {
|
|
// Cart context still loading — wait
|
|
return;
|
|
}
|
|
attempted.current = true;
|
|
|
|
async function completeOrder() {
|
|
try {
|
|
const res = await fetch("/api/checkout/complete", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ cartId: cart!.id }),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const body = await res.json().catch(() => ({}));
|
|
throw new Error(body.error ?? "Failed to complete order");
|
|
}
|
|
|
|
const result = await res.json();
|
|
|
|
if (result.type === "order" && result.order) {
|
|
setOrder(result.order);
|
|
resetCart();
|
|
|
|
// Fetch download links for digital items
|
|
try {
|
|
const dlRes = await fetch(
|
|
`/api/checkout/downloads?orderId=${result.order.id}`,
|
|
);
|
|
if (dlRes.ok) {
|
|
const dlData = await dlRes.json();
|
|
if (dlData.downloads?.length) setDownloads(dlData.downloads);
|
|
if (dlData.upcoming?.length) setUpcoming(dlData.upcoming);
|
|
}
|
|
} catch {
|
|
// Download links are non-critical — don't block the page
|
|
}
|
|
} else {
|
|
throw new Error("Payment was not completed. Please try again.");
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Something went wrong");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
completeOrder();
|
|
}, [cart?.id, resetCart]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<main className="flex min-h-[60vh] items-center justify-center">
|
|
<p className="text-lightsec dark:text-darksec">Completing your order…</p>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<main className="mx-auto max-w-lg px-6 py-16 text-center">
|
|
<h1 className="font-argesta text-3xl">Something went wrong</h1>
|
|
<p className="mt-4 text-lightsec dark:text-darksec">{error}</p>
|
|
<div className="mt-8 flex justify-center gap-4">
|
|
<Link
|
|
href="/checkout"
|
|
className="rounded-xl border border-lightline px-6 py-3 text-sm transition-all duration-200 hover:border-lightline-hover hover:text-trptkblue dark:border-darkline dark:hover:border-darkline-hover dark:hover:text-white"
|
|
>
|
|
Try Again
|
|
</Link>
|
|
<Link
|
|
href="/"
|
|
className="rounded-xl bg-trptkblue px-6 py-3 text-sm font-silkasb text-white shadow-lg transition-all duration-200 hover:opacity-90 dark:bg-white dark:text-lighttext"
|
|
>
|
|
Go Home
|
|
</Link>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
if (!order) return null;
|
|
|
|
const currencyCode = order.currency_code ?? "eur";
|
|
|
|
return (
|
|
<main className="mx-auto max-w-lg px-6 py-16">
|
|
<div className="text-center">
|
|
<h1 className="font-argesta text-3xl">Thank you!</h1>
|
|
<p className="mt-2 text-lightsec dark:text-darksec">
|
|
Your order has been confirmed.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="mt-10 rounded-2xl border border-lightline p-6 dark:border-darkline">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm text-lightsec dark:text-darksec">Order number</span>
|
|
<span className="font-silkasb">#{order.display_id}</span>
|
|
</div>
|
|
|
|
{order.email && (
|
|
<div className="mt-2 flex items-center justify-between">
|
|
<span className="text-sm text-lightsec dark:text-darksec">Confirmation sent to</span>
|
|
<span className="text-sm">{order.email}</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-6 space-y-2 border-t border-lightline pt-4 text-sm dark:border-darkline">
|
|
<div className="flex justify-between">
|
|
<span className="text-lightsec dark:text-darksec">Subtotal</span>
|
|
<span>{formatPrice(order.subtotal, currencyCode)}</span>
|
|
</div>
|
|
{order.shipping_total > 0 && (
|
|
<div className="flex justify-between">
|
|
<span className="text-lightsec dark:text-darksec">Shipping</span>
|
|
<span>{formatPrice(order.shipping_total, currencyCode)}</span>
|
|
</div>
|
|
)}
|
|
{order.tax_total > 0 && (
|
|
<div className="flex justify-between">
|
|
<span className="text-lightsec dark:text-darksec">Tax</span>
|
|
<span>{formatPrice(order.tax_total, currencyCode)}</span>
|
|
</div>
|
|
)}
|
|
<div className="flex justify-between border-t border-lightline pt-2 font-silkasb dark:border-darkline">
|
|
<span>Total</span>
|
|
<span>{formatPrice(order.total, currencyCode)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Download links for digital purchases */}
|
|
{downloads.length > 0 && (
|
|
<div className="mt-6 rounded-2xl border border-lightline p-6 dark:border-darkline">
|
|
<h2 className="font-silkasb text-sm uppercase tracking-wider">
|
|
Your Downloads
|
|
</h2>
|
|
<div className="mt-4 space-y-3">
|
|
{downloads.map((dl) => (
|
|
<div key={dl.sku} className="flex items-center justify-between gap-4">
|
|
<div className="min-w-0">
|
|
<p className="truncate text-sm font-silkasb">
|
|
{dl.product_title}
|
|
</p>
|
|
<p className="truncate text-xs text-lightsec dark:text-darksec">
|
|
{dl.variant_title}
|
|
</p>
|
|
</div>
|
|
<a
|
|
href={dl.download_url}
|
|
className="shrink-0 rounded-lg bg-trptkblue px-4 py-2 text-xs font-silkasb text-white shadow-lg transition-all duration-200 hover:opacity-90 dark:bg-white dark:text-lighttext"
|
|
>
|
|
Download
|
|
</a>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<p className="mt-4 text-xs text-lightsec dark:text-darksec">
|
|
Download links expire after 5 minutes. A copy has been sent to your email.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Pre-order items not yet available */}
|
|
{upcoming.length > 0 && (
|
|
<div className="mt-6 rounded-2xl border border-lightline p-6 dark:border-darkline">
|
|
<h2 className="font-silkasb text-sm uppercase tracking-wider">
|
|
Upcoming Releases
|
|
</h2>
|
|
<div className="mt-4 space-y-3">
|
|
{upcoming.map((item) => (
|
|
<div key={item.sku} className="flex items-center justify-between gap-4">
|
|
<div className="min-w-0">
|
|
<p className="truncate text-sm font-silkasb">
|
|
{item.product_title}
|
|
</p>
|
|
<p className="truncate text-xs text-lightsec dark:text-darksec">
|
|
{item.variant_title}
|
|
</p>
|
|
</div>
|
|
<span className="shrink-0 text-xs text-lightsec dark:text-darksec">
|
|
Available {new Date(item.release_date).toLocaleDateString("en-GB", {
|
|
day: "numeric",
|
|
month: "short",
|
|
year: "numeric",
|
|
})}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<p className="mt-4 text-xs text-lightsec dark:text-darksec">
|
|
You'll receive download links by email when these releases become available.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-8 text-center">
|
|
<Link
|
|
href="/releases"
|
|
className="inline-block rounded-xl bg-trptkblue px-6 py-3 font-silkasb text-sm text-white shadow-lg transition-all duration-200 hover:opacity-90 dark:bg-white dark:text-lighttext"
|
|
>
|
|
Continue Shopping
|
|
</Link>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|