feat: premium UI overhaul, AI suggestions, WAHA WhatsApp integration

PREMIUM UI:
- All animations: fade-up, scale-in, stagger children, confetti celebration
- Glass effects, gradient icons, premium card hover states
- Custom CSS: shimmer, pulse-ring, bounce, counter-roll animations
- Smooth progress bar with gradient

AI-POWERED (GPT-4o-mini nano model):
- Smart amount suggestions based on peer data (/api/ai/suggest)
- Social proof: '42 people pledged · Average £85'
- AI-generated nudge text for conversion
- AI fuzzy matching for bank reconciliation
- AI reminder message generation

WAHA WHATSAPP INTEGRATION:
- Auto-send pledge receipt with bank details via WhatsApp
- 4-step reminder sequence: gentle → nudge → urgent → final
- Chatbot: donors reply PAID, HELP, CANCEL, STATUS
- Volunteer notification on new pledges
- WhatsApp status in dashboard settings
- Webhook endpoint for incoming messages

DONOR FLOW (CRO):
- Amount step: AI suggestions, Gift Aid preview, social proof, haptic feedback
- Payment step: trust signals, fee comparison, benefit badges
- Identity step: email/phone toggle, WhatsApp reminder indicator
- Bank instructions: tap-to-copy each field, WhatsApp delivery confirmation
- Confirmation: confetti, pulse animation, share CTA, WhatsApp receipt

COMPOSE:
- Added WAHA env vars + qc-comms network for WhatsApp access
This commit is contained in:
2026-03-03 04:31:07 +08:00
parent 0236867c88
commit c6e7e4f01e
15 changed files with 1473 additions and 383 deletions

View File

@@ -0,0 +1,109 @@
import { NextRequest, NextResponse } from "next/server"
import prisma from "@/lib/prisma"
import { sendWhatsAppMessage } from "@/lib/whatsapp"
/**
* WAHA webhook — receives incoming WhatsApp messages
* Handles: PAID, HELP, CANCEL commands from donors
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { payload } = body
// WAHA sends message events
if (!payload?.body || !payload?.from) {
return NextResponse.json({ ok: true })
}
const text = payload.body.trim().toUpperCase()
const fromPhone = payload.from.replace("@c.us", "")
// Only handle known commands
if (!["PAID", "HELP", "CANCEL", "STATUS"].includes(text)) {
return NextResponse.json({ ok: true })
}
if (!prisma) return NextResponse.json({ ok: true })
// Find pledges by this phone number
// Normalize: try with and without country code
const phoneVariants = [fromPhone]
if (fromPhone.startsWith("44")) {
phoneVariants.push("0" + fromPhone.slice(2))
phoneVariants.push("+" + fromPhone)
}
const pledges = await prisma.pledge.findMany({
where: {
donorPhone: { in: phoneVariants },
status: { in: ["new", "initiated"] },
},
include: {
event: { select: { name: true } },
paymentInstruction: true,
},
orderBy: { createdAt: "desc" },
take: 5,
})
if (pledges.length === 0) {
await sendWhatsAppMessage(fromPhone, `We couldn't find any pending pledges for this number. If you need help, please contact the charity directly.`)
return NextResponse.json({ ok: true })
}
const pledge = pledges[0] // Most recent
const amount = (pledge.amountPence / 100).toFixed(0)
switch (text) {
case "PAID": {
await prisma.pledge.update({
where: { id: pledge.id },
data: { status: "initiated", iPaidClickedAt: new Date() },
})
await sendWhatsAppMessage(fromPhone,
`✅ Thanks! We've noted that you've paid your *£${amount}* pledge to ${pledge.event.name}.\n\nWe'll confirm once the payment is matched. Ref: \`${pledge.reference}\``
)
break
}
case "HELP": {
const bankDetails = pledge.paymentInstruction?.bankDetails as Record<string, string> | null
if (bankDetails) {
await sendWhatsAppMessage(fromPhone,
`🏦 *Bank Details for your £${amount} pledge:*\n\nSort Code: \`${bankDetails.sortCode}\`\nAccount: \`${bankDetails.accountNo}\`\nName: ${bankDetails.accountName}\nReference: \`${pledge.reference}\`\n\n⚠ _Use the exact reference above_`
)
} else {
await sendWhatsAppMessage(fromPhone,
`Your pledge ref is \`${pledge.reference}\` for £${amount} to ${pledge.event.name}.\n\nContact the charity for payment details.`
)
}
break
}
case "CANCEL": {
await prisma.pledge.update({
where: { id: pledge.id },
data: { status: "cancelled", cancelledAt: new Date() },
})
await sendWhatsAppMessage(fromPhone,
`Your £${amount} pledge to ${pledge.event.name} has been cancelled. No worries — thank you for considering! 🙏`
)
break
}
case "STATUS": {
const statusText = pledges.map(p =>
`• £${(p.amountPence / 100).toFixed(0)}${p.event.name} (${p.status}) [${p.reference}]`
).join("\n")
await sendWhatsAppMessage(fromPhone, `📋 *Your pledges:*\n\n${statusText}`)
break
}
}
return NextResponse.json({ ok: true })
} catch (error) {
console.error("WhatsApp webhook error:", error)
return NextResponse.json({ ok: true })
}
}