Files
touchbase/src/components/PaymentForm.tsx
2026-05-02 14:05:30 -04:00

102 lines
2.8 KiB
TypeScript

"use client";
// Stripe Payment Element for the booking-deposit flow.
// First real client component in the app — needed because Stripe.js can only
// run in the browser.
import { useState } from "react";
import { loadStripe, type Stripe } from "@stripe/stripe-js";
import {
Elements,
PaymentElement,
useElements,
useStripe,
} from "@stripe/react-stripe-js";
// Cache the Stripe object across renders. loadStripe returns a Promise-like.
let stripePromise: ReturnType<typeof loadStripe> | null = null;
function getStripe(publishableKey: string) {
if (!stripePromise) stripePromise = loadStripe(publishableKey);
return stripePromise;
}
export function PaymentForm({
publishableKey,
clientSecret,
amountCents,
returnUrl,
}: {
publishableKey: string;
clientSecret: string;
amountCents: number;
returnUrl: string;
}) {
return (
<Elements
stripe={getStripe(publishableKey)}
options={{
clientSecret,
appearance: { theme: "stripe" },
}}
>
<CheckoutInner amountCents={amountCents} returnUrl={returnUrl} />
</Elements>
);
}
function CheckoutInner({
amountCents,
returnUrl,
}: {
amountCents: number;
returnUrl: string;
}) {
const stripe = useStripe();
const elements = useElements();
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
if (!stripe || !elements) return;
setSubmitting(true);
setError(null);
const { error: stripeError } = await stripe.confirmPayment({
elements,
confirmParams: { return_url: returnUrl },
});
// confirmPayment only returns here on validation/network errors;
// on success Stripe redirects to return_url.
if (stripeError) {
setError(stripeError.message ?? "Payment failed — try again.");
setSubmitting(false);
}
}
return (
<form onSubmit={onSubmit} className="space-y-4">
<PaymentElement />
{error && (
<div className="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-800 dark:border-red-900 dark:bg-red-950/40 dark:text-red-200">
{error}
</div>
)}
<button
type="submit"
disabled={!stripe || !elements || submitting}
className="w-full rounded-md bg-zinc-900 py-2.5 text-sm font-medium text-white hover:bg-zinc-800 disabled:opacity-50 dark:bg-zinc-100 dark:text-zinc-900"
>
{submitting ? "Processing…" : `Pay $${(amountCents / 100).toFixed(2)} deposit`}
</button>
<p className="text-center text-xs text-zinc-500">
Your card is charged the deposit now. The remaining balance is paid in person.
</p>
</form>
);
}
// Type re-export so the page can declare props without importing @stripe/stripe-js.
export type { Stripe };