158 lines
5.0 KiB
TypeScript
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>
|
|
);
|
|
}
|