Universal CTA: payment_block + pay_link adapt to bank/external/card/DD
THE PROBLEM:
Every message assumed bank transfer. But pledges can be on LaunchGood,
JustGiving, Stripe, GoCardless, or bank transfer. The CTA must adapt.
THE SOLUTION: Two smart template variables that resolve at send time:
{{payment_block}} — Full payment instruction block (receipts, due date):
Bank transfer: sort code, account, reference with dividers
External: 'Complete your donation on LaunchGood: [link]'
Card: 'Pay by card: [Stripe checkout link]'
GoCardless: 'DD is set up, payment collected automatically'
{{pay_link}} — Single link to complete payment (reminders):
External: links directly to LaunchGood/JustGiving URL
Everything else: /p/pay?ref=XXX which adapts per rail
NEW: /p/pay?ref=PNPL-XXXX — Universal payment completion page
One link in every message. Shows the right thing for every rail:
- Bank: sort code, account, reference with copy buttons
- External: link + redirect to platform
- Card: Stripe checkout button
- GoCardless: 'DD set up, nothing to do'
- Already paid: green checkmark
- Cancelled: 'no action needed'
NEW: /api/pledges/pay-info — Public endpoint for pay page data
DEFAULT TEMPLATES REWRITTEN:
Step 0 (receipt): {{payment_block}} instead of hardcoded bank details
Step 4 (due date): {{payment_block}} + {{due_date}}
Steps 1-3 (reminders): {{pay_link}} as primary CTA
Every message now has a clear call-to-action regardless of rail
AI PROMPT UPDATED:
- Knows about universal CTA system
- Enforces {{payment_block}} for receipts, {{pay_link}} for reminders
- Told to NEVER hardcode bank details — use smart variables
- Rewrite action also enforces CTA rules
CRON UPDATED:
- buildPaymentBlock() resolves per pledge context
- computePayLink() resolves per payment mode
- Event query now includes paymentMode, externalUrl, externalPlatform
- Both due date and normal reminders use universal vars
This commit is contained in:
@@ -95,11 +95,11 @@ export async function POST(request: NextRequest) {
|
||||
})
|
||||
|
||||
const stepLabels: Record<number, string> = {
|
||||
0: "pledge receipt (instant confirmation — MUST include full bank transfer details)",
|
||||
4: "due date reminder (sent on the day the donor said they'd pay — MUST include due date and bank details)",
|
||||
1: "gentle reminder (day 2, donor hasn't paid yet — friendly check-in)",
|
||||
2: "impact nudge (day 7, building urgency with purpose — why their money matters)",
|
||||
3: "final reminder (day 14, last message before marking overdue — respect + options)",
|
||||
0: "pledge receipt (instant confirmation — MUST include {{payment_block}} for payment details)",
|
||||
4: "due date reminder (sent on the day the donor said they'd pay — MUST include {{due_date}} and {{payment_block}})",
|
||||
1: "gentle reminder (day 2, donor hasn't paid yet — friendly check-in with {{pay_link}})",
|
||||
2: "impact nudge (day 7, building urgency with purpose — why their money matters, include {{pay_link}})",
|
||||
3: "final reminder (day 14, last message before marking overdue — respect + options, include {{pay_link}} and {{cancel_url}})",
|
||||
}
|
||||
|
||||
const channelRules: Record<string, string> = {
|
||||
@@ -159,10 +159,28 @@ ${requiredStr}
|
||||
|
||||
These are template placeholders that get filled in with real data at send time. If you leave any out, the message is BROKEN and the donor won't know how to pay.
|
||||
|
||||
${step === 0 ? `STEP 0 IS THE RECEIPT. The donor just pledged. They NEED the bank details to actually transfer the money. You MUST include a bank details section with {{sort_code}}, {{account_no}}, {{bank_name}}, and {{reference}}. Without this, the donor CANNOT PAY.` : ""}
|
||||
${step === 4 ? `STEP 4 IS THE DUE DATE REMINDER. Sent on the exact day the donor said they'd pay. You MUST include {{due_date}} so they know today is the day. Include bank details ({{sort_code}}, {{account_no}}, {{bank_name}}) so they can pay right now without looking anything up.` : ""}
|
||||
UNIVERSAL CTA SYSTEM — READ THIS:
|
||||
We DON'T always process payments ourselves. Donors might pay via bank transfer, card (Stripe), Direct Debit, or EXTERNAL platforms (LaunchGood, JustGiving, GoFundMe). The template must work for ALL of these. That's why we have two smart CTA variables:
|
||||
|
||||
All available variables: {{name}}, {{amount}}, {{event}}, {{reference}}, {{bank_name}}, {{sort_code}}, {{account_no}}, {{org_name}}, {{days}}, {{due_date}}, {{cancel_url}}, {{pledge_url}}
|
||||
{{payment_block}} — Renders the FULL payment instruction. Adapts automatically at send time:
|
||||
• Bank transfer → sort code, account number, reference
|
||||
• External platform → link to LaunchGood/JustGiving
|
||||
• Card → link to Stripe checkout
|
||||
• Direct Debit → "DD is set up, payment collected automatically"
|
||||
USE THIS in receipts (step 0) and due date messages (step 4). Just write {{payment_block}} on its own line.
|
||||
|
||||
{{pay_link}} — Single clickable link to complete payment. Adapts at send time:
|
||||
• External → links to the platform directly
|
||||
• Everything else → links to /p/pay which shows the right thing
|
||||
USE THIS in reminders (steps 1, 2, 3) as the primary CTA.
|
||||
|
||||
NEVER hardcode bank details like sort codes or account numbers. Use {{payment_block}} or {{pay_link}} instead. They handle everything.
|
||||
|
||||
${step === 0 ? `STEP 0 IS THE RECEIPT. The donor just pledged. Include {{payment_block}} — it gives them everything they need to pay, regardless of payment method.` : ""}
|
||||
${step === 4 ? `STEP 4 IS THE DUE DATE REMINDER. Today is the day. Include {{due_date}} so they know, and {{payment_block}} so they can act immediately.` : ""}
|
||||
${[1, 2, 3].includes(step) ? `THIS IS A REMINDER. The CTA is {{pay_link}} — ONE link that takes them to the right place. Don't include {{payment_block}} (too heavy for a reminder). Keep the CTA clear: "Complete your pledge: {{pay_link}}"` : ""}
|
||||
|
||||
All available variables: {{name}}, {{amount}}, {{event}}, {{reference}}, {{payment_block}}, {{pay_link}}, {{org_name}}, {{days}}, {{due_date}}, {{cancel_url}}, {{pledge_url}}
|
||||
|
||||
Rules:
|
||||
- ${channelRules[channel] || "Keep appropriate for the channel."}
|
||||
@@ -297,6 +315,12 @@ Generate a challenger variant B using a DIFFERENT psychological approach. Rememb
|
||||
CRITICAL: You MUST keep ALL these template variables: ${requiredStr}
|
||||
These are placeholders filled with real data. If you remove any, the message breaks.
|
||||
|
||||
CTA RULES:
|
||||
- {{payment_block}} renders full payment details (bank transfer, external platform link, card link, or DD confirmation). Keep it as-is.
|
||||
- {{pay_link}} is a single link to complete payment (adapts to any payment method). Keep it as-is.
|
||||
- NEVER replace {{payment_block}} with hardcoded bank details.
|
||||
- EVERY message must have a clear CTA to complete the pledge.
|
||||
|
||||
${channelHints[channel] || ""}
|
||||
Return ONLY the rewritten message text — nothing else. No JSON, no explanation, no markdown blocks.`
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"
|
||||
import prisma from "@/lib/prisma"
|
||||
import { sendWhatsAppMessage, isWhatsAppReady } from "@/lib/whatsapp"
|
||||
import { generateReminderContent } from "@/lib/reminders"
|
||||
import { resolveTemplate } from "@/lib/templates"
|
||||
import { resolveTemplate, buildPaymentBlock, computePayLink } from "@/lib/templates"
|
||||
|
||||
/**
|
||||
* Process and send pending reminders + due date messages.
|
||||
@@ -48,7 +48,12 @@ export async function GET(request: NextRequest) {
|
||||
status: { notIn: ["paid", "cancelled"] },
|
||||
},
|
||||
include: {
|
||||
event: { select: { name: true } },
|
||||
event: {
|
||||
select: {
|
||||
name: true, paymentMode: true,
|
||||
externalUrl: true, externalPlatform: true,
|
||||
},
|
||||
},
|
||||
organization: {
|
||||
select: {
|
||||
id: true, name: true, bankSortCode: true,
|
||||
@@ -84,15 +89,39 @@ export async function GET(request: NextRequest) {
|
||||
? pledge.dueDate.toLocaleDateString("en-GB", { weekday: "long", day: "numeric", month: "long" })
|
||||
: "today"
|
||||
|
||||
const sortCode = bankDetails?.sortCode || pledge.organization.bankSortCode || "N/A"
|
||||
const accountNo = bankDetails?.accountNo || pledge.organization.bankAccountNo || "N/A"
|
||||
const bankName = bankDetails?.accountName || pledge.organization.bankAccountName || pledge.organization.name || "N/A"
|
||||
|
||||
const payLink = computePayLink({
|
||||
paymentMode: pledge.event.paymentMode,
|
||||
externalUrl: pledge.event.externalUrl || undefined,
|
||||
reference: pledge.reference,
|
||||
baseUrl,
|
||||
})
|
||||
|
||||
const paymentBlock = buildPaymentBlock({
|
||||
rail: pledge.rail,
|
||||
paymentMode: pledge.event.paymentMode,
|
||||
reference: pledge.reference,
|
||||
payLink,
|
||||
sortCode, accountNo, bankName,
|
||||
externalUrl: pledge.event.externalUrl || undefined,
|
||||
externalPlatform: pledge.event.externalPlatform || undefined,
|
||||
channel: "whatsapp",
|
||||
})
|
||||
|
||||
const vars: Record<string, string> = {
|
||||
name: pledge.donorName?.split(" ")[0] || "there",
|
||||
amount: (pledge.amountPence / 100).toFixed(0),
|
||||
event: pledge.event.name,
|
||||
reference: pledge.reference,
|
||||
due_date: dueFormatted,
|
||||
sort_code: bankDetails?.sortCode || pledge.organization.bankSortCode || "N/A",
|
||||
account_no: bankDetails?.accountNo || pledge.organization.bankAccountNo || "N/A",
|
||||
bank_name: bankDetails?.accountName || pledge.organization.bankAccountName || pledge.organization.name || "N/A",
|
||||
payment_block: paymentBlock,
|
||||
pay_link: payLink,
|
||||
sort_code: sortCode,
|
||||
account_no: accountNo,
|
||||
bank_name: bankName,
|
||||
org_name: pledge.organization.name || "Our charity",
|
||||
days: String(Math.floor((now.getTime() - pledge.createdAt.getTime()) / 86400000)),
|
||||
cancel_url: `${baseUrl}/p/cancel?ref=${pledge.reference}`,
|
||||
@@ -103,8 +132,8 @@ export async function GET(request: NextRequest) {
|
||||
if (customTemplate) {
|
||||
messageText = resolveTemplate(customTemplate.body, vars)
|
||||
} else {
|
||||
// Fallback hardcoded due date message
|
||||
messageText = `Salaam ${vars.name} 👋\n\nJust a heads up — your *£${vars.amount}* pledge to *${vars.event}* is due today (${vars.due_date}).\n\nYour ref: \`${vars.reference}\`\n\n━━━━━━━━━━━━━━━━━━\nSort Code: \`${vars.sort_code}\`\nAccount: \`${vars.account_no}\`\nName: ${vars.bank_name}\nReference: \`${vars.reference}\`\n━━━━━━━━━━━━━━━━━━\n\nAlready sent it? Reply *PAID* 🙏\nNeed help? Reply *HELP*`
|
||||
// Fallback hardcoded due date message using payment block
|
||||
messageText = `Salaam ${vars.name} 👋\n\nJust a heads up — your *£${vars.amount}* pledge to *${vars.event}* is due today (${vars.due_date}).\n\nYour ref: \`${vars.reference}\`\n\n${paymentBlock}\n\nAlready sorted? Reply *PAID* 🙏\nNeed help? Reply *HELP*`
|
||||
}
|
||||
|
||||
const result = await sendWhatsAppMessage(phone, messageText)
|
||||
@@ -150,7 +179,12 @@ export async function GET(request: NextRequest) {
|
||||
include: {
|
||||
pledge: {
|
||||
include: {
|
||||
event: { select: { name: true } },
|
||||
event: {
|
||||
select: {
|
||||
name: true, paymentMode: true,
|
||||
externalUrl: true, externalPlatform: true,
|
||||
},
|
||||
},
|
||||
organization: {
|
||||
select: {
|
||||
id: true, name: true, bankSortCode: true,
|
||||
@@ -173,15 +207,39 @@ export async function GET(request: NextRequest) {
|
||||
const daysSince = Math.floor((now.getTime() - pledge.createdAt.getTime()) / 86400000)
|
||||
const bankDetails = pledge.paymentInstruction?.bankDetails as Record<string, string> | null
|
||||
|
||||
// Build template variables
|
||||
// Build template variables with universal CTA
|
||||
const sortCode = bankDetails?.sortCode || pledge.organization.bankSortCode || "N/A"
|
||||
const accountNo = bankDetails?.accountNo || pledge.organization.bankAccountNo || "N/A"
|
||||
const bankName = bankDetails?.accountName || pledge.organization.bankAccountName || pledge.organization.name || "N/A"
|
||||
|
||||
const payLink = computePayLink({
|
||||
paymentMode: pledge.event.paymentMode,
|
||||
externalUrl: pledge.event.externalUrl || undefined,
|
||||
reference: pledge.reference,
|
||||
baseUrl,
|
||||
})
|
||||
|
||||
const paymentBlock = buildPaymentBlock({
|
||||
rail: pledge.rail,
|
||||
paymentMode: pledge.event.paymentMode,
|
||||
reference: pledge.reference,
|
||||
payLink,
|
||||
sortCode, accountNo, bankName,
|
||||
externalUrl: pledge.event.externalUrl || undefined,
|
||||
externalPlatform: pledge.event.externalPlatform || undefined,
|
||||
channel,
|
||||
})
|
||||
|
||||
const vars: Record<string, string> = {
|
||||
name: pledge.donorName?.split(" ")[0] || "there",
|
||||
amount: (pledge.amountPence / 100).toFixed(0),
|
||||
event: pledge.event.name,
|
||||
reference: pledge.reference,
|
||||
sort_code: bankDetails?.sortCode || pledge.organization.bankSortCode || "N/A",
|
||||
account_no: bankDetails?.accountNo || pledge.organization.bankAccountNo || "N/A",
|
||||
bank_name: bankDetails?.accountName || pledge.organization.bankAccountName || pledge.organization.name || "N/A",
|
||||
payment_block: paymentBlock,
|
||||
pay_link: payLink,
|
||||
sort_code: sortCode,
|
||||
account_no: accountNo,
|
||||
bank_name: bankName,
|
||||
org_name: pledge.organization.name || "Our charity",
|
||||
days: String(daysSince),
|
||||
due_date: pledge.dueDate
|
||||
|
||||
62
pledge-now-pay-later/src/app/api/pledges/pay-info/route.ts
Normal file
62
pledge-now-pay-later/src/app/api/pledges/pay-info/route.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import prisma from "@/lib/prisma"
|
||||
|
||||
/**
|
||||
* GET /api/pledges/pay-info?ref=PNPL-XXXX
|
||||
*
|
||||
* Public endpoint (no auth) — returns payment info for a pledge.
|
||||
* Used by /p/pay to show the right payment CTA.
|
||||
*
|
||||
* Returns different data depending on rail/paymentMode:
|
||||
* - Bank: sort code, account, bank name, reference
|
||||
* - External: external URL, platform name
|
||||
* - Card: flag to trigger Stripe checkout
|
||||
* - GoCardless: "set up" flag
|
||||
*/
|
||||
export async function GET(request: NextRequest) {
|
||||
if (!prisma) return NextResponse.json({ error: "Service unavailable" }, { status: 503 })
|
||||
|
||||
const ref = request.nextUrl.searchParams.get("ref")
|
||||
if (!ref) return NextResponse.json({ error: "Missing ref parameter" }, { status: 400 })
|
||||
|
||||
const pledge = await prisma.pledge.findUnique({
|
||||
where: { reference: ref },
|
||||
include: {
|
||||
event: {
|
||||
select: {
|
||||
name: true, paymentMode: true,
|
||||
externalUrl: true, externalPlatform: true,
|
||||
},
|
||||
},
|
||||
organization: {
|
||||
select: {
|
||||
name: true, bankSortCode: true,
|
||||
bankAccountNo: true, bankAccountName: true, bankName: true,
|
||||
},
|
||||
},
|
||||
paymentInstruction: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!pledge) {
|
||||
return NextResponse.json({ error: "Pledge not found. Check your reference." }, { status: 404 })
|
||||
}
|
||||
|
||||
const bankDetails = pledge.paymentInstruction?.bankDetails as Record<string, string> | null
|
||||
|
||||
return NextResponse.json({
|
||||
reference: pledge.reference,
|
||||
amountPence: pledge.amountPence,
|
||||
status: pledge.status,
|
||||
rail: pledge.rail,
|
||||
eventName: pledge.event.name,
|
||||
orgName: pledge.organization.name,
|
||||
paymentMode: pledge.event.paymentMode,
|
||||
externalUrl: pledge.event.externalUrl,
|
||||
externalPlatform: pledge.event.externalPlatform,
|
||||
sortCode: bankDetails?.sortCode || pledge.organization.bankSortCode,
|
||||
accountNo: bankDetails?.accountNo || pledge.organization.bankAccountNo,
|
||||
bankName: bankDetails?.accountName || pledge.organization.bankAccountName || pledge.organization.bankName,
|
||||
dueDate: pledge.dueDate?.toISOString() || null,
|
||||
})
|
||||
}
|
||||
287
pledge-now-pay-later/src/app/p/pay/page.tsx
Normal file
287
pledge-now-pay-later/src/app/p/pay/page.tsx
Normal file
@@ -0,0 +1,287 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect, Suspense } from "react"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { Building2, ExternalLink, CreditCard, Landmark, Shield, Copy, Check, Loader2 } from "lucide-react"
|
||||
|
||||
/**
|
||||
* /p/pay?ref=PNPL-XXXX
|
||||
*
|
||||
* Universal payment completion page. ONE link that adapts to any rail:
|
||||
*
|
||||
* - Bank transfer → shows bank details with copy buttons
|
||||
* - External (LaunchGood, JustGiving) → shows link + redirects
|
||||
* - Card (Stripe) → creates checkout session + redirects
|
||||
* - GoCardless → shows "DD is set up, nothing to do"
|
||||
*
|
||||
* This is the CTA target in every WhatsApp/SMS/Email message.
|
||||
* The donor taps ONE link and gets exactly what they need.
|
||||
*/
|
||||
|
||||
interface PledgeInfo {
|
||||
reference: string
|
||||
amountPence: number
|
||||
status: string
|
||||
rail: string
|
||||
eventName: string
|
||||
orgName: string
|
||||
paymentMode: string
|
||||
externalUrl?: string
|
||||
externalPlatform?: string
|
||||
sortCode?: string
|
||||
accountNo?: string
|
||||
bankName?: string
|
||||
dueDate?: string
|
||||
checkoutUrl?: string
|
||||
}
|
||||
|
||||
function PayPage() {
|
||||
const params = useSearchParams()
|
||||
const ref = params.get("ref")
|
||||
const [pledge, setPledge] = useState<PledgeInfo | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState("")
|
||||
const [copied, setCopied] = useState("")
|
||||
const [creatingCheckout, setCreatingCheckout] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref) { setError("No pledge reference provided."); setLoading(false); return }
|
||||
fetch(`/api/pledges/pay-info?ref=${encodeURIComponent(ref)}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.error) setError(data.error)
|
||||
else setPledge(data)
|
||||
})
|
||||
.catch(() => setError("Something went wrong. Please try again."))
|
||||
.finally(() => setLoading(false))
|
||||
}, [ref])
|
||||
|
||||
const copy = (text: string, label: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
setCopied(label)
|
||||
setTimeout(() => setCopied(""), 2000)
|
||||
}
|
||||
|
||||
const handleCardPayment = async () => {
|
||||
if (!pledge) return
|
||||
setCreatingCheckout(true)
|
||||
try {
|
||||
const res = await fetch("/api/stripe/checkout", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ pledgeReference: pledge.reference }),
|
||||
})
|
||||
const data = await res.json()
|
||||
if (data.checkoutUrl) {
|
||||
window.location.href = data.checkoutUrl
|
||||
} else {
|
||||
setError("Could not create payment session. Please try again.")
|
||||
}
|
||||
} catch {
|
||||
setError("Payment service unavailable. Please try again.")
|
||||
}
|
||||
setCreatingCheckout(false)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 text-[#1E40AF] animate-spin" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !pledge) {
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center p-6">
|
||||
<div className="max-w-sm w-full text-center space-y-4">
|
||||
<div className="inline-flex items-center justify-center w-12 h-12 bg-[#111827]">
|
||||
<span className="text-white text-sm font-black">P</span>
|
||||
</div>
|
||||
<h1 className="text-xl font-black text-[#111827]">{error || "Pledge not found"}</h1>
|
||||
<p className="text-sm text-gray-500">Check the link in your message, or contact the charity.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Already paid or cancelled
|
||||
if (pledge.status === "paid") {
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center p-6">
|
||||
<div className="max-w-sm w-full text-center space-y-4">
|
||||
<div className="w-16 h-16 bg-[#16A34A] flex items-center justify-center mx-auto" style={{ borderRadius: "50%" }}>
|
||||
<Check className="h-8 w-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-black text-[#111827]">Already paid ✓</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
Your £{(pledge.amountPence / 100).toFixed(0)} pledge to {pledge.eventName} has been received. Thank you!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (pledge.status === "cancelled") {
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center p-6">
|
||||
<div className="max-w-sm w-full text-center space-y-4">
|
||||
<h1 className="text-2xl font-black text-[#111827]">Pledge cancelled</h1>
|
||||
<p className="text-sm text-gray-500">This pledge has been withdrawn. No further action needed.</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const pounds = (pledge.amountPence / 100).toFixed(0)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white flex items-center justify-center p-6">
|
||||
<div className="max-w-md w-full space-y-6">
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-2">
|
||||
<div className="inline-flex items-center justify-center w-10 h-10 bg-[#111827]">
|
||||
<span className="text-white text-xs font-black">P</span>
|
||||
</div>
|
||||
<h1 className="text-2xl font-black text-[#111827] tracking-tight">
|
||||
Complete your pledge
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
<span className="font-bold text-[#111827]">£{pounds}</span> to {pledge.eventName}
|
||||
</p>
|
||||
{pledge.dueDate && (
|
||||
<p className="text-xs text-[#F59E0B] font-semibold">
|
||||
Due: {new Date(pledge.dueDate).toLocaleDateString("en-GB", { weekday: "long", day: "numeric", month: "long" })}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── EXTERNAL PLATFORM ── */}
|
||||
{pledge.paymentMode === "external" && pledge.externalUrl && (
|
||||
<div className="space-y-4">
|
||||
<div className="border-2 border-[#1E40AF] p-6 text-center space-y-3">
|
||||
<ExternalLink className="h-8 w-8 text-[#1E40AF] mx-auto" />
|
||||
<p className="text-sm text-gray-600">
|
||||
{pledge.orgName} collects donations via <strong className="text-[#111827]">
|
||||
{pledge.externalPlatform === "launchgood" ? "LaunchGood" :
|
||||
pledge.externalPlatform === "justgiving" ? "JustGiving" :
|
||||
pledge.externalPlatform === "gofundme" ? "GoFundMe" :
|
||||
pledge.externalPlatform === "enthuse" ? "Enthuse" :
|
||||
"their donation page"}
|
||||
</strong>
|
||||
</p>
|
||||
<a href={pledge.externalUrl} target="_blank" rel="noopener noreferrer"
|
||||
className="block w-full bg-[#1E40AF] text-white py-3.5 text-sm font-bold hover:bg-blue-900 transition-colors text-center">
|
||||
Donate £{pounds} →
|
||||
</a>
|
||||
<p className="text-[11px] text-gray-400">
|
||||
Use reference <code className="bg-gray-100 px-1.5 py-0.5 font-mono text-[#111827]">{pledge.reference}</code> if asked
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 text-center">
|
||||
Once you've donated, reply <strong>PAID</strong> to our WhatsApp message so we can mark it.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── BANK TRANSFER ── */}
|
||||
{pledge.rail === "bank" && pledge.paymentMode !== "external" && (
|
||||
<div className="space-y-4">
|
||||
<div className="border-2 border-gray-200 divide-y divide-gray-200">
|
||||
<div className="p-4 bg-gray-50">
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 className="h-4 w-4 text-[#111827]" />
|
||||
<span className="text-sm font-bold text-[#111827]">Bank Transfer</span>
|
||||
<span className="ml-auto text-[10px] font-bold text-[#16A34A] bg-green-50 px-2 py-0.5">Zero fees</span>
|
||||
</div>
|
||||
</div>
|
||||
{[
|
||||
{ label: "Sort Code", value: pledge.sortCode || "N/A" },
|
||||
{ label: "Account Number", value: pledge.accountNo || "N/A" },
|
||||
{ label: "Account Name", value: pledge.bankName || "N/A" },
|
||||
{ label: "Reference", value: pledge.reference, highlight: true },
|
||||
].map(row => (
|
||||
<div key={row.label} className={`p-4 flex items-center justify-between ${row.highlight ? "bg-[#FEF3C7]" : ""}`}>
|
||||
<div>
|
||||
<p className="text-[10px] font-bold text-gray-400 uppercase tracking-wide">{row.label}</p>
|
||||
<p className={`text-sm font-mono font-bold ${row.highlight ? "text-[#111827]" : "text-[#111827]"}`}>{row.value}</p>
|
||||
</div>
|
||||
<button onClick={() => copy(row.value, row.label)}
|
||||
className="p-2 hover:bg-gray-100 transition-colors" title="Copy">
|
||||
{copied === row.label
|
||||
? <Check className="h-4 w-4 text-[#16A34A]" />
|
||||
: <Copy className="h-4 w-4 text-gray-400" />
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 text-center">
|
||||
⚠️ Use the exact reference above so {pledge.orgName} can match your payment.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── CARD / STRIPE ── */}
|
||||
{pledge.rail === "card" && pledge.paymentMode !== "external" && (
|
||||
<div className="space-y-4">
|
||||
<div className="border-2 border-[#1E40AF] p-6 text-center space-y-3">
|
||||
<CreditCard className="h-8 w-8 text-[#1E40AF] mx-auto" />
|
||||
<p className="text-sm text-gray-600">
|
||||
Secure card payment via Stripe
|
||||
</p>
|
||||
<button onClick={handleCardPayment} disabled={creatingCheckout}
|
||||
className="w-full bg-[#1E40AF] text-white py-3.5 text-sm font-bold hover:bg-blue-900 transition-colors disabled:opacity-50">
|
||||
{creatingCheckout
|
||||
? <span className="flex items-center justify-center gap-2"><Loader2 className="h-4 w-4 animate-spin" /> Creating payment…</span>
|
||||
: `Pay £${pounds} by card →`
|
||||
}
|
||||
</button>
|
||||
<p className="text-[11px] text-gray-400">
|
||||
Visa, Mastercard, Amex, Apple Pay, Google Pay
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── GOCARDLESS / DIRECT DEBIT ── */}
|
||||
{pledge.rail === "gocardless" && (
|
||||
<div className="space-y-4">
|
||||
<div className="border-2 border-[#16A34A] p-6 text-center space-y-3">
|
||||
<Landmark className="h-8 w-8 text-[#16A34A] mx-auto" />
|
||||
<h2 className="text-lg font-bold text-[#111827]">Direct Debit is set up</h2>
|
||||
<p className="text-sm text-gray-600">
|
||||
Payment of <strong>£{pounds}</strong> will be collected automatically within 3-5 working days.
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 mt-2">
|
||||
Protected by the Direct Debit Guarantee. You can cancel anytime.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="text-center space-y-3 pt-2">
|
||||
{pledge.status !== "paid" && pledge.status !== "cancelled" && (
|
||||
<a href={`/p/cancel?ref=${pledge.reference}`}
|
||||
className="text-xs text-gray-400 hover:text-red-600 transition-colors">
|
||||
Cancel this pledge
|
||||
</a>
|
||||
)}
|
||||
<div className="flex items-center justify-center gap-2 text-xs text-gray-400">
|
||||
<Shield className="h-3 w-3" />
|
||||
<span>{pledge.orgName} · Powered by Pledge Now, Pay Later</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function PayPageWrapper() {
|
||||
return (
|
||||
<Suspense fallback={<div className="min-h-screen bg-white flex items-center justify-center"><Loader2 className="h-6 w-6 text-[#1E40AF] animate-spin" /></div>}>
|
||||
<PayPage />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user