From 510bd352d726e90a961dd8732b0d4ecf54b0585e Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 14:50:45 +0000 Subject: [PATCH 1/2] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/lib/payments.functions.ts | 62 +++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/lib/payments.functions.ts b/src/lib/payments.functions.ts index f274b17..dbca942 100644 --- a/src/lib/payments.functions.ts +++ b/src/lib/payments.functions.ts @@ -189,3 +189,65 @@ export const getPublicPaymentRequest = createServerFn({ method: "GET" }) if (!row) throw new Error("Payment request not found"); return row; }); + +// Public: always creates a fresh Stripe Checkout session for the pay page. +// Old stripe_checkout_url values become invalid when the Stripe key is +// rotated or after 24h, so we never reuse the stored URL on click. +export const startPublicCheckout = createServerFn({ method: "POST" }) + .inputValidator((data: { id: string }) => data) + .handler(async ({ data }) => { + const { supabaseAdmin } = await import("@/integrations/supabase/client.server"); + const { data: row, error } = await supabaseAdmin + .from("payment_requests") + .select("*") + .eq("id", data.id) + .maybeSingle(); + if (error || !row) throw new Error("Payment request not found"); + if (row.status === "paid") throw new Error("This payment has already been paid"); + if (row.status === "canceled") throw new Error("This payment request was canceled"); + + const stripe = getStripe(); + const origin = getOrigin(); + const successUrl = `${origin}/pay/${row.id}?status=success`; + const cancelUrl = `${origin}/pay/${row.id}?status=cancel`; + + const session = await stripe.checkout.sessions.create({ + mode: "payment", + payment_method_types: ["card"], + customer_email: row.recipient_email || undefined, + line_items: [ + { + price_data: { + currency: "usd", + unit_amount: row.base_amount_cents, + product_data: { + name: row.description || `Payment from ${row.recipient_name}`, + }, + }, + quantity: 1, + }, + { + price_data: { + currency: "usd", + unit_amount: row.fee_amount_cents, + product_data: { name: "Processing fee (2.9% + $0.30)" }, + }, + quantity: 1, + }, + ], + success_url: successUrl, + cancel_url: cancelUrl, + metadata: { payment_request_id: row.id }, + }); + + await supabaseAdmin + .from("payment_requests") + .update({ + stripe_session_id: session.id, + stripe_checkout_url: session.url ?? null, + }) + .eq("id", row.id); + + if (!session.url) throw new Error("Stripe did not return a checkout URL"); + return { checkout_url: session.url }; + }); From ce4d543cbf690c6cac35022592cd8806e9c6c5bc Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 11 May 2026 14:51:04 +0000 Subject: [PATCH 2/2] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/routes/pay.$id.tsx | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/routes/pay.$id.tsx b/src/routes/pay.$id.tsx index 032287e..cb29723 100644 --- a/src/routes/pay.$id.tsx +++ b/src/routes/pay.$id.tsx @@ -2,11 +2,11 @@ import { createFileRoute, useSearch } from "@tanstack/react-router"; import { useEffect, useState } from "react"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; -import { CheckCircle2, CreditCard, XCircle } from "lucide-react"; +import { CheckCircle2, CreditCard, XCircle, Loader2 } from "lucide-react"; import { formatCurrency } from "@/lib/format"; -import { getPublicPaymentRequest } from "@/lib/payments.functions"; +import { getPublicPaymentRequest, startPublicCheckout } from "@/lib/payments.functions"; import { JusticeIcon } from "@/components/justice-icon"; +import { toast } from "sonner"; export const Route = createFileRoute("/pay/$id")({ validateSearch: (s: Record) => ({ @@ -43,6 +43,7 @@ function PayPage() { const data = Route.useLoaderData(); const search = useSearch({ from: "/pay/$id" }); const [row, setRow] = useState(data); + const [starting, setStarting] = useState(false); useEffect(() => { setRow(data); @@ -104,19 +105,26 @@ function PayPage() { - {row.stripe_checkout_url ? ( - - ) : ( - Checkout link unavailable - )} + {search.status === "cancel" && (