feat: add /donate page with Stripe embedded checkout and Cloudflare Worker backend#118
feat: add /donate page with Stripe embedded checkout and Cloudflare Worker backend#118alexwolson wants to merge 33 commits into
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove locally-added test keys from _config.yml - Fix "Sponsor a month" amount: $270 → $260 ($65 × 4) - Update explainer copy: clearer framing of what donations cover - Remove em dashes from all copy - Fix thank-you message to reflect food funding, not general operations Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pa11y Accessibility Report✅ All pages passed — 0 warning(s) (WCAG2AA) Homepage✅ No issues found. Event✅ No issues found. Project✅ No issues found. Person✅ No issues found. Organization✅ No issues found. Venue✅ No issues found. Resource✅ No issues found. Tag✅ No issues found. Category✅ No issues found. |
There was a problem hiding this comment.
Pull request overview
Adds a new /donate/ page to the Jekyll site and a Cloudflare Worker backend to support Stripe Embedded Checkout plus a live donation progress total stored in Cloudflare KV.
Changes:
- Adds Cloudflare Worker with
GET /total,POST /checkout, andPOST /webhookendpoints and supporting validation/helpers. - Adds a new
/donate/Jekyll page that displays a live progress bar and mounts Stripe Embedded Checkout inline. - Updates GitHub Pages workflow to inject Stripe publishable key and Worker URL into
_config.ymlat build time.
Reviewed changes
Copilot reviewed 12 out of 13 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
worker/wrangler.toml |
Configures the Cloudflare Worker entrypoint and KV binding. |
worker/vitest.config.js |
Sets up Vitest configuration for Worker unit tests. |
worker/test/webhook.test.js |
Adds unit tests for KV total increment logic. |
worker/test/validate.test.js |
Adds unit tests for donation amount validation. |
worker/src/webhook.js |
Implements KV counter update and Stripe webhook signature verification. |
worker/src/validate.js |
Implements donation amount validation rules. |
worker/src/index.js |
Implements Worker routing + Stripe Checkout Session creation + webhook handler. |
worker/package.json |
Adds Worker dev tooling dependencies and npm scripts. |
worker/package-lock.json |
Locks Worker npm dependency tree. |
worker/.gitignore |
Ignores Worker-local artifacts (node_modules, Wrangler state, local vars). |
docs/superpowers/specs/2026-05-04-donation-campaign-design.md |
Adds design spec documenting the donation architecture and UX. |
docs/superpowers/plans/2026-05-04-donation-campaign.md |
Adds implementation plan and operational/testing runbook. |
.github/workflows/pages.yml |
Injects STRIPE_PUBLIC_KEY and WORKER_URL into Jekyll config during CI build. |
_pages/donate.html |
Adds /donate/ page UI + Stripe Embedded Checkout client integration. |
Files not reviewed (1)
- worker/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
node-addon-api and node-gyp were listed as runtime deps but the Worker uses no native addons. Removed them and regenerated the lockfile. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced signatures.includes(computed) string comparison with crypto.subtle.verify, which performs a constant-time comparison internally and avoids timing side-channels. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers valid signature, wrong signature, multiple v1 values (key rotation), tampered body, wrong secret, and malformed headers. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…victech.ca Previously the fallback set Access-Control-Allow-Origin to civictech.ca for every origin, which incorrectly told browsers all origins were allowed. Now the header is only included for origins in the allowlist. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Check r.ok before parsing JSON so non-2xx responses fall through to the catch handler rather than producing NaN UI state - Guard against missing/non-finite raised_cents - Clamp rendered dollars to 3500 so the <progress> bar never exceeds max Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…erance - Trim whitespace around comma-separated header fields - Skip malformed fields (no '=' or empty value) - Reject non-numeric timestamps - Reject signatures older than 5 minutes or more than 5 minutes in the future (Stripe replay-attack mitigation) - Update tests to use dynamic timestamps and cover new cases Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated 10 comments.
Files not reviewed (1)
- worker/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Returning 400 causes Stripe to retry for days with no benefit since the payload won't change. Log the anomaly for manual reconciliation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.
Files not reviewed (1)
- worker/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
I have 2 questions:
|
|
Khasir
left a comment
There was a problem hiding this comment.
I'll approve! @alexwolson for any changes/additions to testing do you think it makes sense to update the readme in another PR?
Definitely! |
|
Not adding for now as there are further discussions happening re. funding. |
Summary
/donate/page to civictech.ca for the 2027 food funding campaign, with a goal of raising $3,500 CAD to cover samosas across 50 Tuesday meetupsctt-donations.ctt-donations.workers.dev) with three endpoints:GET /total,POST /checkout,POST /webhookSTRIPE_PUBLIC_KEYandWORKER_URLArchitecture
flowchart LR Page["civictech.ca/donate/\nJekyll static page"] Worker["Cloudflare Worker"] KV["Cloudflare KV\nrunning total"] Stripe["Stripe\npayment + events"] Page -- "GET /total" --> Worker Page -- "POST /checkout" --> Worker Worker -- "clientSecret" --> Page Page -- "card details" --> Stripe Stripe -- "POST /webhook" --> Worker Worker -- "read/write total_cents" --> KVJekyll page (
_pages/donate.html): campaign explainer, live progress bar, preset amount buttons ($10 / $25 / $50 / custom / Sponsor a Tuesday $65 / Sponsor a month $260), donate button, embedded checkout mount, and thank-you state on return from Stripe.Cloudflare Worker (
worker/): plain JS, no framework, deployed to Cloudflare's edge. Stripe secret key and webhook secret live in Cloudflare Worker secrets (never in the repository).Cloudflare KV: stores a single key
total_cents. Incremented on eachcheckout.session.completedwebhook event.Stripe: processes card payments via Embedded Checkout (
ui_mode: embedded). Firescheckout.session.completedwebhook on success. Automatic email receipts enabled. Currency: CAD.Files changed
_pages/donate.htmlworker/src/index.jsGET /total,POST /checkout,POST /webhookhandlersworker/src/validate.jsvalidateAmount()— validates donation amount (integer, $1–$3,500)worker/src/webhook.jsincrementTotal()andverifyStripeSignature()(HMAC-SHA256 via Web Crypto API)worker/test/worker/wrangler.tomlworker/package.json.github/workflows/pages.ymlSTRIPE_PUBLIC_KEYandWORKER_URLsecret injection at build timeSecurity
STRIPE_WEBHOOK_SECRET(stored in Cloudflare Worker secrets)STRIPE_SECRET_KEYlives in Cloudflare Worker secrets, never in the repositorySTRIPE_PUBLIC_KEYis a publishable key — safe to embed in page JShttps://civictech.caonly in productionBefore merging
Two GitHub Actions secrets must be added in repo Settings → Secrets → Actions:
STRIPE_PUBLIC_KEYpk_live_...for production)WORKER_URLhttps://ctt-donations.ctt-donations.workers.devTesting performed
4242 4242 4242 4242across multiple preset amounts and a custom amountcheckout.session.completedwebhook fires, signature verification passes, and KV total increments correctly?session_id=in URLGenerated with Claude Code