feat: invitation-only open letters (€10)#54
Open
xdamman-bot wants to merge 23 commits intocitizenspring:masterfrom
Open
feat: invitation-only open letters (€10)#54xdamman-bot wants to merge 23 commits intocitizenspring:masterfrom
xdamman-bot wants to merge 23 commits intocitizenspring:masterfrom
Conversation
- api/scripts/sync-donors.js (npm run cron): fetches Stripe checkout sessions for the donation payment link, builds donors.json - api/scripts/seed-oc-donors.js (npm run seed:oc-donors): one-time script to snapshot current OC backers into donors.json - frontend/components/DonorsList.js: replaces OpenCollectiveData, reads from /data/donors.json (static file, no API calls) - Updated donate page and confirm_signature to use DonorsList - Empty donors.json placeholder committed so frontend doesn't 404 Flow: 1. Run seed:oc-donors once to capture existing OC backers 2. Set up daily cron: STRIPE_SECRET_KEY=rk_xxx npm run cron 3. Cron merges Stripe donors with existing OC donors 4. donors.json is committed/deployed with the frontend
Scripts write to frontend/public/data/ so they belong in the frontend package. Moved sync-donors.js and seed-oc-donors.js to frontend/scripts/, updated output paths, moved stripe dep from api to frontend.
Reads the optional 'displaynameforthedonorslist' custom field from checkout sessions. Donors who leave it blank remain anonymous (skipped).
Use CREDIT transactions instead of members query to get per-donor amount totals and earliest donation date. Aggregates multiple donations per person into one entry.
- Top 10 contributors by amount in a featured section - All contributors sorted by date (newest first), show 10 initially - 'Show all' button to expand the full list - Compact mode (inline names) used on post-signature confirmation
Skips the actual confirmation flow and shows the signed state with the compact donors list. Usage: /any-letter/confirm_signature?preview=confirmed
…lative dates - Filter out 'Citizen Spring' from OC seed script - No links on any donor names - All contributors list shows relative date (e.g. '3 months ago')
Fetches expenses from OC GraphQL, stores in donors.json, displays them on /donate with description, amount and relative date.
Matches the current live layout: 1. Cost summary cards (total spent, daily avg, monthly avg) 2. Latest 5 expenses with status 3. Top 10 contributors 4. All contributors by date with show more All powered by static donors.json instead of live OC GraphQL.
Adds prebuild script so donors.json is refreshed from OpenCollective on every Vercel deploy. Falls back gracefully (|| true) if the OC API is unreachable.
The OC GraphQL v1 API returns createdAt as human-readable strings
(e.g. 'Sun Mar 29 2026 23:36:03 GMT+0000'), not ISO-8601.
The old .split('T')[0] was splitting on the T in 'GMT', producing
broken date strings that caused NaN in the frontend.
Now uses new Date() → toISOString() → YYYY-MM-DD for robust parsing.
Without this, only OC donors appear — Stripe donors were missing entirely. sync-donors.js gracefully exits if STRIPE_SECRET_KEY is not set.
sync-donors.js was overwriting donors.json without carrying over the expenses and ocLegacy fields written by seed-oc-donors.js.
- Migration: add letter_type, restriction_mode, allowed_domains, invites_per_person, allow_chain_invites, is_paid, stripe_session_id, deleted_at columns to letters table - Migration: create invitations table with web-of-trust tracking (token, email, invited_by, generation, invites_remaining, signature_id) - New Invitation model with letter/parent/children/signature relations - InvitationController: create (batch invite by email), list (creator), validate (public invite token check) - LetterController.sign: enforce invite-only restrictions - restriction_mode=invite: require valid invite_token - restriction_mode=domain: validate email against allowed_domains - Link invitation to signature after signing - LetterController.get: expose letter_type, restriction_mode, is_paid, allowed_domains in API response - Letter.createWithLocales: accept invite-only fields - New API routes: POST/GET letters/:slug/invitations, GET invitations/:token
|
The preview deployment for openletter is ready. 🟢 Open Preview | Open Build Logs | Open Application Logs Last updated at: 2026-04-05 18:31:33 CET |
- StripeController: createCheckout (creates €10 Checkout session), verifyPayment (checks payment status on redirect), webhook handler (verifies via Stripe API instead of raw body signature — Adonis v4 compat) - Routes: POST letters/:slug/checkout, POST letters/:slug/verify-payment, POST webhooks/stripe - Added stripe dependency to api/package.json Flow: creator creates invite-only letter → redirected to Stripe Checkout → on success, letter.is_paid = true → invitations can be sent. Both webhook and verify-payment endpoints can activate the letter, providing redundancy.
Stripe integration: - StripeController: createCheckout (€10 session), verifyPayment (status check on redirect), webhook (verifies via Stripe API) - Added stripe dependency to api/package.json - Routes: POST letters/:slug/checkout, verify-payment, webhooks/stripe Frontend: - LetterForm: letter type selector (Public vs Invitation-only €10), restriction mode (invite links vs email domain), invite settings (invites per person, chain invites toggle, allowed domains) - create.js: redirects to Stripe Checkout for invite-only letters - [slug]/index.js: gates signing form behind invite token or domain check, shows appropriate messaging for invite-only letters - [slug]/manage.js: invitation management dashboard — send invites, view status (invited/signed/pending), domain mode info - pricing.js: pricing page with Public vs Invitation-only comparison, explains anti-bot benefits, web of trust, genuine signatures - Footer: added Pricing link - en.json: 60+ new i18n keys for all new UI - Updated FAQ answers for 'can I limit who signs'
- Hero section with stats and dual CTA (public free vs invite-only €10) - NEW badge section highlighting invitation-only: stop AI bots, web of trust, more weight for your letter - 4-step 'how it works' with public/invite distinction - Values section in card grid - Cleaner structure, removed inline SVG duplication - 15+ new i18n keys for homepage content
LetterForm: - Replaced all styled-components with Tailwind classes - Clean rounded inputs with focus rings, serif title font - Inline preview panel (toggle show/hide) renders markdown-ish bold/italic with live title and image - Auto-saves to localStorage on every keystroke (debounce via setState callback) - Restores draft on page load — survives crashes, tab closes, refreshes - Exports DRAFT_KEY constant for external clear create.js: - Clean centered layout with 2/3 form + 1/3 sidebar (FAQ + draft indicator) - DraftIndicator component shows green pulse dot + last save time - Clear draft button to start fresh - Draft only cleared after confirmed publish (Router.push) or Stripe redirect - Page title and heading via i18n New i18n keys: create.page.*, create.preview.*
Replaced the confusing 'letter type' framing with 'Who can sign?': - Clear subtitle: 'Your letter is always public and readable by everyone. Choose who can add their signature.' - Three distinct options: ✅ Open to everyone (free) 🔗 Personal invitation required (€10) — own column with settings 🏛️ Email domain restriction (€10) — own column with settings - Each restricted option is a separate selectable card, not a toggle inside a collapsed section - Settings (invites per person, chain invites, domain list) appear directly below the selected option - Removed 'letter type' terminology entirely from the UI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Invitation-Only Letters
Adds a new letter type: invitation-only (€10 to publish). Letters are always publicly readable, but signing is restricted via personal invitation links or email domain restrictions.
Phase 1: Backend (this commit)
letters+ newinvitationstableletter_type,restriction_mode,is_paid,allowed_domainsComing next
See
SPEC-invite-only.mdfor full specification.