Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Off-plan riders (pulled forward, rename-independent):
- 2026-07-11: Stripe decision — GO. S18–S21 build starts 2026-07-11; pricing per `docs/ideation/2026-07-02-embeds-spec.md` §4 ladder ($29/mo or $290/yr Pro, $15/mo or $150/yr nonprofit, network hand-negotiated). Stripe is the institutional-embeds rail only; HCB is the citizen-donations rail; the two are never conflated.
- 2026-07-11: S12 ruling — quiet-submit now (registry + directory submissions run this week; review/listing latency burns down while unannounced), the public announcement itself still holds for the late Aug–Sep window.
- 2026-07-11: Pregen + Blob portraits — both armed, pending owner secrets (~$2.50–3.80/mo pregen + ~cents/mo Blob), surfaced and approved.
- 2026-07-15: HCB application DENIED (HCB currently sponsors teen hacker builds only) — the citizen-donations rail is undecided again; About/footer copy scrubbed of every fiscal-sponsor/501(c)(3)/tax-deductible claim and `DonateSupport` retired in branch `feat/funding-transparency` (founder-funded framing is the single truthful surface); `DONATE_URL` stays null until a replacement rail is chosen.

## Issues & learnings ledger (compound these)

Expand Down
27 changes: 20 additions & 7 deletions app/[locale]/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Metadata } from 'next';
import { setRequestLocale, getTranslations } from 'next-intl/server';
import { DonateSupport } from '@/components/DonateSupport';
import { hreflangAlternates } from '@/lib/hreflang';
import { DONATE_URL } from '@/lib/site';

