"use client" import { useState, useEffect } from "react" import { useParams, useSearchParams } from "next/navigation" import { AmountStep } from "./steps/amount-step" import { ScheduleStep } from "./steps/schedule-step" import { PaymentStep } from "./steps/payment-step" import { IdentityStep } from "./steps/identity-step" import { ConfirmationStep } from "./steps/confirmation-step" import { BankInstructionsStep } from "./steps/bank-instructions-step" import { ExternalRedirectStep } from "./steps/external-redirect-step" import { CardPaymentStep } from "./steps/card-payment-step" import { DirectDebitStep } from "./steps/direct-debit-step" import { Heart, ChevronLeft, ArrowRight } from "lucide-react" export type Rail = "bank" | "gocardless" | "card" export interface PledgeData { amountPence: number rail: Rail donorName: string donorEmail: string donorPhone: string donorAddressLine1: string donorPostcode: string giftAid: boolean isZakat: boolean emailOptIn: boolean whatsappOptIn: boolean // eslint-disable-next-line @typescript-eslint/no-explicit-any consentMeta?: any // Scheduling scheduleMode: "now" | "date" | "installments" dueDate?: string installmentCount?: number installmentDates?: string[] // Conditional / match funding isConditional: boolean conditionType?: "threshold" | "match" | "custom" conditionText?: string conditionThreshold?: number } interface EventInfo { id: string name: string organizationName: string qrSourceId: string | null qrSourceLabel: string | null paymentMode: "self" | "external" externalUrl: string | null externalPlatform: string | null zakatEligible: boolean hasStripe: boolean goalAmount: number | null } /* Flow: -1 = Mini widget (embed only — sleek card that starts the flow) 0 = Amount 1 = Schedule (When to pay?) 2 = Payment method (if "now") or Identity (if deferred) 3 = Identity (for bank transfer "now") 4 = Bank instructions (now) 5 = Confirmation (generic — card, DD, or deferred pledge) 6 = Card payment 7 = External redirect 8 = Direct Debit */ export default function PledgePage() { const params = useParams() const searchParams = useSearchParams() const token = params.token as string // Detect embed: ?embed=1 query param OR inside an iframe const [isEmbed, setIsEmbed] = useState(false) useEffect(() => { const embedParam = searchParams.get("embed") === "1" const inIframe = typeof window !== "undefined" && window.self !== window.top setIsEmbed(embedParam || inIframe) }, [searchParams]) const [step, setStep] = useState(0) const [eventInfo, setEventInfo] = useState(null) const [pledgeData, setPledgeData] = useState({ amountPence: 0, rail: "bank", donorName: "", donorEmail: "", donorPhone: "", donorAddressLine1: "", donorPostcode: "", giftAid: false, isZakat: false, emailOptIn: false, whatsappOptIn: false, scheduleMode: "now", isConditional: false, }) const [pledgeResult, setPledgeResult] = useState<{ id: string reference: string bankDetails?: { bankName: string; sortCode: string; accountNo: string; accountName: string } } | null>(null) const [loading, setLoading] = useState(true) const [error, setError] = useState("") useEffect(() => { fetch(`/api/qr/${token}`) .then((r) => r.json()) .then((data) => { if (data.error) setError(data.error) else setEventInfo(data) setLoading(false) }) .catch(() => { setError("Unable to load pledge page"); setLoading(false) }) fetch("/api/analytics", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ eventType: "pledge_start", metadata: { token } }), }).catch(() => {}) }, [token]) // Start at mini widget step for embeds once loaded useEffect(() => { if (!loading && isEmbed && step === 0) { setStep(-1) } }, [loading, isEmbed, step]) // Notify parent frame of height changes when embedded useEffect(() => { if (!isEmbed) return const sendHeight = () => { const height = step === -1 ? 160 : 700 window.parent.postMessage({ type: "pnpl-resize", height }, "*") } sendHeight() }, [isEmbed, step]) const isExternal = eventInfo?.paymentMode === "external" && eventInfo?.externalUrl // Step 0: Amount selected const handleAmountSelected = (amountPence: number, conditional?: { isConditional: boolean; conditionType?: "threshold" | "match" | "custom"; conditionText?: string; conditionThreshold?: number }) => { const conditionalData = conditional || { isConditional: false } setPledgeData((d) => ({ ...d, amountPence, ...conditionalData })) if (isExternal) { setPledgeData((d) => ({ ...d, amountPence, rail: "bank", scheduleMode: "now", ...conditionalData })) setStep(3) } else { setStep(1) } } // Step 1: Schedule selected (self-payment events only) const handleScheduleSelected = (schedule: { mode: "now" | "date" | "installments" dueDate?: string installmentCount?: number installmentDates?: string[] }) => { setPledgeData((d) => ({ ...d, scheduleMode: schedule.mode, dueDate: schedule.dueDate, installmentCount: schedule.installmentCount, installmentDates: schedule.installmentDates, })) if (schedule.mode === "now") { setStep(2) // → Payment method selection } else { // Deferred or installments: skip payment method, go to identity setPledgeData((d) => ({ ...d, rail: "bank" })) setStep(3) // → Identity } } // Step 2: Payment method selected (only for "now" self-payment mode) const handleRailSelected = (rail: Rail) => { setPledgeData((d) => ({ ...d, rail })) setStep(rail === "bank" ? 3 : rail === "card" ? 6 : 8) } // Submit pledge (from identity step, or card/DD steps) // eslint-disable-next-line @typescript-eslint/no-explicit-any const submitPledge = async (identity: any) => { const finalData = { ...pledgeData, ...identity } setPledgeData(finalData) // Inject IP + user agent into consent metadata for audit trail const consentMeta = finalData.consentMeta ? { ...finalData.consentMeta, userAgent: navigator.userAgent, } : undefined try { const res = await fetch("/api/pledges", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ...finalData, consentMeta, eventId: eventInfo?.id, qrSourceId: eventInfo?.qrSourceId, isZakat: finalData.isZakat || false, }), }) const result = await res.json() if (result.error) { setError(result.error); return } setPledgeResult(result) // Where to go after pledge is created: if (isExternal) { setStep(7) // External redirect } else if (finalData.scheduleMode === "now" && finalData.rail === "bank") { setStep(4) // Bank instructions } else { setStep(5) // Confirmation } } catch { setError("Something went wrong. Please try again.") } } if (loading) { return (
🤲

Loading...

) } if (error) { return (
😔

Something went wrong

{error}

) } const shareUrl = `${typeof window !== "undefined" ? window.location.origin : ""}/p/${token}` const isDeferred = pledgeData.scheduleMode !== "now" // Format due date for display const dueDateLabel = pledgeData.dueDate ? new Date(pledgeData.dueDate).toLocaleDateString("en-GB", { weekday: "short", day: "numeric", month: "short" }) : pledgeData.installmentCount ? `${pledgeData.installmentCount} monthly payments` : undefined const steps: Record = { [-1]: setStep(0)} />, 0: , 1: , 2: , 3: , 4: pledgeResult && , 5: pledgeResult && ( ), 6: , 7: pledgeResult && , 8: , } const backableSteps = new Set([1, 2, 3, 6, 8]) const getBackStep = (s: number): number => { if (s === 0 && isEmbed) return -1 // amount → mini widget (embed only) if (s === 6 || s === 8) return 2 // card/DD → payment method if (s === 3 && pledgeData.scheduleMode !== "now") return 1 // deferred identity → schedule if (s === 3) return 2 // bank identity → payment method return s - 1 } // Can go back from step 0 only in embed mode const canGoBack = backableSteps.has(step) || (step === 0 && isEmbed) const isFinished = step === 4 || step === 5 || step === 7 const stepLabels: Record = { [-1]: "", 0: "Amount", 1: "Schedule", 2: "Payment method", 3: "Your details", 4: "Complete", 5: "Complete", 6: "Card payment", 7: "Redirecting", 8: "Direct Debit", } const progressMap: Record = { [-1]: 0, 0: 8, 1: 25, 2: 40, 3: 60, 4: 100, 5: 100, 6: 60, 7: 100, 8: 60 } const progressPercent = progressMap[step] ?? 10 // Mini widget mode — compact layout, no chrome if (step === -1) { return (
{steps[-1]}
) } return (
{/* ── Top bar: progress + back + step label ── */}
{/* Progress bar */}
{/* Navigation row */}
{canGoBack ? ( ) : (
/* Spacer */ )}

{eventInfo?.organizationName}

{!isFinished && step >= 0 ? ( {stepLabels[step]} ) : (
)}
{/* QR source label */} {eventInfo?.qrSourceLabel && (

{eventInfo.qrSourceLabel}

)} {/* Content — padded for fixed header */}
{steps[step]}
) } /* ── Mini Widget ───────────────────────────────────────────────── Compact card shown in embedded mode. Designed to be: - 80-120px tall at rest - Branded but not loud - Single CTA button that launches the full flow ────────────────────────────────────────────────────────────── */ function MiniWidget({ eventInfo, onStart, }: { eventInfo: EventInfo | null onStart: () => void }) { const [hover, setHover] = useState(false) return (
{/* Powered by */}

Powered by Pledge Now, Pay Later

) }