add Stripe
This commit is contained in:
101
src/components/PaymentForm.tsx
Normal file
101
src/components/PaymentForm.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
"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 };
|
||||
Reference in New Issue
Block a user