feat: remove FPX, add UK charity persona features
- Remove FPX payment rail entirely (Malaysian, not UK) - Add volunteer portal (/v/[code]) with live pledge tracking - Add public event page (/e/[slug]) with progress bar + social proof - Add fundraiser leaderboard (/dashboard/events/[id]/leaderboard) - Add WhatsApp share buttons on confirmation, bank instructions, volunteer view - Enhanced Gift Aid UX with +25% bonus display and HMRC declaration text - Gift Aid report export (HMRC-ready CSV filter) - Volunteer view link + WhatsApp share on QR code cards - Updated home page: 4 personas, 3 UK payment rails, 8 features - Public event API endpoint with privacy-safe donor name truncation - Volunteer API with stats, conversion rate, auto-refresh
This commit is contained in:
@@ -8,10 +8,9 @@ import { IdentityStep } from "./steps/identity-step"
|
||||
import { ConfirmationStep } from "./steps/confirmation-step"
|
||||
import { BankInstructionsStep } from "./steps/bank-instructions-step"
|
||||
import { CardPaymentStep } from "./steps/card-payment-step"
|
||||
import { FpxPaymentStep } from "./steps/fpx-payment-step"
|
||||
import { DirectDebitStep } from "./steps/direct-debit-step"
|
||||
|
||||
export type Rail = "bank" | "gocardless" | "card" | "fpx"
|
||||
export type Rail = "bank" | "gocardless" | "card"
|
||||
|
||||
export interface PledgeData {
|
||||
amountPence: number
|
||||
@@ -30,18 +29,15 @@ interface EventInfo {
|
||||
qrSourceLabel: string | null
|
||||
}
|
||||
|
||||
// Step indices:
|
||||
// 0 = Amount selection
|
||||
// 1 = Payment method selection
|
||||
// Steps:
|
||||
// 0 = Amount
|
||||
// 1 = Payment method
|
||||
// 2 = Identity (for bank transfer)
|
||||
// 3 = Bank instructions
|
||||
// 4 = Confirmation (generic — card, DD, FPX)
|
||||
// 4 = Confirmation (card, DD)
|
||||
// 5 = Card payment step
|
||||
// 6 = FPX payment step
|
||||
// 7 = Direct Debit step
|
||||
|
||||
const STEP_TO_RAIL: Record<number, number> = { 5: 1, 6: 1, 7: 1 } // maps back to payment selection
|
||||
|
||||
export default function PledgePage() {
|
||||
const params = useParams()
|
||||
const token = params.token as string
|
||||
@@ -80,7 +76,6 @@ export default function PledgePage() {
|
||||
setError("Unable to load pledge page")
|
||||
setLoading(false)
|
||||
})
|
||||
// Track pledge_start
|
||||
fetch("/api/analytics", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -96,10 +91,9 @@ export default function PledgePage() {
|
||||
const handleRailSelected = (rail: Rail) => {
|
||||
setPledgeData((d) => ({ ...d, rail }))
|
||||
const railStepMap: Record<Rail, number> = {
|
||||
bank: 2, // → identity step → bank instructions
|
||||
card: 5, // → card payment step (combined identity + card)
|
||||
fpx: 6, // → FPX step (bank selection + identity + redirect)
|
||||
gocardless: 7, // → direct debit step (bank details + mandate)
|
||||
bank: 2,
|
||||
card: 5,
|
||||
gocardless: 7,
|
||||
}
|
||||
setStep(railStepMap[rail])
|
||||
}
|
||||
@@ -119,12 +113,8 @@ export default function PledgePage() {
|
||||
}),
|
||||
})
|
||||
const result = await res.json()
|
||||
if (result.error) {
|
||||
setError(result.error)
|
||||
return
|
||||
}
|
||||
if (result.error) { setError(result.error); return }
|
||||
setPledgeResult(result)
|
||||
// Bank rail shows bank instructions; everything else shows generic confirmation
|
||||
setStep(finalData.rail === "bank" ? 3 : 4)
|
||||
} catch {
|
||||
setError("Something went wrong. Please try again.")
|
||||
@@ -151,50 +141,38 @@ export default function PledgePage() {
|
||||
)
|
||||
}
|
||||
|
||||
const shareUrl = eventInfo?.qrSourceId ? `${typeof window !== "undefined" ? window.location.origin : ""}/p/${token}` : undefined
|
||||
|
||||
const steps: Record<number, React.ReactNode> = {
|
||||
0: <AmountStep onSelect={handleAmountSelected} eventName={eventInfo?.name || ""} />,
|
||||
1: <PaymentStep onSelect={handleRailSelected} amount={pledgeData.amountPence} />,
|
||||
2: <IdentityStep onSubmit={submitPledge} />,
|
||||
2: <IdentityStep onSubmit={submitPledge} amount={pledgeData.amountPence} />,
|
||||
3: pledgeResult && <BankInstructionsStep pledge={pledgeResult} amount={pledgeData.amountPence} eventName={eventInfo?.name || ""} />,
|
||||
4: pledgeResult && <ConfirmationStep pledge={pledgeResult} amount={pledgeData.amountPence} rail={pledgeData.rail} eventName={eventInfo?.name || ""} />,
|
||||
4: pledgeResult && <ConfirmationStep pledge={pledgeResult} amount={pledgeData.amountPence} rail={pledgeData.rail} eventName={eventInfo?.name || ""} shareUrl={shareUrl} />,
|
||||
5: <CardPaymentStep amount={pledgeData.amountPence} eventName={eventInfo?.name || ""} eventId={eventInfo?.id || ""} qrSourceId={eventInfo?.qrSourceId || null} onComplete={submitPledge} />,
|
||||
6: <FpxPaymentStep amount={pledgeData.amountPence} eventName={eventInfo?.name || ""} onComplete={submitPledge} />,
|
||||
7: <DirectDebitStep amount={pledgeData.amountPence} eventName={eventInfo?.name || ""} organizationName={eventInfo?.organizationName || ""} eventId={eventInfo?.id || ""} qrSourceId={eventInfo?.qrSourceId || null} onComplete={submitPledge} />,
|
||||
}
|
||||
|
||||
// Determine which steps allow back navigation
|
||||
const backableSteps = new Set([1, 2, 5, 6, 7])
|
||||
const getBackStep = (current: number): number => {
|
||||
if (current in STEP_TO_RAIL) return STEP_TO_RAIL[current] // rail-specific steps → payment selection
|
||||
return current - 1
|
||||
const backableSteps = new Set([1, 2, 5, 7])
|
||||
const getBackStep = (s: number): number => {
|
||||
if (s === 5 || s === 7) return 1
|
||||
return s - 1
|
||||
}
|
||||
|
||||
// Progress calculation: steps 0-2 map linearly, 3+ means done
|
||||
const progressSteps = step >= 3 ? 3 : Math.min(step, 2) + 1
|
||||
const progressPercent = step >= 5 ? 66 : (progressSteps / 3) * 100 // rail steps show 2/3 progress
|
||||
const progressPercent = step >= 3 ? 100 : step >= 2 ? 66 : step >= 1 ? 33 : 10
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-trust-blue/5 via-white to-warm-amber/5">
|
||||
{/* Progress bar */}
|
||||
<div className="fixed top-0 left-0 right-0 h-1 bg-gray-100 z-50">
|
||||
<div
|
||||
className="h-full bg-trust-blue transition-all duration-500 ease-out"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
<div className="h-full bg-trust-blue transition-all duration-500 ease-out" style={{ width: `${progressPercent}%` }} />
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="pt-6 pb-2 px-4 text-center">
|
||||
<p className="text-sm text-muted-foreground">{eventInfo?.organizationName}</p>
|
||||
<p className="text-xs text-muted-foreground/60">{eventInfo?.qrSourceLabel || ""}</p>
|
||||
</div>
|
||||
|
||||
{/* Step content */}
|
||||
<div className="px-4 pb-8">
|
||||
{steps[step]}
|
||||
</div>
|
||||
<div className="px-4 pb-8">{steps[step]}</div>
|
||||
|
||||
{/* Back button */}
|
||||
{backableSteps.has(step) && (
|
||||
<div className="fixed bottom-6 left-4">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user