Expand All @@ -27,6 +26,26 @@ export default async function AboutPage({ params }: { params: Promise<{ locale:
<section className="mt-10">
<h2 className="font-display text-2xl font-bold">{t('fundingTitle')}</h2>
<p className="mt-2 max-w-prose leading-relaxed">{t('fundingBody')}</p>
{/* The one support ask on this page (the HCB fiscal-sponsorship route
was denied 2026-07-15, so the former DonateSupport section and its
sponsor/tax-deductible claims are retired). Dark by construction
until DONATE_URL is set - one constant, no second flag. Never
claims tax-deductibility or nonprofit status - this is a personal,
founder-funded contribution, not a charitable gift. Link-out only:
never an iframe or a payment field on Oravan's own infra (§6). */}
{DONATE_URL && (
<p className="mt-2 max-w-prose leading-relaxed">
{t('fundingSupportBody')}{' '}
<a
href={DONATE_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-h-11 items-center font-semibold underline underline-offset-2 hover:text-ink"
>
{t('fundingSupportCta')}
</a>
</p>
)}
</section>

{/* Who's accountable + how to reach a person — surfaced outside the
Expand All @@ -37,12 +56,6 @@ export default async function AboutPage({ params }: { params: Promise<{ locale:
<h2 className="font-display text-2xl font-bold">{t('accountabilityTitle')}</h2>
<p className="mt-2 max-w-prose leading-relaxed">{t('accountabilityBody')}</p>
</section>

{/* §6: the donations leg — link-out only, never a payment field on our
own infra. Dark by construction until DONATE_URL is set (HCB
onboarding is separate, in-flight paperwork); flipping that one
constant is the entire code change needed to light this up. */}
<DonateSupport donateUrl={DONATE_URL} />
</article>
);
}
42 changes: 0 additions & 42 deletions components/DonateSupport.tsx

This file was deleted.

25 changes: 24 additions & 1 deletion components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,28 @@ export function Footer({ donateUrl = DONATE_URL }: { donateUrl?: string | null }
<p className="max-w-prose italic text-ink-faint">{t('footer.lore')}</p>
<p className="max-w-prose">{t('footer.mission')}</p>
<p className="max-w-prose">{t('footer.aiNote')}</p>
{/* Phase 1/2 funding transparency line: dark copy today (DONATE_URL
null), automatically upgrades to the "funded + supporters" line
with a Support Oravan link the moment DONATE_URL is set - one
constant, no separate flag, same pattern as the nav Donate link
below. */}
<p className="max-w-prose">
{donateUrl ? (
<>
{t('footer.fundingLive')}{' '}
<a
href={donateUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-h-11 items-center underline underline-offset-2 hover:text-ink"
>
{t('footer.fundingCta')}
</a>
</>
) : (
t('footer.funding')
)}
</p>
<nav aria-label={t('footer.navLabel')} className="flex flex-wrap gap-5 pt-2">
<Link href="/privacy" className="underline underline-offset-2 hover:text-ink">
{t('footer.privacy')}
Expand Down Expand Up @@ -53,7 +75,8 @@ export function Footer({ donateUrl = DONATE_URL }: { donateUrl?: string | null }
{t('footer.partners')}
</Link>
{/* §6: quiet, persistent, never a banner or modal - link only, dark
until HCB onboarding completes (DONATE_URL flips from null). */}
until a donation rail exists (DONATE_URL flips from null; the
HCB fiscal-sponsorship route was denied 2026-07-15, rail TBD). */}
{donateUrl && (
<a
href={donateUrl}
Expand Down
24 changes: 16 additions & 8 deletions lib/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,23 @@
export const SITE_ORIGIN = 'https://oravan.org';

/**
* The HCB-hosted donation page URL (§6 of the build/GTM strategy). Null
* until the HCB fiscal-sponsorship application clears — that's paperwork,
* not code, and is in flight separately. Every donate affordance (footer
* link, About/Support page ask copy) reads this one constant and renders
* nothing when it's null, so flipping this to the live HCB URL is the
* entire code change needed to light up donations everywhere at once.
* The externally-hosted donation page URL. Null until a donation rail
* exists: the HCB fiscal-sponsorship application was DENIED 2026-07-15
* (HCB currently sponsors teen hacker builds only), so the rail decision
* is back with the owner and this stays null until a replacement is chosen
* and live. Every donate affordance (footer funding line + Donate link,
* the About page's "Who pays for this?" ask copy) reads this one constant
* and renders nothing extra when it's null, so flipping this to a live URL
* is the entire code change needed to light up donations everywhere at once.
*
* Copy rule, non-negotiable: no surface may claim tax-deductibility,
* nonprofit status, or a fiscal sponsor — contributions are personal
* support for a founder-funded project (tests/donate.unit.spec.ts pins
* this against the message files).
*
* Always link out (target="_blank", rel="noopener noreferrer") — never
* iframe HCB's page in, and never add a payment field anywhere on Oravan's
* own infra. See docs/ideation/2026-07-05-build-gtm-strategy.md §6.
* iframe a payment page in, and never add a payment field anywhere on
* Oravan's own infra. See docs/ideation/2026-07-05-build-gtm-strategy.md §6
* (historical record; its HCB-specific plan is superseded by the denial).
*/
export const DONATE_URL: string | null = null;
14 changes: 8 additions & 6 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
"donate": "Donate",
"embeds": "Embeds",
"navLabel": "Footer",
"partners": "Partners"
"partners": "Partners",
"funding": "Founder-funded. No ads, no tracking, no outside money.",
"fundingLive": "Funded by the founder and supporters.",
"fundingCta": "Support Oravan"
},
"switchLocale": "En español",
"loading": "Loading…",
Expand Down Expand Up @@ -348,11 +351,10 @@
"about": {
"title": "About Oravan",
"intro": "Oravan is free, nonpartisan civic infrastructure: find your federal representatives, understand active bills in plain language, and make a 30-second call — in English or Spanish, with no account and no server-side record of your political activity.",
"fundingTitle": "How Oravan is funded",
"fundingBody": "Oravan runs without ads, without partisan grants, and without selling data — there's no data to sell. A donation option, hosted entirely off Oravan's own servers, is on the way.",
"supportTitle": "Support Oravan",
"supportBody": "Oravan is free, ad-free, and takes no partisan money. Your gift helps us: track every bill moving through Congress each week, keep the plain-language decodes current and human-reviewed, and keep call scripts free in English and Spanish. We store no donor political activity, ever — gifts are tax-deductible and processed off Oravan's own servers by our fiscal sponsor, The Hack Foundation, a 501(c)(3) nonprofit.",
"donateCta": "Donate via our fiscal sponsor",
"fundingTitle": "Who pays for this?",
"fundingBody": "Oravan is independently run and founder-funded. It costs a few hundred dollars a year to host and maintain, currently paid out of one person's pocket. There are no ads, no sponsors, and no investors — and nothing to sell, because we collect no personal data.",
"fundingSupportBody": "If Oravan is useful to you, you can chip in to keep it running. Contributions support hosting and upkeep and are not tax-deductible.",
"fundingSupportCta": "Support Oravan",
"accountabilityTitle": "Independent, and reachable",
"accountabilityBody": "Oravan is an independent, nonpartisan project — not a campaign, a party, or a PAC. Its promises are built to be verified, not trusted: no account, no tracking, and nothing about you kept on a server. For a correction, a question, or a partnership inquiry, the feedback link at the bottom of any page reaches a real person, or email us at hello@oravan.org."
},
Expand Down
14 changes: 8 additions & 6 deletions messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
"donate": "Donar",
"embeds": "Widgets",
"navLabel": "Pie de página",
"partners": "Socios"
"partners": "Socios",
"funding": "Financiado por su fundador. Sin anuncios, sin rastreo, sin dinero externo.",
"fundingLive": "Financiado por el fundador y quienes lo apoyan.",
"fundingCta": "Apoya a Oravan"
},
"switchLocale": "In English",
"loading": "Cargando…",
Expand Down Expand Up @@ -348,11 +351,10 @@
"about": {
"title": "Acerca de Oravan",
"intro": "Oravan es infraestructura cívica gratuita y no partidista: encuentra a tus representantes federales, entiende los proyectos de ley activos en lenguaje claro y haz una llamada de 30 segundos — en inglés o español, sin cuenta y sin ningún registro en un servidor de tu actividad política.",
"fundingTitle": "Cómo se financia Oravan",
"fundingBody": "Oravan funciona sin anuncios, sin subvenciones partidistas y sin vender datos — no hay datos que vender. Una opción para donar, alojada por completo fuera de los servidores de Oravan, está en camino.",
"supportTitle": "Apoya a Oravan",
"supportBody": "Oravan es gratuito, sin anuncios y no acepta dinero partidista. Tu donación nos ayuda a: dar seguimiento a cada proyecto de ley que avanza en el Congreso cada semana, mantener las explicaciones en lenguaje sencillo actualizadas y revisadas por personas, y mantener gratuitos los guiones de llamada en inglés y español. Nunca almacenamos la actividad política de quienes donan — las donaciones son deducibles de impuestos y se procesan fuera de los servidores de Oravan, por nuestro patrocinador fiscal, The Hack Foundation, una organización 501(c)(3).",
"donateCta": "Dona a través de nuestro patrocinador fiscal",
"fundingTitle": "¿Quién paga esto?",
"fundingBody": "Oravan es un proyecto independiente, financiado por su fundador. Cuesta unos cientos de dólares al año alojarlo y mantenerlo, actualmente pagados de su propio bolsillo por una sola persona. No hay anuncios, ni patrocinadores, ni inversionistas — y nada que vender, porque no recopilamos datos personales.",
"fundingSupportBody": "Si Oravan te resulta útil, puedes colaborar para mantenerlo en marcha. Las contribuciones ayudan a cubrir el alojamiento y el mantenimiento y no son deducibles de impuestos.",
"fundingSupportCta": "Apoya a Oravan",
"accountabilityTitle": "Independiente y accesible",
"accountabilityBody": "Oravan es un proyecto independiente y no partidista — no es una campaña, un partido ni un comité de acción política. Sus promesas están hechas para comprobarse, no para creerse: sin cuenta, sin rastreo y sin guardar nada sobre ti en un servidor. Para una corrección, una pregunta o una consulta de colaboración, el enlace de comentarios al final de cualquier página llega a una persona real, o escríbenos a hello@oravan.org."
},
Expand Down
35 changes: 21 additions & 14 deletions tests/donate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,37 @@ import en from '../messages/en.json';
import es from '../messages/es.json';

/*
* §6 donations leg (S4-S5). DONATE_URL (lib/site.ts) is null today — HCB
* onboarding is separate, in-flight paperwork — so this build's real,
* observable behavior is "dark": no Donate affordance exists anywhere.
* That's what these e2e tests hold the current build to.
* §6 donations leg (S4-S5). DONATE_URL (lib/site.ts) is null today — the HCB
* fiscal-sponsorship application was denied 2026-07-15 (teen-builds-only
* policy) and no replacement rail is chosen — so this build's real,
* observable behavior is "dark": no Donate affordance exists anywhere, the
* footer states the founder-funded line, and the About page's support ask
* paragraph does not render. That's what these e2e tests hold the current
* build to.
*
* The "lit" state (DONATE_URL set) can't be exercised in this same run
* without a second `next build` under a different constant value - this
* suite's webServer builds once. That's consistent with the plan's own
* checklist (S4-S5 "Done" line): "the link may sit dark until HCB
* onboarding completes - the launch-week checklist re-verifies it's live."
* See tests/donate.unit.spec.ts for a source-level guard that the wiring
* itself (both surfaces keying off the one DONATE_URL constant) is intact,
* so flipping it is guaranteed to be a one-line change and nothing more.
* suite's webServer builds once. See tests/donate.unit.spec.ts for a
* source-level guard that the wiring itself (every gated surface keying off
* the one DONATE_URL constant) is intact, so flipping it is guaranteed to
* be a one-line change and nothing more.
*/

for (const [locale, prefix, messages] of [
['en', '', en],
['es', '/es', es],
] as const) {
test.describe(`${locale} locale: donate surfaces are dark`, () => {
test('footer has no Donate link, but always has an About link', async ({ page }) => {
test('footer states the founder-funded line, with no Donate or Support link, but always an About link', async ({
page,
}) => {
await page.goto(`${prefix}/`);
const footer = page.locator('footer');
await expect(footer.getByText(messages.common.footer.funding)).toBeVisible();
await expect(footer.getByText(messages.common.footer.fundingLive)).toHaveCount(0);
await expect(
footer.getByRole('link', { name: messages.common.footer.fundingCta })
).toHaveCount(0);
await expect(footer.getByRole('link', { name: messages.common.footer.donate })).toHaveCount(0);
await expect(footer.getByRole('link', { name: messages.common.footer.about })).toBeVisible();
});
Expand All @@ -38,9 +46,8 @@ for (const [locale, prefix, messages] of [
await expect(page).toHaveURL(new RegExp(`${prefix || ''}/about$`));
await expect(page.getByRole('heading', { name: messages.about.title, level: 1 })).toBeVisible();
await expect(page.getByText(messages.about.fundingBody)).toBeVisible();
await expect(page.getByRole('heading', { name: messages.about.supportTitle })).toHaveCount(0);
await expect(page.getByText(messages.about.supportBody)).toHaveCount(0);
await expect(page.getByRole('link', { name: messages.about.donateCta })).toHaveCount(0);
await expect(page.getByText(messages.about.fundingSupportBody)).toHaveCount(0);
await expect(page.getByRole('link', { name: messages.about.fundingSupportCta })).toHaveCount(0);
});

test('the About page content itself has no form fields or iframes (link-out only, per §6)', async ({
Expand Down
Loading
Loading