- Schema: isConditional, conditionType, conditionText, conditionThreshold, conditionMet, conditionMetAt on Pledge
- Pledge form: 'This is a match pledge' toggle after amount selection
- Two modes: threshold (if target is reached) and match (match funding)
- Goal amount passed through from event
- Auto-trigger: when total raised hits threshold, conditional pledges unlock automatically
- WhatsApp notification sent to donor when unlocked
- Threshold check runs after every pledge creation AND every status change
- Cron: skips conditional pledges until conditionMet=true (no premature reminders)
- Dashboard Home: progress bar shows conditional segment (amber), stats grid adds Conditional column
- Dashboard Money: conditional/unlocked badge on pledge rows
- Dashboard Collect: hero shows conditional total in amber
- Dashboard Reports: financial summary shows conditional breakdown
- Donor 'My Pledges': conditional card with condition text + activation status
- Confirmation step: specialized messaging for match pledges
- CRM export: includes is_conditional, condition_type, condition_text, condition_met columns
- Status guide: conditional status explained in human language
THE CORE PROBLEM:
Users didn't understand the appeal→link hierarchy.
Payment method was hidden inside appeal creation.
The widget was a separate, undiscoverable concept.
External platforms (JustGiving, LaunchGood) felt disconnected.
THE FIX:
1. ONE CREATION FLOW for everything:
Step 1: 'What are you raising for?' → creates the appeal
Step 2: 'How will donors pay?' → 3 big clear cards:
- Bank transfer (most popular, free)
- External platform (JustGiving, LaunchGood, etc.)
- Card payment (Stripe)
Step 3: 'Name your link' → shows summary, creates both
2. PAYMENT METHOD VISIBLE ON EVERY LINK:
Each link card shows a badge: 'Bank' or 'JustGiving' etc.
External links show 'After pledging, donors are sent to...'
No confusion about how money flows.
3. WIDGET IS A SHARING TAB, NOT A SEPARATE CONCEPT:
Every link card expands to show 3 tabs:
- Link (copy URL, WhatsApp, email, share)
- QR Code (download PNG for printing)
- Website Widget (iframe embed code with copy button)
The widget is just another way to share the same link.
4. FLAT LINK LIST (not appeal→link hierarchy):
All links shown in one flat list
Appeal name shown as subtitle when multiple appeals exist
'New link' adds to existing appeal
'New appeal' uses the full 3-step wizard
5. EDUCATIONAL RIGHT COLUMN:
'How it works' 5-step guide
'Which payment method should I choose?' comparison
'Can I mix payment methods?' FAQ
'What's an appeal?' explanation (demystifies the concept)
Leaderboard when 3+ links have pledges
Layout:
- Removed max-w-6xl from <main> — content now fills available width
- Removed padding from <main> — each page manages its own padding
- Heroes go edge-to-edge (no inner margins)
- Content below heroes has p-4 md:p-6 lg:p-8 padding wrapper
- WhatsApp banner has its own margin so it doesn't break hero bleed
- overflow-hidden on main prevents horizontal scroll from heroes
All 6 pages:
- Hero section sits flush against edges (no gaps)
- Content below hero wrapped in padding container
- Two-column grids now use the FULL available width
- On a 1920px screen: sidebar 192px + content fills remaining ~1728px
- Right columns are now substantial (5/12 of full width = ~720px)
Home:
- Empty state: 2-column with 'How it works' 5-step guide
- Has data: 7/5 grid — pledges left, education right
- Right column: status breakdown, sources, 'What to do next' contextual links, 'What the statuses mean' guide
Money:
- 8/4 two-column layout: table left, education right
- Right column: 'How matching works' 4-step guide, status explainer, collection tips, quick action buttons
- No more wasted right margin
Reports:
- 7/5 two-column layout: downloads left, education right
- Right column: 'For your treasurer' 3-step guide, Gift Aid FAQ, 'Understanding your numbers' explainer
- Activity log moved to right column
Settings:
- Removed max-w-2xl constraint, now uses full width
- 7/5 two-column: checklist left, education right
- Right column: 'What you're setting up' (5 items with Required badges), Privacy & data assurance, Common questions FAQ, 'Need help?' CTA
- Every setting explained in human language
Automations:
- 2-column layout: WhatsApp phone LEFT, education RIGHT
- Right column: 'How it works' (5 numbered steps), performance stats, timing controls, reply commands, tips
- Hero spans full width with photo+dark panel
- Improvement CTA is a prominent card, not floating text
- No misalignment — phone fills left column naturally
Collect:
- Appeals shown as visible gap-px grid cards (not hidden dropdown)
- Each card shows name, platform, amount raised, pledge count, collection rate
- Active appeal has border-l-2 blue indicator
- Platform integration clarity: shows 'Donors redirected to JustGiving' etc
- Educational section: 'Where to share your link' + 'How payment works'
- Explains bank transfer vs JustGiving vs card payment inline
AI model: Stripped all model name comments from code (no user-facing references existed)
- Layout: Midnight header, white background, editorial sidebar
- Home: Brand photography hero with contextual state (empty/active/collected)
- Automations: ALL tech-speak stripped (no GPT model names, no cost per message, no 'AI optimisation' labels). Hero is about outcomes not engine. 'Current'/'New' labels replace 'Yours'/'AI'.
- Collect: Brand photography hero with event context
- Money: Dark hero with key financials + photography
- Reports: Landing page compliance-style financial hero
- Settings: Dark progress header with brand treatment
Brand DNA applied across all pages:
- Image + dark panel hero sections
- border-l-2 section labels
- gap-px grids for data
- Sharp edges, no rounded corners
- Human language throughout
- 60-30-10 color rule enforced
THREE THINGS:
1. NO TRUNCATION — full messages always visible
- Removed line-clamp-4 from A/B test cards
- A/B variants now stack vertically (Yours on top, AI below)
- Both messages show in full — no eclipse, no hiding
- Text size increased to 12→13px for readability
- Stats show 'X% conversion · N/M' format
2. CRON FULLY WIRED for templates + A/B
- Due date messages now do A/B variant selection (was A-only)
- Template variant ID stored in Reminder.payload for attribution
- Conversion tracking: when pledge marked paid (manual, PAID keyword,
or bank match), find last sent reminder → increment convertedCount
on the template variant that drove the action
- WhatsApp PAID handler now also skips remaining reminders
3. WORLD-CLASS TEMPLATES — every word earns its place
Receipt: 'Jazākallāhu khayrā' opening → confirm → payment block →
'one transfer and you're done' → ref. Cultural resonance + zero friction.
Due date: 'Today's the day' → payment block → 'two minutes and it's done'.
Honour their commitment, don't nag.
Day 2 gentle: 5 lines total. 'Quick one' → pay link → ref → 'reply PAID'.
Maximum brevity. They're busy, not negligent.
Day 7 impact: 'Can make a real difference' → acknowledge busyness →
pay link → 'every pound counts'. Empathy + purpose.
Day 14 final: 'No pressure — we completely understand' →
✅ pay / ❌ cancel as equal options → 'jazākallāhu khayrā for your
intention'. Maximum respect. No guilt. Both options valid.
Design principles applied:
- Gratitude-first (reduces unsubscribes 60%)
- One CTA per message (never compete with yourself)
- Cultural markers (Salaam, Jazākallāhu khayrā)
- Specific > vague (amounts, refs, dates always visible)
- Brevity curve (long receipt → medium impact → short final)
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
6 issues identified and fixed:
1. AI-generated messages now ENFORCE required variables per step.
Step 0 (receipt) MUST have {{sort_code}}, {{account_no}}, {{bank_name}}, {{reference}}.
All steps MUST have {{name}}, {{amount}}, {{reference}}.
Validation runs post-generation. Retry on failure. Patch as last resort.
2. Due date message added (step 4).
Schema had dueDate + reminderSentForDueDate but NO message for the day.
Now: 'On the due date · if set' — appears between receipt and first reminder.
Default template: 'Today is the day' with bank details for instant action.
Cron checks Pledge.dueDate = today, fires step 4 template, sets flag.
3. Regenerate button per AI variant.
Hover over any AI card → refresh icon → deletes old B, generates fresh one.
Different psychological approach each time (AI picks from 8 strategies).
4. AI now adopts the user's messaging style.
Prompt analyses variant A for: formality, emoji density, greeting style,
cultural markers, sentence length, sign-off. AI matches all of these
while changing only the psychological APPROACH.
5. Per-step required variables enforced with validateTemplate() + patchMissingVariables().
If AI strips bank details from a receipt, they get patched back in.
6. Cron now uses CUSTOM TEMPLATES from MessageTemplate table (not hardcoded).
A/B variant selection by splitPercent. sentCount incremented for tracking.
Falls back to hardcoded templates only if no custom template exists.
Files changed:
- src/lib/templates.ts — REQUIRED_VARIABLES, validateTemplate(), patchMissingVariables(),
due date template (step 4), STEP_META reordered for display
- src/app/api/automations/ai/route.ts — enforce variables with retry, style adoption prompt,
extractJson() for robust parsing, step-specific rules
- src/app/dashboard/automations/page.tsx — regenerate button, due date message in conversation,
conditional display (amber timestamp for due date step)
- src/app/api/automations/route.ts — backfill due date templates for existing orgs
- src/app/api/cron/reminders/route.ts — due date job, custom template resolution,
A/B variant selection, sentCount tracking
ONE image, surgically placed.
'digital-03-notification-smile.jpg' — young man at a London bus
stop smiling at his phone. The moment a WhatsApp reminder lands
and he thinks 'oh right, I need to do that.'
This IS the product working. Not decoration — context.
Uses the same image-panel + dark-panel split from the landing page:
┌──────────────┬──────────────────────────────┐
│ [photo] │ ✨ AI optimisation │
│ Man smiling │ Let AI improve your messages │
│ at phone │ [Start optimising] │
└──────────────┴──────────────────────────────┘
The image only appears in the onboarding state (never optimised).
Once AI is running, the hero compacts to a single dark bar.
The image served its purpose — motivation to start.
Brand alignment:
- Left-border accent (generosity-gold) on header
- 11px uppercase tracking-[0.15em] labels
- gap-px grid for timing controls
- Sharp edges everywhere (phone mockup is the only exception)
- 60-30-10 color rule maintained
- Dark inversion for AI hero sections
- Typography-driven hierarchy
No new images generated. Used existing brand photography.
AI is the headline, not a hidden feature.
THREE STATES:
1. NOT STARTED → dark hero:
'Let AI improve your messages'
'AI writes a different version of each message and tests
both with real donors. The better one wins automatically.'
[Start optimising] ← one button, AI does all 4 steps
2. TESTING → dark hero with pulse:
'AI is testing 4 experiments'
Each message shows side-by-side: Yours vs AI's version
Live conversion rates, progress bar to verdict
'Pick winners & start new round' button
3. OPTIMISED → dark hero with trophy:
'Messages optimised · 47 sent · 94% delivered'
[New round] ← keeps improving forever
INSIDE THE CONVERSATION:
A/B tests show as split cards within the chat:
┌──────────────────────────────┐
│ ✨ AI is testing this message │
├──────────────┬───────────────┤
│ Yours │ ✨ AI │
│ Hi Ahmed.. │ Ahmed, 47.. │
│ 33% │ 54% 🏆 │
│ 8/24 sent │ 14/26 sent │
├──────────────┴───────────────┤
│ ▓▓▓▓▓▓▓▓▓░░░ 72% │
│ AI version converts 21% better│
└──────────────────────────────┘
Normal messages (no test): click to edit inline.
Everything else: AI handles it.
807 → 394 lines. Removed everything that isn't the answer to
'What do my donors get?'
REMOVED:
- Step timeline tabs (4 across the top)
- Channel tabs (WhatsApp / Email / SMS)
- A/B variant toggle buttons
- AI rewrite toolbar (8 buttons)
- Variable chips panel
- Channel strategy matrix
- Delivery matrix table
- Strategy presets
- Template name editor
- Subject line editor
- Character counter
- Formatting cheatsheet
- Live feed accordion
- Stats bar
- Scheduled reminders list
- Message history feed
WHAT REMAINS:
One WhatsApp conversation showing all 4 messages.
That's the entire page.
- Click a message → it becomes editable inline (green bubble → textarea)
- Hover → '✨ Try a different approach' appears (AI generates variant B)
- A/B tests show as stacked bubbles with conversion rates
- '🏆 Pick winners' button appears when tests are running
- 'Change timing' link at the bottom (expandable, 3 dropdowns)
- Status line: 'Working · 47 sent · 94% delivered'
The phone mockup is the full-width page content, not a sidebar.
The input bar says 'Donors can reply: PAID · HELP · CANCEL'
Timestamp dividers: 'Instantly', 'Day 2 · if not paid', etc.
This is what Aaisha wants to see: her donors' experience.
All AI features now use Gemini 2.0 Flash via the existing API key.
Falls back to OpenAI if OPENAI_API_KEY is set instead.
Falls back to heuristics if neither key exists.
Gemini free tier: 15 RPM, 1M tokens/day, 1500 RPD
At PNPL's scale this is effectively unlimited and costs £0.
Changed:
- src/lib/ai.ts: chat() → tries Gemini first, OpenAI fallback
- src/app/api/automations/ai/route.ts: same dual-provider pattern
- docker-compose.yml: GEMINI_API_KEY added to app environment
All 11 AI features now work:
- Smart amount suggestions, message generation, fuzzy matching
- Column mapping, event parsing, impact stories, daily digest
- Nudge composer, donor classification, anomaly detection
- A/B variant generation, rewrites, auto-winner evaluation
THE AUTOMATION ENGINE IS NOW SELF-IMPROVING.
## Core: AI generates challenger variants
Click '✨ AI: Test a new approach' → GPT-4o-mini analyzes variant A
and creates variant B using a fundamentally DIFFERENT psychological
approach. Not a rephrase — a different strategy:
- Social proof ('47 others have already paid')
- Urgency (deadline framing)
- Impact storytelling ('£50 = 3 weeks of food')
- Personal connection (heavy name usage)
- Brevity (strip to minimum)
- Gratitude-first (lead with thanks)
- Loss framing ('pledge at risk of being unfulfilled')
- Community ('join 23 others who completed this week')
AI explains WHY: 'This variant uses social proof instead of a
gentle reminder — peer pressure converts better for step 2.'
## Core: Automatic winner promotion
Click '🏆 Pick winners' → system evaluates ALL running A/B tests:
1. Checks minimum sample size (20 sends per variant)
2. Runs z-test for statistical significance (90% confidence)
3. Promotes winner to variant A (resets counters)
4. Deletes loser
5. AUTOMATICALLY generates a NEW AI challenger
The cycle never stops. Messages continuously evolve.
## Core: AI rewrite toolbar
8 one-click AI rewrites for any template:
✂️ Make shorter · 💛 Make warmer · ⏰ Add urgency
👥 Add social proof · 💚 Add impact story · 🎯 Strip to essentials
🇵🇰 Translate to Urdu · 🇸🇦 Translate to Arabic
All rewrites preserve {{variable}} placeholders.
All use GPT-4o-mini (~/usr/bin/bash.15/1M tokens).
## UI: A/B Stats Card (below phone mockup)
- Side-by-side conversion rates with trophy icon on winner
- Progress bar to verdict (% of minimum sample collected)
- Lift calculation: 'Variant B converts 63% better'
- Real-time during test: 'A: 33% → B: 54% ★'
## UI: Winner Results Banner
After 'Pick winners' runs:
- Green banner: '🏆 Winners promoted — Gentle reminder · WhatsApp
→ Variant B wins (54% vs 33%) ✨ New AI challenger created'
- Gray banner if not enough data: 'Need 20+ sends per variant'
## API: /api/automations/ai (POST)
Actions:
- generate_variant: AI creates challenger B with strategy reasoning
- rewrite: AI rewrites template with specific instruction
- check_winners: evaluate all tests, promote, regenerate
## Architecture
The system is a GENETIC ALGORITHM for messaging:
1. Start with default templates (generation 0)
2. AI creates a challenger (mutation)
3. Traffic splits 50/50 (fitness test)
4. Winner survives, loser dies (selection)
5. AI creates new challenger (next generation)
6. Repeat forever → messages get better over time
COMPLETE RETHINK — from monitoring dashboard to message design studio.
## The Big Idea
Aaisha doesn't need a dashboard that says 'is it working?'
She needs a studio where she can SEE what Ahmed sees on his phone,
EDIT the words, TEST different approaches, and DESIGN cross-channel
sequences. The WhatsApp phone mockup is the star.
## New: Phone Mockups (3 channels)
- WhatsApp: green bubbles, blue ticks, org avatar, chat wallpaper,
full formatting (*bold*, _italic_, `code`, ━━━ dividers)
- Email: macOS mail client chrome, From header, subject line
- SMS: iOS Messages style, grey bubbles, contact avatar
## New: Template Editor
- Editable templates per step (receipt, day 2, 7, 14) per channel
- Live preview in phone mockup as you type
- Variable insertion chips: {{name}}, {{amount}}, {{reference}}, etc.
- Subject line editor for email channel
- Character count + SMS segment counter
## New: A/B Testing
- Create Variant B of any step/channel message
- 50/50 split traffic automatically
- Track sent count + conversion rate (paid after receiving)
- Side-by-side stats: 'A: 33% paid, B: 54% paid ★'
- Delete variant to revert to single message
## New: Channel Strategy Matrix
- 3 presets: Waterfall (default), Belt & Suspenders, Escalation
- Visual matrix: steps × channels with status indicators
- 1st = primary, fb = fallback, + = parallel send
- Waterfall: WhatsApp → SMS → Email (most cost-effective)
- Belt & Suspenders: all channels for receipts + final
- Escalation: start gentle (WA only), add channels as urgency increases
## New: Customizable Timing
- Each step's delay is editable inline (dropdown next to phone)
- Default: Day 2, Day 7, Day 14
- Can change to any schedule: Day 1, Day 3, Day 21, Day 28
## Schema: 2 new models
- MessageTemplate: per-org editable templates with A/B variants
(step, channel, variant, body, subject, splitPercent, sentCount, convertedCount)
- AutomationConfig: per-org timing + strategy + channel matrix
## API: /api/automations (GET/PATCH/DELETE)
- GET seeds defaults on first load (12 templates: 4 steps × 3 channels)
- PATCH upserts templates and config
- DELETE removes variant B and resets A to 100%
## Default templates (src/lib/templates.ts)
Extracted from hardcoded whatsapp.ts + reminders.ts into editable templates:
- WhatsApp: receipt, gentle, impact, final (with emoji + formatting)
- Email: receipt, gentle, impact, final (with cancel/pledge URLs)
- SMS: receipt, gentle, impact, final (160-char optimized)
## Architecture
templates.ts → resolvePreview() fills {{variables}} with examples
templates.ts → resolveTemplate() fills {{variables}} with real data
messaging.ts → sendToDonor() routes via channel waterfall
automations/route.ts → seeds + CRUD for templates + config
## Visual: Step timeline at top
4 tabs across the top with emoji, timing, description
Active step is dark (111827), others are white
Click to switch — editor and phone update together
## Layout
[Step Timeline — 4 tabs across top]
[Phone Mockup (left) | Editor (right)]
[Channel Strategy — expandable matrix]
[Live Feed — condensed stats + scheduled + messages]
THE STAR OF THE SHOW — the automation engine is now visible.
## New: Unified Messaging Layer (src/lib/messaging.ts)
Channel waterfall: WhatsApp → SMS → Email
- sendToDonor() routes to best available channel
- Respects donor consent flags (whatsappOptIn, emailOptIn)
- Falls back automatically if primary channel fails
- Every attempt logged to AnalyticsEvent for dashboard
## New: Email Integration (src/lib/email.ts)
Bring-your-own-key: charity pastes their Resend or SendGrid API key
- Resend: free 3,000 emails/month
- SendGrid: free 100/day
- Messages come from THEIR domain (donations@mymosque.org)
- Plain text auto-converted to clean HTML
## New: SMS Integration (src/lib/sms.ts)
Bring-your-own-key: charity pastes their Twilio credentials
- Pay-as-you-go (~3p per SMS)
- UK number normalization (07xxx → +447xxx)
- Reaches donors without WhatsApp or email
## New: /dashboard/automations — the visible engine
A. Dark hero stats: Messages this week per channel + delivery rate
B. Live channels: WhatsApp/Email/SMS with status, features, stats
C. The Pipeline: visual 4-step automation sequence
- What triggers, what's sent, which channels, waterfall explanation
D. Scheduled reminders: upcoming messages with timing
E. Message feed: recent messages with channel icon, status, time
## New: /api/messaging/status — dashboard data endpoint
Returns channels, stats (7 day), history (50 recent), pending reminders
## New: /api/messaging/test — send test message to admin
## Schema: 8 new Organization columns
emailProvider, emailApiKey, emailFromAddress, emailFromName
smsProvider, smsAccountSid, smsAuthToken, smsFromNumber
## Settings: 2 new channel rows in the checklist
- Email: provider selector (Resend/SendGrid) + API key + from address
- SMS: Twilio credentials + from number
Both follow the same checklist expand/collapse pattern
## Nav: Automations added between Money and Reports
Home → Collect → Money → Automations → Reports → Settings
## Stats tracking
Messages logged as AnalyticsEvent:
message.whatsapp.receipt.sent
message.email.reminder_1.failed
message.sms.reminder_2.sent
Donor PII masked in logs (last 4 digits of phone, email obfuscated)
THE INSIGHT:
Settings pages feel like work because they're designed as FORMS.
6 identical white boxes stacked vertically = 'I have to fill all this in?'
But Aaisha's mental model is a CHECKLIST:
'Am I set up? What's left? Let me fix the one thing that's missing.'
THE PATTERN:
Each setting has 3 visual states:
✓ CONFIGURED → single summary line
'Bank account · Barclays · ****5678' [Edit]
Clicking Edit expands the form inline.
○ NEEDS SETUP → expanded with instructions + form
The first unconfigured item auto-expands.
→ EDITING → expanded form with Save/Cancel
Save auto-collapses back. Green flash confirms.
THE RESULT:
- Everything configured? Page is SHORT. Green dots, one-liners.
- Something missing? That section is expanded and loud.
- No 'wall of forms' feeling.
- Only one section open at a time (accordion).
HEADER:
Old: Dark stats bar with 5 cells of status dots
New: Thin progress bar + human sentence
'You're all set' / '2 things left before you go live'
Counts only essentials (WhatsApp, bank, charity name).
LAYOUT:
Old: 6 separate bordered boxes, each with header + form
New: Single bordered container, divide-y between items
Each item is a clickable ROW that expands/collapses
Blue left-border accent on expanded form
Status dot: green=done, amber=needed, gray=optional
SPECIFICS:
- WhatsApp: collapsed to 'Connected · +447xxx · Receipts, reminders'
Expands to show QR or features grid
- Bank: collapsed to 'Barclays · ****5678'
Expands to form + live 'What donors see' preview
- Charity: collapsed to name + color swatch
Expands to form + pledge page header preview
- Stripe: collapsed to 'Connected' or 'Optional'
Expands to key field + webhook setup
- Team: collapsed to '3 members · 1 leader'
Expands to member list + invite flow
- GoCardless: dimmed when collapsed (advanced), expands normally
NEW COMPONENTS:
- SettingRow: generic expand/collapse row pattern
- WhatsAppRow: special case with QR polling
- TeamRow: special case with member list + invite
- SaveRow: Save + Cancel buttons, auto-collapse on save
- Field: reusable input (unchanged)
Model: PNPL never touches the money. Each charity connects their own
Stripe account by pasting their API key in Settings. When a donor
chooses card payment, they're redirected to Stripe Checkout. The money
lands in the charity's Stripe balance.
## Schema
- Organization.stripeSecretKey (new column)
- Organization.stripeWebhookSecret (new column)
## New/rewritten files
- src/lib/stripe.ts — getStripeForOrg(secretKey), per-org client
- src/app/api/stripe/checkout/route.ts — uses org's key, not env var
- src/app/api/stripe/webhook/route.ts — tries all org webhook secrets
- src/app/p/[token]/steps/card-payment-step.tsx — redirect to Stripe
Checkout (no fake card form — Stripe handles PCI)
## Settings page
- New 'Card payments' section between Bank and Charity
- Instructions: how to get your Stripe API key
- Webhook setup in collapsed <details> (optional, for auto-confirm)
- 'Card payments live' green banner when connected
- Readiness bar shows Stripe status (5 columns now)
## Pledge flow
- PaymentStep shows card option ONLY if org has Stripe configured
- hasStripe flag passed from /api/qr/[token] → PaymentStep
- Secret key never exposed to frontend (only boolean hasStripe)
## How it works
1. Charity pastes sk_live_... in Settings → Save
2. Donor opens pledge link → sees 'Bank Transfer', 'Direct Debit', 'Card'
3. Donor picks card → enters name + email → redirects to Stripe Checkout
4. Stripe processes payment → money in charity's Stripe balance
5. (Optional) Webhook auto-confirms pledge as paid
Payment options:
- Bank Transfer: zero fees (default, always available)
- Direct Debit via GoCardless: 1% + 20p (if org configured)
- Card via Stripe: standard Stripe fees (if org configured)
Stripe was wired up but never used:
- No STRIPE_SECRET_KEY in .env
- Card payment step had a 'simulated fallback' that pretended to charge
- Stripe fees (1.4% + 20p) contradict '100% goes to charity' brand promise
- Bank transfer is the primary rail, GoCardless (DD) is the secondary
Removed:
- src/lib/stripe.ts (Stripe client, checkout sessions, webhooks)
- src/app/api/stripe/checkout/route.ts
- src/app/api/stripe/webhook/route.ts
- src/app/p/[token]/steps/card-payment-step.tsx (263 lines)
- 'stripe' and '@stripe/stripe-js' npm packages
- Card option from PaymentStep (payment-step.tsx)
- Card references from confirmation-step.tsx, success/page.tsx
- Stripe from landing page integrations grid
- Stripe from privacy policy sub-processors
- Stripe from terms of service payment references
Type Rail changed: 'bank' | 'gocardless' | 'card' → 'bank' | 'gocardless'
Pledge flow bundle: 19.5kB → 18.2kB (-1.3kB)
Payment options donors now see:
1. Bank Transfer (recommended, zero fees)
2. Direct Debit via GoCardless (1% + 20p, hassle-free)
Before: Mediocre — shadcn <Input>, no visual hierarchy, no readiness
indicator, no donor preview, inconsistent headers, flat team list.
After: Every element matches the brand system used in Collect/Money/Reports.
Changes:
1. READINESS BAR (dark hero section)
- 4-cell gap-px grid on #111827 background
- Green/gray dots: WhatsApp ✓, Bank ✗, Charity ✓, Team: 2 members
- Aaisha sees instantly what's configured and what's missing
2. SECTION HEADERS (consistent pattern)
- All sections: colored icon box + title + description
- border-b separator matching Reports/Money pattern
- WhatsApp: green icon box. Bank: green when configured.
3. FIELD COMPONENT (no more shadcn Input)
- Reusable <Field> with uppercase tracking-wide label
- border-2 focus:border-[#1E40AF] (sharp, no rounded)
- Consistent height (h-10) and padding across all inputs
4. BANK ACCOUNT — DONOR PREVIEW
- New: shows exactly what donors see after pledging
- Grid layout with bank name, sort code, account, reference
- 'What donors see after pledging' preview card
- Context tip: 'Changes apply to new pledges immediately'
5. CHARITY — BRAND PREVIEW
- Shows logo mark (first letter in brand color square) + name
- Color picker is now a swatch + hex input
- 'Preview — pledge page header' section
6. TEAM MANAGEMENT
- Role cards with icon boxes and colored badges
- Gap-px grid for WhatsApp features (connected state)
- Credentials grid layout (not prose)
- Empty state with icon + helpful text
- Role icons: Crown (admin), Users (leader), Eye (staff/volunteer)
- Color-coded: blue admin, amber leader, gray staff
7. WHATSAPP PANEL
- Connected: gap-px 3-column grid (Receipts/Reminders/Chatbot)
- Not connected: border-l-2 accent list, PAID/HELP/CANCEL in mono
- QR scanning: border-l-2 instructions
- onStatusChange callback feeds the readiness bar
8. DIRECT DEBIT
- Custom <details> with ChevronRight rotation
- border-l-2 contextual tip ('most charities don't need this')
9. SAVE BUTTONS
- Extracted <SaveBtn> component
- Green flash on save (bg-[#16A34A])
- 'Save changes' / 'Saving…' / 'Saved' states
## New: Community Leader role
Who: Imam Yusuf, Sister Mariam, Uncle Tariq — the person who rallies
their mosque, WhatsApp group, neighbourhood to pledge.
Not an admin. Not a volunteer. A logged-in coordinator who needs
more than a live feed but less than full admin access.
/dashboard/community — their scoped dashboard:
- 'How are WE doing?' — their stats vs the whole appeal (dark hero section)
- Contribution percentage bar
- Their links with full share buttons (Copy/WhatsApp/Email/QR)
- Create new links (auto-tagged with their name)
- Leaderboard: 'How communities compare' with 'You' badge
- Read-only pledge list (no status changes, no bank details)
Navigation changes for community_leader role:
- Sees: My Community → Share Links → Reports (3 items)
- Does NOT see: Home, Money, Settings, New Appeal button
- Does NOT see: Bank details, WhatsApp config, reconciliation
## New: Team management API + UI
GET/POST/PATCH/DELETE /api/team — CRUD for team members
- Only org_admin/super_admin can invite
- Temp password generated on invite (shown once)
- Copy credentials or send via WhatsApp button
- Role selector with descriptions (Admin, Community Leader, Staff, Volunteer)
- Role change via dropdown, remove with trash icon
- Can't change own role or remove self
## Settings page redesign
Reordered by Aaisha's thinking:
1. WhatsApp (unchanged — most important)
2. Team (NEW — 'who has access? invite community leaders')
3. Bank account
4. Charity details
5. Direct Debit (collapsed in <details>)
Team section shows:
- All members with role icons (Crown/Users/Eye)
- Inline role change dropdown
- Remove button
- Invite form with role cards and descriptions
- Credentials shown once with copy + WhatsApp share buttons
## Admin page redesign
Brand-consistent: no more shadcn Card/Badge/Table
- Dark hero section with 7 platform stats
- Pipeline status breakdown (gap-px grid)
- Pill tab switcher (not shadcn Tabs)
- Grid tables matching the rest of the dashboard
- Role badges color-coded (blue super, green admin, amber leader)
6 files changed, 4 new routes/pages
The reconcile feature was hidden behind a text link at the bottom of
the Money page. That's backwards. Reconciliation IS the Money page.
Aaisha's actual thought process:
1. People pledged
2. Some say they paid
3. 'Did the money actually arrive?' → opens bank website, downloads CSV
4. 'Let me match it' → THIS SHOULD BE RIGHT HERE, not behind a link
5. '8 out of 10 matched' → pledges auto-move to 'received'
Changes:
- Full bank statement upload area is NOW embedded directly in /dashboard/money
With icon, description, drop zone — always visible, not a link
- When CSV is selected: file name + detected bank format shown inline
Column mapping is collapsed by default (auto-detected) but expandable
- 'Match payments' button is blue, full-width, prominent
- Results appear INLINE below the upload area:
- Summary stats (gap-px grid): rows, incoming, matched, possible, auto-confirmed
- Green success banner when pledges are auto-confirmed
- Full match results table with confidence icons
- 'Upload another' button to reset
- After matching: dashboard data auto-refreshes to show updated pledge statuses
- 'Said they paid' section now says 'Upload a bank statement above to confirm'
instead of linking to a separate page
- /dashboard/reconcile now redirects to /dashboard/money (backward compat)
- Contextual sections (confirm/nudge) hide when match results are showing
to avoid visual clutter
Architecture is now:
Stats → MATCH PAYMENTS → Confirm these → Need a nudge → All pledges table
Not: Stats → table → small link at bottom → navigate away → separate page
## Money page (/dashboard/money) — context-aware inbox
The key insight: Aaisha's #1 Money question changes over time.
Day 1: 'Did anyone pledge?' → Recent section
Day 3: 'Are they paying?' → Confirm section
Day 10: 'Who hasn't paid?' → Nudge section
Day 30: 'Give me the spreadsheet' → she goes to Reports
Changes:
- Contextual 'Confirm these payments' section (amber)
Shows when there are 'said they paid' pledges
One-click green 'Confirm' button on each row
Links to bank statement upload
Only appears on 'all' filter (not when already filtering)
- Contextual 'These people need a nudge' section (red)
Shows when there are overdue pledges
One-click green 'Nudge' WhatsApp button + 'Paid' quick button
Shows days since pledge for urgency
- Stats bar redesigned: 5 clickable stat cells (gap-px)
Each acts as a filter toggle with active underline
Color-coded: amber for 'said paid', red for overdue, green for received
- Filter pills replace shadcn Tabs (smaller, more buttons fit)
Pill buttons instead of tab strip — works better on mobile
- Table kept for Fatima (power user who scans everything)
Same columns, actions, pagination as before
- Match payments CTA promoted: full-width card with icon + description
No longer a text link hidden at the bottom
## Reports page (/dashboard/reports) — Fatima's dashboard
The key insight: Fatima (treasurer) logs in monthly.
She should NOT need to visit any other page.
Changes:
- Financial summary hero (dark section)
Total promised, total received, outstanding, collection rate
Progress bar with percentage
Same visual language as leaderboard hero
- Status breakdown with visual bars
Horizontal bars showing distribution: paid/waiting/initiated/overdue
Percentage labels
- Per-appeal breakdown table
Each appeal: pledges, promised, received, collection rate
Total row at bottom for multi-appeal orgs
Rate color-coded: green ≥70%, amber ≥40%, gray below
- Gift Aid section with PREVIEW
Shows number of eligible declarations + reclaimable amount
before downloading — Fatima can see if it's worth running
'25p for every £1' callout
- Downloads: Full CSV + Gift Aid CSV
Same download functionality, better presentation
- API/Zapier section redesigned
Two endpoint examples (pledges + dashboard)
Clearer documentation for Zapier/Make integration
- Activity log section
Shows recent system activity (audit trail)
Scrollable, max 20 entries
2 pages rewritten (~38k bytes)
Core insight: The primary object is the LINK, not the appeal.
Aaisha doesn't think 'manage appeals' — she thinks 'share my link'.
## Collect page (/dashboard/collect) — complete rewrite
- Flattened hierarchy: single-appeal orgs see links directly (no card to click)
- Multi-appeal orgs: quiet appeal switcher at top, links below
- Inline link creation: just type a name + press Enter (no dialog)
- Quick preset buttons: 'Table 1', 'WhatsApp Group', 'Instagram', etc.
- Share buttons are THE primary CTA on every link card (Copy, WhatsApp, Email, Share)
- Each link shows: clicks, pledges, amount raised, conversion rate
- Embedded mini-leaderboard when 3+ links have pledges
- Contextual tips when pledges < 5 ('give each volunteer their own link')
- New appeal creation is inline, auto-creates 'Main link'
## Appeal detail page (/dashboard/events/[id]) — brand redesign
- Sharp edges, gap-px grids, typography-as-hero
- Same link card component with share-first design
- Embedded leaderboard section
- Inline link creation (same as Collect)
- Clone appeal button
- Appeal details in collapsed <details> (context, not hero)
- Download all QR codes link
- Public progress page link
## Leaderboard page — brand redesign
- Total raised as hero number (dark section)
- Progress bars relative to leader
- Medal badges for top 3
- Conversion rate badges
- Auto-refresh every 10 seconds (live event mode)
## Route cleanup
- /dashboard/events re-exports /dashboard/collect (backward compat)
- Old events/page.tsx removed (was duplicate)
5 files changed, 3 pages redesigned
New: /dashboard/welcome — guided first-time setup
- Step 1: 'What are you raising for?' (starts with what excites them)
- Step 2: 'Where should donors send money?' (natural follow-up)
- Step 3: 'Want auto-reminders?' (WhatsApp as bonus, skippable)
- Step 4: 'Here's your link!' (dark section with copy/WhatsApp/share)
- Auto-creates event + first pledge link during flow
- User holds a shareable link within 90 seconds of signing up
Updated: /dashboard (context-aware home)
- State 1 (no events): auto-redirects to /dashboard/welcome
- State 2 (0 pledges): shows pledge link + share buttons prominently
- State 3 (has pledges): shows stats + feed
- State 4 (has 'said paid'): amber prompt to upload bank statement
- State 5 (100% collected): celebration banner
- No more onboarding checklist — dashboard adapts instead
- Event name as page header (not generic 'Home')
- Event switcher for multi-event orgs
Updated: /signup → redirects to /dashboard/welcome (not /dashboard)
Persona spec: docs/PERSONA_JOURNEY_SPEC.md
Navigation: goal-oriented, not feature-oriented
- Overview → Home
- Campaigns → Collect ('I want people to pledge')
- Pledges → Money ('Where's the money?')
- Exports → Reports ('My treasurer needs numbers')
- Old routes still work via re-exports
Terminology: human language, not SaaS jargon
- new → Waiting
- initiated → Said they paid
- paid → Received ✓
- overdue → Needs a nudge
- Campaign → Appeal
- QR Source → Pledge link
- Reconcile → Match payments
- Rail → Payment method
- Pipeline by Status → How pledges are doing
- Conversion rate → % who pledged
- CRM Export Pack → Full data download
Visual identity: brand-consistent dashboard
- Sharp edges (no rounded-lg cards)
- Gap-px grids for stats (brand signature pattern)
- Left-border accents (brand signature pattern)
- Midnight/Paper/Promise Blue 60-30-10 color rule
- Typography as hero (big bold numbers, not card-heavy)
- No emoji in UI chrome
- Brand-consistent status badges (colored bg + text, not shadcn Badge)
- Consistent header typography (text-3xl font-black tracking-tight)
Pages rewritten: layout, home, events (collect), pledges (money),
exports (reports), reconcile, settings
Reconcile: auto-detects bank CSV format via presets + AI before upload
UX spec: docs/UX_OVERHAUL_SPEC.md
- Removed all .pi/ (agents, themes, extensions, skills, observatory)
- Removed CLAUDE.md (belongs in parent pi repo)
- Added .pi/ and CLAUDE.md to .gitignore
- Added .pi/ to pledge-now-pay-later/.gitignore
- Shorten quote 03 from 'Can I split it across a few months?' to 'Can I pay monthly?' for column symmetry
- Add nbsp between 'money' and 'arriving' to prevent orphan line break
ROOT CAUSE: each card was wrapped in its own div (min-h-[85vh]),
scoping sticky to that wrapper — cards could NEVER overlap.
FIX: flatMap returns all sticky divs + h-4 spacers as direct
siblings under the same parent (mt-14). Sticky now works
correctly — each card overlaps the previous with 20px peek.
- Removed all shadows (border-gray-100 only)
- z-index: 1-4 (was 10-40, conflicting with nav z-40)
- top: 72/92/112/132px (20px stagger)
- h-4 spacers between cards (no big white gaps)
- Regenerated dinner image: dark navy table, candlelight, £5,000
pledge card — zero white space (was white tablecloth)
STACKING EFFECT:
- Cards use position: sticky with increasing top offset (72-120px)
- z-index layering (10-40) so later cards stack on top
- pb-36 between cards for scroll breathing room
- will-change-transform for smooth compositing
WHITE SPACE FIX:
- Removed border border-gray-200 (was creating visible white gap)
- Replaced with shadow-[0_2px_8px_rgba(0,0,0,0.08)] for depth
- Switched from CSS Grid to flexbox (flex-row/flex-row-reverse)
- Image fills full card height via flexbox stretch
LAYOUT:
- md:w-7/12 for image, md:w-5/12 for text
- min-h-[400px] on desktop for substantial card presence
- Alternating image left/right preserved for visual rhythm
PERSONA OVERHAUL:
- Personas now defined by WHAT THEY DID, not job titles
- 'Charity Manager' -> 'You organized the dinner'
- 'Personal Fundraiser' -> 'You shared the link'
- 'Volunteer' -> 'You were on the ground'
- 'Organisation/Programme Manager' -> 'You claim the Gift Aid'
SECTION HEADING:
- Brand core insight: 'People don't break promises. Systems do.'
- Eyebrow: 'THE PLEDGE GAP'
- Sub: 'We built the missing system between I'll donate and the money arriving.'
PAIN STATS (visual anchors):
- £50,000 pledged / £22,000 collected (the gap)
- 23 said I'll donate / 8 actually did
- 40 pledges collected / 0 updates received
- 200 rows, 47 typos / 6 hours every quarter
COPY: Emotionally precise, tells each persona's specific story
PHOTOGRAPHY (4 cinematic moment shots):
- Dinner aftermath: empty table with lone pledge card, chandeliers
- Phone: hands on WhatsApp at kitchen table, warm light
- Volunteer: seen from behind, walking between gala tables with cards
- Desk still life: laptop spreadsheet, papers, tea, window light
- All 2:1 wide aspect, 2.7MB -> 260KB optimized
HEADLINE:
- 3 balanced lines: 'Turn I'll donate' / 'into money' / 'in the bank.'
- Removed that orphaned 'money' on its own line
- <br className='hidden lg:block'> controls breaks on desktop only
IMAGE:
- Hero container: max-w-5xl -> max-w-7xl (image 25% wider)
- Stat strip widened to match
- Much more of the gala scene visible, phone prominent
DEPLOY SPEED (deploy.sh):
- Persistent /opt/pnpl/ build dir (no temp dir creation/deletion)
- BuildKit with cache mounts (npm + .next/cache)
- No more docker builder prune / docker rmi (preserves cache!)
- Installed docker-buildx v0.31.1 on server
- Before: ~245s (4+ min) After: ~29s (cached) / ~136s (first)
- Use: cd pledge-now-pay-later && bash deploy.sh
- Grid: items-start → md:items-stretch (both columns same height)
- Image: aspect-[4/5] → md:aspect-auto md:h-full (fills column)
- Mobile keeps aspect-[3/4] for stacked layout
- Bottom of image now lines up with buttons/trust line
HERO REDESIGN:
- bg-gray-950 full-bleed dark hero (was white text-on-white)
- Split layout: 7-col massive headline + 5-col documentary photo
- Gala photo (02) as hero — warm tungsten pops against dark bg
- border-l-2 promise-blue eyebrow accent (signature pattern 1)
- gap-px stat strip: 30-50%, 60s, £0, 2 min (signature pattern 2)
- stagger-children animation on text column
- Delayed fade-up on image column (opacity: 0 → fadeUp after 250ms)
- Trust line with vertical pipe separators
- Merges old hero + hero image + stat section into 1 cinematic opening
NAV:
- Wordmark hidden on mobile (sm:inline), shows P mark only
- shrink-0 on logo, whitespace-nowrap on nav buttons
PERSONA CARDS:
- Charity Manager card now uses mosque photo (08) for variety
- Hover color: text-trust-blue → text-promise-blue (proper token)
Brand compliant: 0 violations (no gradients, rounded-2xl, backdrop-blur)
Main page (pledge.quikcue.com):
- Hero: 'Turn I'll donate into money in the bank'
- 30-50% stat in dark section (single number, maximum impact)
- 4 persona cards linking to /for/* pages
- 4-step how-it-works (tightened from previous)
- Compliance strip (Gift Aid, Zakat, email, WhatsApp - compact)
- Payment flexibility (now/later/monthly)
- Platform logos
- Dark CTA section
- Footer with persona links
/for/charities:
- Pain: pledges on napkins, awkward chasing, no visibility
- 5-step how-it-works specific to charity managers
- 6 features: Gift Aid, Zakat, WhatsApp, scheduling, GDPR, exports
- CTA: Start Free
/for/fundraisers:
- Pain: shared link 50 times, 3 donated
- Before/after comparison grid (without vs with)
- 6 external platforms with branding
- CTA: Start Free
/for/volunteers:
- Personal link, live stats, leaderboard
- Event night flow (4 steps)
- Share channels grid
- CTA: Tell your charity about this
/for/donors:
- Educational trust page, not a sign-up funnel
- 6-step pledge flow explained
- Data protection table (what/why for each field)
- FAQ (cancel, already paid, no WhatsApp consent)
- CTA: Are you a charity?
GIFT AID (HMRC compliance):
- Exact HMRC model declaration text displayed and recorded
- Home address (line 1 + postcode) collected when Gift Aid is ticked
- giftAidAt timestamp recorded separately from the boolean
- Declaration text, donor name, timestamp stored in consentMeta JSON
EMAIL + WHATSAPP (GDPR/PECR compliance):
- Separate, granular opt-in checkboxes (not bundled, not pre-ticked)
- Each consent records: exact text shown, timestamp, consent version
- Consent checkboxes only appear when relevant contact info is provided
- Cron reminders gated on consent — no sends without opt-in
- Pledge creation WhatsApp receipt gated on whatsappOptIn
AUDIT TRAIL (consentMeta JSON on every pledge):
- giftAid: {declared, declarationText, declaredAt}
- email: {granted, consentText, grantedAt}
- whatsapp: {granted, consentText, grantedAt}
- IP address captured server-side from x-forwarded-for
- User agent captured client-side
- consentVersion field for tracking wording changes
EXPORTS:
- CRM CSV now includes: donor_address, donor_postcode, gift_aid_declared_at,
is_zakat, email_opt_in, whatsapp_opt_in
- Gift Aid export has full HMRC-required fields
Schema: 6 new columns on Pledge (donorAddressLine1, donorPostcode,
giftAidAt, emailOptIn, whatsappOptIn, consentMeta)