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
All checks were successful
CI / test (push) Successful in 1m57s
This commit is contained in:
commit
a575d40163
3 changed files with 51 additions and 2 deletions
|
|
@ -12,6 +12,8 @@ import {
|
||||||
import { MediaType, UserRole } from "@/generated/prisma/enums";
|
import { MediaType, UserRole } from "@/generated/prisma/enums";
|
||||||
import { formatAverageRating } from "@/lib/reviews";
|
import { formatAverageRating } from "@/lib/reviews";
|
||||||
|
|
||||||
|
import { isStripeConfigured } from "@/lib/stripe";
|
||||||
|
|
||||||
import { BookingForm } from "../_components/booking-form";
|
import { BookingForm } from "../_components/booking-form";
|
||||||
import { CarbetGallery } from "../_components/carbet-gallery";
|
import { CarbetGallery } from "../_components/carbet-gallery";
|
||||||
import { CarbetMap } from "../_components/carbet-map";
|
import { CarbetMap } from "../_components/carbet-map";
|
||||||
|
|
@ -255,6 +257,7 @@ export default async function PublicCarbetPage({ params }: PageProps) {
|
||||||
minStayNights={carbet.minStayNights}
|
minStayNights={carbet.minStayNights}
|
||||||
maxStayNights={carbet.maxStayNights}
|
maxStayNights={carbet.maxStayNights}
|
||||||
isAuthenticated={Boolean(viewerId)}
|
isAuthenticated={Boolean(viewerId)}
|
||||||
|
stripeEnabled={isStripeConfigured()}
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ type Props = {
|
||||||
minStayNights: number | null;
|
minStayNights: number | null;
|
||||||
maxStayNights: number | null;
|
maxStayNights: number | null;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
|
stripeEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function todayPlus(n: number): string {
|
function todayPlus(n: number): string {
|
||||||
|
|
@ -38,6 +39,7 @@ export function BookingForm({
|
||||||
minStayNights,
|
minStayNights,
|
||||||
maxStayNights,
|
maxStayNights,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
|
stripeEnabled,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [startDate, setStartDate] = useState<string | null>(null);
|
const [startDate, setStartDate] = useState<string | null>(null);
|
||||||
|
|
@ -88,6 +90,34 @@ export function BookingForm({
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
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", {
|
const res = await fetch("/api/bookings", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
|
|
@ -187,7 +217,13 @@ export function BookingForm({
|
||||||
disabled={!canSubmit}
|
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"
|
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>
|
</button>
|
||||||
|
|
||||||
{!isAuthenticated ? (
|
{!isAuthenticated ? (
|
||||||
|
|
@ -200,7 +236,9 @@ export function BookingForm({
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<p className="text-center text-[11px] text-zinc-500">
|
<p className="text-center text-[11px] text-zinc-500">
|
||||||
Le créneau est bloqué dès l'envoi. Statut « En attente » jusqu'à 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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
import Stripe from "stripe";
|
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;
|
let stripeClient: Stripe | null = null;
|
||||||
|
|
||||||
export function getStripeClient(): Stripe {
|
export function getStripeClient(): Stripe {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue