Stripe integration: charity connects their own Stripe account
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)
This commit is contained in:
@@ -5,7 +5,7 @@ import { useSession } from "next-auth/react"
|
||||
import {
|
||||
Check, Loader2, AlertCircle, MessageCircle, Radio, RefreshCw,
|
||||
Smartphone, Wifi, WifiOff, QrCode, UserPlus, Trash2, Copy,
|
||||
Users, Crown, Eye, Building2, CreditCard, Palette, ChevronRight
|
||||
Users, Crown, Eye, Building2, CreditCard, Palette, ChevronRight, Zap
|
||||
} from "lucide-react"
|
||||
|
||||
/**
|
||||
@@ -29,6 +29,7 @@ interface OrgSettings {
|
||||
name: string; bankName: string; bankSortCode: string; bankAccountNo: string
|
||||
bankAccountName: string; refPrefix: string; primaryColor: string
|
||||
gcAccessToken: string; gcEnvironment: string; orgType: string
|
||||
stripeSecretKey: string; stripeWebhookSecret: string
|
||||
}
|
||||
|
||||
interface TeamMember {
|
||||
@@ -148,6 +149,7 @@ export default function SettingsPage() {
|
||||
const bankReady = !!(settings.bankSortCode && settings.bankAccountNo && settings.bankAccountName)
|
||||
const whatsappReady = waStatus === "CONNECTED"
|
||||
const charityReady = !!settings.name
|
||||
const stripeReady = !!settings.stripeSecretKey
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
@@ -161,11 +163,12 @@ export default function SettingsPage() {
|
||||
{/* ── Readiness bar — "Am I set up?" ── */}
|
||||
<div className="bg-[#111827] p-5">
|
||||
<p className="text-[10px] font-bold text-gray-500 uppercase tracking-widest mb-3">Setup progress</p>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-px bg-gray-700">
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-px bg-gray-700">
|
||||
{[
|
||||
{ label: "WhatsApp", ready: whatsappReady, detail: whatsappReady ? "Connected" : "Not connected" },
|
||||
{ label: "Bank details", ready: bankReady, detail: bankReady ? `${settings.bankSortCode}` : "Not set" },
|
||||
{ label: "Charity name", ready: charityReady, detail: charityReady ? settings.name : "Not set" },
|
||||
{ label: "Card payments", ready: stripeReady, detail: stripeReady ? "Stripe connected" : "Not set up" },
|
||||
{ label: "Team", ready: team.length > 0, detail: `${team.length} member${team.length !== 1 ? "s" : ""}` },
|
||||
].map(item => (
|
||||
<div key={item.label} className="bg-[#111827] p-3">
|
||||
@@ -429,7 +432,95 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── 4. Charity details ── */}
|
||||
{/* ── 4. Card payments (Stripe) ── */}
|
||||
<section className="bg-white border border-gray-200">
|
||||
<div className="border-b border-gray-100 px-5 py-4 flex items-center gap-3">
|
||||
<div className={`w-8 h-8 flex items-center justify-center shrink-0 ${stripeReady ? "bg-[#635BFF]/10" : "bg-gray-100"}`}>
|
||||
<CreditCard className={`h-4 w-4 ${stripeReady ? "text-[#635BFF]" : "text-gray-400"}`} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-sm font-bold text-[#111827]">Card payments</h2>
|
||||
{stripeReady && <div className="w-2 h-2 bg-[#635BFF]" />}
|
||||
</div>
|
||||
<p className="text-[10px] text-gray-500">Let donors pay by card using your Stripe account</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-5 space-y-4">
|
||||
<div className="border-l-2 border-[#635BFF] pl-3 text-xs text-gray-500 space-y-1">
|
||||
<p>Connect your <strong className="text-gray-700">own Stripe account</strong> to accept card payments. Money goes directly to your Stripe balance — we never touch it.</p>
|
||||
<p>When connected, donors see a third payment option: <strong className="text-gray-700">Bank Transfer</strong>, <strong className="text-gray-700">Direct Debit</strong>, and <strong className="text-gray-700">Card Payment</strong>.</p>
|
||||
</div>
|
||||
|
||||
{!stripeReady && (
|
||||
<div className="bg-[#F9FAFB] border border-gray-100 p-4 space-y-3">
|
||||
<p className="text-xs font-bold text-[#111827]">How to get your Stripe API key</p>
|
||||
<ol className="text-[10px] text-gray-500 space-y-1.5 list-decimal list-inside">
|
||||
<li>Go to <a href="https://dashboard.stripe.com/apikeys" target="_blank" rel="noopener noreferrer" className="text-[#635BFF] font-bold hover:underline">dashboard.stripe.com/apikeys</a></li>
|
||||
<li>Copy your <strong className="text-gray-700">Secret key</strong> (starts with <code className="bg-gray-100 px-1 py-0.5 text-[9px] font-mono">sk_live_</code> or <code className="bg-gray-100 px-1 py-0.5 text-[9px] font-mono">sk_test_</code>)</li>
|
||||
<li>Paste it below</li>
|
||||
</ol>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Field
|
||||
label="Stripe secret key"
|
||||
value={settings.stripeSecretKey || ""}
|
||||
onChange={v => update("stripeSecretKey", v)}
|
||||
placeholder="sk_live_... or sk_test_..."
|
||||
type="password"
|
||||
/>
|
||||
|
||||
<details className="text-xs">
|
||||
<summary className="text-gray-400 cursor-pointer hover:text-gray-600 font-bold">Webhook setup (optional — for auto-confirmation)</summary>
|
||||
<div className="mt-3 space-y-3 pt-3 border-t border-gray-100">
|
||||
<div className="border-l-2 border-gray-200 pl-3 text-[10px] text-gray-500 space-y-1">
|
||||
<p>If you want pledges to auto-confirm when the card is charged:</p>
|
||||
<ol className="list-decimal list-inside space-y-0.5">
|
||||
<li>In Stripe Dashboard → Developers → Webhooks</li>
|
||||
<li>Add endpoint: <code className="bg-gray-100 px-1 py-0.5 font-mono">{typeof window !== "undefined" ? window.location.origin : ""}/api/stripe/webhook</code></li>
|
||||
<li>Select events: <code className="bg-gray-100 px-1 py-0.5 font-mono">checkout.session.completed</code></li>
|
||||
<li>Copy the signing secret and paste below</li>
|
||||
</ol>
|
||||
</div>
|
||||
<Field
|
||||
label="Webhook signing secret"
|
||||
value={settings.stripeWebhookSecret || ""}
|
||||
onChange={v => update("stripeWebhookSecret", v)}
|
||||
placeholder="whsec_..."
|
||||
type="password"
|
||||
/>
|
||||
<p className="text-[10px] text-gray-400">Without webhooks, you confirm card payments manually in the Money page (same as bank transfers).</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
{stripeReady && (
|
||||
<div className="bg-[#635BFF]/5 border border-[#635BFF]/20 p-3 flex items-start gap-2">
|
||||
<Zap className="h-4 w-4 text-[#635BFF] mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-xs font-bold text-[#635BFF]">Card payments are live</p>
|
||||
<p className="text-[10px] text-gray-500 mt-0.5">Donors will see “Pay by Card” as an option. They'll be redirected to Stripe's checkout page. Apple Pay and Google Pay work automatically.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<p className="text-[10px] text-gray-400">{stripeReady ? "Key stored securely · never shown to donors" : "Optional — bank transfer works without this"}</p>
|
||||
<SaveBtn
|
||||
section="stripe"
|
||||
saving={saving}
|
||||
saved={saved}
|
||||
onSave={() => save("stripe", {
|
||||
stripeSecretKey: settings.stripeSecretKey || "",
|
||||
stripeWebhookSecret: settings.stripeWebhookSecret || "",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ── 5. Charity details ── */}
|
||||
<section className="bg-white border border-gray-200">
|
||||
<div className="border-b border-gray-100 px-5 py-4 flex items-center gap-3">
|
||||
<div className="w-8 h-8 bg-gray-100 flex items-center justify-center shrink-0">
|
||||
|
||||
Reference in New Issue
Block a user