Files
Shifted/app/storefront/[accountId]/pay/page.tsx
2026-02-10 01:14:19 +00:00

158 lines
5.0 KiB
TypeScript

"use client";
import { useEffect, useMemo, useState } from "react";
import { useSearchParams } from "next/navigation";
import { Elements, LinkAuthenticationElement, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js";
import { stripePromise } from "../../../../lib/stripeJs";
type Product = {
id: string;
name: string;
description?: string | null;
default_price?: {
id: string;
unit_amount: number | null;
currency: string;
} | null;
};
function CheckoutForm({ accountId, product }: { accountId: string; product: Product }) {
const stripe = useStripe();
const elements = useElements();
const [message, setMessage] = useState("");
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!stripe || !elements) return;
setLoading(true);
setMessage("");
const result = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/success`,
},
});
if (result.error) {
setMessage(result.error.message || "Payment failed.");
}
setLoading(false);
}
return (
<form className="payment-form" onSubmit={handleSubmit}>
<h2>{product.name}</h2>
<p className="payment-muted">{product.description || "No description provided."}</p>
<div className="payment-price">
{product.default_price?.unit_amount
? `${(product.default_price.unit_amount / 100).toFixed(2)} ${product.default_price.currency.toUpperCase()}`
: "No price"}
</div>
<div className="payment-section">
<LinkAuthenticationElement />
</div>
<div className="payment-section">
<PaymentElement />
</div>
<button className="btn" type="submit" disabled={!stripe || loading}>
{loading ? "Processing..." : "Pay now"}
</button>
{message ? <div className="connect-message">{message}</div> : null}
</form>
);
}
export default function StorefrontPayPage({ params }: { params: { accountId: string } }) {
const { accountId: slug } = params;
const searchParams = useSearchParams();
const productId = searchParams.get("productId");
const [product, setProduct] = useState<Product | null>(null);
const [clientSecret, setClientSecret] = useState("");
const [message, setMessage] = useState("");
const [accountId, setAccountId] = useState("");
const elementsOptions = useMemo(() => {
if (!clientSecret) return null;
return { clientSecret };
}, [clientSecret]);
useEffect(() => {
if (!productId) {
setMessage("Missing productId in URL.");
return;
}
if (slug) {
window.localStorage.setItem("connectStoreSlug", slug);
}
async function load() {
setMessage("");
try {
const lookup = await fetch(`/api/connect/account/lookup?slug=${slug}`);
const lookupData = await lookup.json();
if (!lookup.ok) throw new Error(lookupData.error || "Store not found");
setAccountId(lookupData.accountId);
const prodRes = await fetch(
`/api/connect/products/get?accountId=${lookupData.accountId}&productId=${productId}`
);
const prodData = await prodRes.json();
if (!prodRes.ok) throw new Error(prodData.error || "Failed to load product");
setProduct(prodData.product);
const priceId = prodData.product?.default_price?.id;
if (!priceId) throw new Error("Product does not have a default price.");
const intentRes = await fetch("/api/connect/payment-intent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ accountId: lookupData.accountId, priceId }),
});
const intentData = await intentRes.json();
if (!intentRes.ok) throw new Error(intentData.error || "Failed to create payment intent");
setClientSecret(intentData.clientSecret);
} catch (err: any) {
setMessage(err.message || "Failed to initialize checkout.");
}
}
load();
}, [slug, productId]);
if (!stripePromise) {
return (
<main className="storefront-page">
<section className="section">
<div className="container storefront-page__inner">
<div className="connect-message">
Missing `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY`.
</div>
</div>
</section>
</main>
);
}
return (
<main className="storefront-page">
<section className="section">
<div className="container storefront-page__inner">
<h1 className="page-title">Card Checkout</h1>
<p className="storefront-muted">Store: {slug}</p>
{message ? <div className="connect-message">{message}</div> : null}
{product && elementsOptions ? (
<Elements stripe={stripePromise} options={elementsOptions}>
<CheckoutForm accountId={accountId} product={product} />
</Elements>
) : null}
</div>
</section>
</main>
);
}