Merge pull request 'feat: BookingForm → Stripe Checkout' (#64) from feat/wire-stripe-checkout into main
All checks were successful
CI / test (push) Successful in 1m57s

This commit is contained in:
tarzzan 2026-06-01 23:35:33 +00:00
commit a575d40163
3 changed files with 51 additions and 2 deletions

View file

@ -12,6 +12,8 @@ import {
import { MediaType, UserRole } from "@/generated/prisma/enums";
import { formatAverageRating } from "@/lib/reviews";
import { isStripeConfigured } from "@/lib/stripe";
import { BookingForm } from "../_components/booking-form";
import { CarbetGallery } from "../_components/carbet-gallery";
import { CarbetMap } from "../_components/carbet-map";
@ -255,6 +257,7 @@ export default async function PublicCarbetPage({ params }: PageProps) {
minStayNights={carbet.minStayNights}
maxStayNights={carbet.maxStayNights}
isAuthenticated={Boolean(viewerId)}
stripeEnabled={isStripeConfigured()}
/>
</aside>
</div>

View file

@ -14,6 +14,7 @@ type Props = {
minStayNights: number | null;
maxStayNights: number | null;
isAuthenticated: boolean;
stripeEnabled: boolean;
};
function todayPlus(n: number): string {
@ -38,6 +39,7 @@ export function BookingForm({
minStayNights,
maxStayNights,
isAuthenticated,
stripeEnabled,
}: Props) {
const router = useRouter();
const [startDate, setStartDate] = useState<string | null>(null);
@ -88,6 +90,34 @@ export function BookingForm({
setBusy(true);
setError(null);
try {
if (stripeEnabled) {
// Checkout Stripe : crée la résa + une session Checkout, redirige le user.
const res = await fetch("/api/stripe/checkout/booking", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
carbetId,
startDate,
endDate,
guestCount,
amount: nights * nightlyPrice,
currency: "EUR",
}),
});
const json = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(json?.error || `Erreur ${res.status}`);
}
if (json.checkoutUrl) {
window.location.assign(json.checkoutUrl);
return;
}
// Fallback si pas d'URL retournée → page de la résa créée.
router.push(`/reservations/${json.bookingId ?? ""}`);
return;
}
// Pas de Stripe configuré → flux direct, résa en PENDING manuel.
const res = await fetch("/api/bookings", {
method: "POST",
headers: { "Content-Type": "application/json" },
@ -187,7 +217,13 @@ export function BookingForm({
disabled={!canSubmit}
className="w-full rounded-md bg-emerald-600 px-4 py-2.5 text-sm font-semibold text-white hover:bg-emerald-700 disabled:opacity-50"
>
{busy ? "Envoi…" : isAuthenticated ? "Réserver" : "Se connecter pour réserver"}
{busy
? "Envoi…"
: !isAuthenticated
? "Se connecter pour réserver"
: stripeEnabled
? "Payer et réserver"
: "Réserver"}
</button>
{!isAuthenticated ? (
@ -200,7 +236,9 @@ export function BookingForm({
) : null}
<p className="text-center text-[11px] text-zinc-500">
Le créneau est bloqué dès l&apos;envoi. Statut « En attente » jusqu&apos;à confirmation du paiement.
{stripeEnabled
? "Vous serez redirigé vers Stripe pour le paiement sécurisé."
: "Le créneau est bloqué dès l'envoi. Statut « En attente » jusqu'à confirmation."}
</p>
</div>
);

View file

@ -1,5 +1,13 @@
import Stripe from "stripe";
/** Détecte si Stripe est utilisable (clé posée + pas un placeholder). */
export function isStripeConfigured(): boolean {
const key = (process.env.STRIPE_SECRET_KEY ?? "").trim();
if (!key) return false;
if (key.includes("REPLACE_ME") || key.includes("PLACEHOLDER")) return false;
return key.startsWith("sk_test_") || key.startsWith("sk_live_") || key.startsWith("rk_");
}
let stripeClient: Stripe | null = null;
export function getStripeClient(): Stripe {