The iframe.diy-based sandbox infrastructure powered two features:
the nsite preview dialog (Run button on NsiteCard and AppHandlerContent)
and the webxdc embed (cartridge launcher + sandboxed iframe runtime
with kind 4932/20932 sync events).
Both are removed wholesale:
- iframe sandbox plumbing: SandboxFrame, src/lib/sandbox/*,
iframeSubdomain, previewInjectedScript.
- nsite preview: NsitePreviewDialog, NsitePermissionManager,
NsitePermissionPrompt, useNsiteSignerRpc, nsitePermissions,
nsiteNostrProvider, NsitePlayerContext.
- webxdc runtime: WebxdcEmbed, Webxdc, GameControls, useWebxdc,
webxdcMeta, public/cartridge.png, NOSTR_WEBXDC.md,
@webxdc/types dependency, kindLabels/signerWithNudge entries
for kinds 4932/20932.
- AppConfig: drop sandboxDomain, showWebxdc, feedIncludeWebxdc.
- extraKinds: drop kind 1063 webxdc entry; sidebar drops 'webxdc'.
- ComposeBox: .xdc uploads now flow through the generic file path
(no UUID injection, no manifest extraction, .xdc removed from
the file picker accept list).
- NoteContent / FileMetadataContent: webxdc branches removed; .xdc
attachments fall through to the generic file card.
- LayoutContext / CenterColumnContext: only consumed by the removed
fullscreen preview panels — deleted along with its provider in
AppRouter.
NsiteCard keeps its rich link-preview card but loses the Run button
and preview dialog. AppHandlerContent keeps a kind 35128 `a`-tag
reference but replaces 'Run' with an external 'Open Site' link to
`<pubkeyB36><dTag>.nsite.lol`. The standard HTML iframe `sandbox`
attribute used by SpotifyEmbed/TweetEmbed/RedditEmbed is unrelated
to iframe.diy and stays.
Squashed re-application of 15 local commits onto the new route-level
layout system (refactor: replace useLayoutOptions store with route-level
layout choice). Drops the obsolete useLayoutOptions({ fullBleed: true })
calls; the About page and the two guide pages instead live under the
wide FundraiserLayout route group in AppRouter.
Routing
- /about, /about/donors, /about/activists are now the canonical paths,
in the wide layout (no max-width cap so sections can span the
viewport with their own backgrounds).
- /help, /help/donors, /help/activists become <Navigate> redirects so
existing bookmarks and links keep working.
About page (new src/pages/AboutPage.tsx)
A landing-style document modeled on https://soapbox.pub/agora,
brought in-app to explain how the platform works. Five sections:
1. Hero (dark navy + world-map texture + orange halos), Bebas Neue
italic headline with an inline orange highlighter behind the
brand name, three trust chips, and Donor / Activist Guide CTAs.
Tilted Venezuelan sample-campaign card on lg+, hidden on mobile.
On mobile the H1 fits on one line via text-4xl + a conditional
<br className="hidden sm:inline" />.
2. Three steps. No middleman. (cream) 3-up white cards with 4:3
step images and corner 01 / 02 / 03 numerals.
3. Two ways to get paid. (white) Bitcoin Public Payments vs.
Bitcoin Silent Payments compare cards with gradient header
strips. Public-Payments trade-off carries the above-ground-
activism warning; Silent-Payments trade-off is five bold-headline
bullets (few wallets, slow, no push notifications, buggy,
no public counts). Below: a primary-tinted No-Custody banner
plus a 3-column comparison grid (Unlike GoFundMe / Unlike
GiveSendGo / Unlike other 'Bitcoin' platforms).
4. Frequently asked. (cream) Three integrated FAQ chapters in
page flow (Getting started / Bitcoin donations / About Nostr),
each with a Bebas Neue numeral + Inter Bold heading + card-row
accordion items with a left orange accent on hover/open.
5. Pick the side you're on. (white) Two large image-led guide
cards (Donor / Activist) using the soapbox.pub photography.
Closes with a quiet 'Still stuck? Follow Team Soapbox' line
linking in-app to the pack via the /:nip19 route.
Typography is Bebas Neue (font-display) italic font-normal with
WebkitTextStroke for the hero H1 and the step numerals only; every
other heading uses Inter Bold (font-sans font-bold tracking-tight).
Bebas Neue is never font-bold (synthetic bold renders as smear at
display sizes).
Em dashes have been removed throughout the page and the
HelpFAQSection component. Box-drawing chars (U+2500) in section
banner comments are not em dashes and stay.
Section backgrounds alternate dark → cream → white → cream → white.
The dark and cream sections keep their literal palette in both light
and dark mode (an editorial choice that gives the page its
landing-page identity rather than being just another themed surface).
Donor + Activist Guides (new block-based design)
Both pages now compose from a typed sequence of GuideBlock variants
defined in helpContent.ts. Each block kind is rendered by a dedicated
component under src/components/guide/:
- GuideTLDR top-of-page summary card with lede + checklist
- GuideSteps numbered vertical flow of short steps
- PaymentComparisonTable Public vs. Silent side-by-side. Three-column
grid on desktop, two stacked tinted cards on
mobile. Row content driven by audience flag.
- CalloutCard tinted info / warning / danger / success blocks
- OptionGrid two-column tile grid for privacy and cash-out
options
- GuideProse plain prose escape hatch
- InlinePaymentBadge small pill that distinguishes the two payment
options
- index.ts barrel
Content is rewritten throughout to reflect current reality: campaigns
can accept Public only, Silent only, or both; the QR code carries
both endpoints when both are accepted; wallets without silent-
payments support fall back to a regular Bitcoin transaction; silent
payments are slow, scan-based, lack push notifications, are
bleeding-edge, and produce no public donation counts.
Activist Guide structure:
TLDR
How receiving works
What everyone can see (intentionally before the table)
Public vs. Silent comparison
A note on silent payments today (calm prose, not an alarm callout)
Move donations promptly
Cashing out privately (silent-payments hop, Lightning
swap, coinjoin, P2P with brokers,
spend it directly)
Avoid centralized tumblers
Donor Guide structure:
TLDR
How a donation flows
Public vs. Silent comparison
Public donations are visible on-chain forever callout
Donating privately option grid
Consumer apps can't make you anonymous callout
A note on silent payments today
Other touchpoints
- Sidebar (sidebarItems.tsx): Help label → About, icon LifeBuoy → Info,
path /help → /about.
- Top nav profile menu (TopNav.tsx): Help → About.
- Site footer (AppRouter.tsx inline): Help → About.
- AccountSwitcher dropdown: Help → About.
- LandingHero FAQ button → /about#faq.
- HelpTip popover footer link → /about#faq.
- GuideHero back link → /about, label 'Back to About', wider
max-w-5xl on lg+ container so it sits well on the now-full-bleed
hero. Inner overlay min-height bumped to 320px on lg+.
- CampaignsPage 'How it works' button → /about.
New assets in /public/about/ pulled from soapbox.pub:
- world-map-bg.png (hero + textures)
- venezuela-libertad-presos-politicos.png (hero sample-card image)
- donor-guide-freedom-libertad.jpeg
- activist-guide-unity.png
Step photos in /public/help/ (step-1-account.jpg, step-2-send.jpg,
step-3-spend.jpg) for the Three Steps section.
HelpFAQSection gains:
- variant: 'list' | 'cards' (default 'list')
- tabs: boolean (only meaningful with variant='cards')
- listTone: 'default' | 'reference' (quieter category labels and more
breathable accordion items for the About page; existing inline
callers keep the default pill style)
In 'reference' mode each accordion item gets a card-row treatment
(rounded white card, subtle border, hover lifts to primary/40 border,
left orange accent rule driven off data-state=open).
helpContent.ts FAQ content (FAQItem / FAQCategory and templates) is
left untouched. Only the donor/activist guide section was rewritten
into GuideBlock[] arrays.
The Ditto-era LayoutStore let pages push layout config (FAB, sidebars,
arc styling, max-width) up to MainLayout via an external store. After
the fundraiser refocus the only knob left is noMaxWidth, and the
`useLayoutEffect + deferred-rAF-reset` dance had a real race condition
where the previous page's options could bleed into the next page
during Suspense transitions — visible as max-w-3xl appearing on pages
that should be wide.
Switch to idiomatic React: two layout variants picked by the router.
- Narrow group (`max-w-3xl`): /feed, /notifications, /search, /profile,
/t/:tag, /g/:geohash, /settings/*, /wallet*, /bitcoin, /help/*,
/privacy, /safety, /changelog, /organizers, /remoteloginsuccess
- Wide group (no max-width): /, /campaigns/*, /groups/*, /pledges/*,
/dashboard, /i/*, /:nip19, catch-all NotFound
`FundraiserLayout` now takes a `narrow` prop and is mounted twice in
the route tree, once per group. Pages no longer reach upward — the
width is a property of the route they live in.
Delete useLayoutOptions and its 13 call sites, useLayoutSnapshot,
LayoutOptions, LayoutStore, and LayoutStoreContext. `LayoutContext.ts`
shrinks to just CenterColumnContext + useCenterColumn (still used by
WebxdcEmbed and NsitePreviewDialog for portal targets).
After removing the sidebar / mobile drawer chrome the registry's
remaining consumers are useFeedSettings, ProfileSearchDropdown, and
CONTENT_KIND_ICONS. Drop the helpers no one calls anymore:
- isSidebarDivider, nostrUriToNip19, nsiteUriToSubdomain
- getSidebarItem, sidebarItemIcon, itemLabel, itemPath, isItemActive
- The internal OPTIONAL_SIDEBAR_ITEMS / ALL_SIDEBAR_ITEMS /
SIDEBAR_ITEM_MAP machinery that only served those helpers
Fold the previously-optional 'dashboard' entry into the main
SIDEBAR_ITEMS list so SIDEBAR_ITEM_IDS still recognizes it.
The inlined FundraiserLayout only reads noMaxWidth and wrapperClassName
from the layout snapshot, so all other LayoutOptions fields (showFAB,
fabKind, fabHref, onFabClick, fabIcon, fabMenu, hasSubHeader,
rightSidebar, scrollContainer, noOverscroll, noArcs, hideTopBar,
hideBottomNav, fullBleed) are dead.
- Strip those fields from every useLayoutOptions call site (~25 pages)
- Remove the now-empty calls entirely where the result is { }
- Slim LayoutOptions to just noMaxWidth + wrapperClassName
- Remove the fullBleed preset expansion in useLayoutOptions
- Drop DrawerContext, useOpenDrawer, NavHiddenContext, useNavHidden
(no useful consumers without mobile bottom nav)
- Inline navHidden=false at the two SubHeaderBar / ProfileTabs sites
and drop the dead conditional classes
- Keep CenterColumnContext + useCenterColumn (still used by
WebxdcEmbed and NsitePreviewDialog for portal targets)
- Drop LiveStreamPage's chatSidebar (it relied on rightSidebar which
no longer exists)
- Drop ProfileSettings's useLayoutOptions sidebar injection
(ProfileRightSidebar is still rendered inline in the page)
The router-level layout introduced in 73bb2a17 hardcoded max-w-3xl on
every page, ignoring useLayoutOptions({ noMaxWidth: true }) and
wrapperClassName. Split FundraiserLayout into an outer provider and
an inner consumer so the inner component can read the snapshot, and
restore the conditional class composition the original
FundraiserLayout used. CampaignDetailPage and ActionDetailPage (and
any page using the fullBleed preset) render edge-to-edge again.
Move the protocol-level Nostr questions (what is Nostr, why is the
sign-in so long, what happens if I lose my key, password-manager use)
out of 'About Agora' and into a new 'About Nostr' category positioned
after the Bitcoin Donations section. Newcomers see what Agora is and
how Bitcoin works first, and only dig into Nostr's identity model
once they care.
Also reorder the silent-payments FAQ to sit above the Lightning one,
since silent payments are part of the answer to 'how do payments
work' while Lightning is the explanation of an absence.
Donations settle to the activist's wallet, not to abstract
'beneficiaries' — the wallet framing matches the rest of the app.
Adds a secondary outline button next to 'Start a campaign' that links
to /help so newcomers have an obvious next step before committing to
creating a campaign.
Silent payments still settle on-chain; the meaningful distinction is
public vs. private (a silent-payment transaction's output can't be
linked back to the recipient's reusable code). Reword help and guide
copy so 'on-chain' only appears where it correctly contrasts with
Lightning or describes chain mechanics that apply to both kinds of
payment.
Activists now pick public, private, or both when creating a campaign;
both generates a combined BIP-21 QR so SP-aware wallets pay privately
and others fall back to on-chain. Update the FAQ, Donor Guide, and
Activist Guide accordingly:
- Recommend Ditto Wallet and Dana for donors who want privacy.
- Lead the activist cashout path with moving funds into a silent-
payments wallet first, then spending onward with the trail broken.
- Flip the 'why no silent payments' FAQ into 'yes, we support them'.
- Note that silent-payment donations are excluded from public donor
lists and totals by design.
The BlindBit Oracle exposes only per-block endpoints, so a 144-block
'Scan recent' was up to ~288 sequential HTTP round trips against the
public mainnet indexer (~700ms each = ~200s wall clock before any ECDH
math). Profiling against the configured indexer showed latency, not
ECDH or bandwidth, was the dominant cost — and that the server happily
handled 10 concurrent requests with ~6x speedup.
Replace the sequential for-loop in scanRange with a sliding-window
pipeline: keep up to SCAN_FETCH_CONCURRENCY (8) fetchBlockEntries
calls in flight, but process completed blocks strictly in height order
so optimisticRef, matchesFound, scan-progress, and the contiguous
scanHeight advancement stay single-writer and monotonic.
Cancel and error semantics are preserved:
- cancelScan still aborts every in-flight fetch via the controller.
- First in-order fetch failure aborts the rest of the scan, same as
before. The finally block drains still-pending fetches so their
rejections don't surface as unhandled promise rejections.
- Card grid: remove the corner "Private" badge on silent-payment cards.
- Detail hero: remove the "Private campaign" badge above the title.
- Detail donate aside: drop the "Private campaign — totals not public"
line; silent-payment campaigns now show only the goal target (if any)
in place of the raised/progress block.
The donate panel already makes it clear which wallet endpoints are
available, so the redundant privacy chrome was just noise.
For dual-wallet campaigns the QR already encodes a single BIP-21 URI
(`bitcoin:<addr>?sp=<sp>`). Mirror that in the copyable row instead of
showing one row per endpoint — modern wallets parse the URI in their
recipient field and BIP-352-aware ones pick up the `sp=` parameter
automatically. Single-endpoint campaigns still show the raw value with
a label-appropriate toast.
Also remove the on-chain traceability disclaimer and the silent-payment
unlinkability notice — both are noise on the donation panel.
The chip + 'Add another address' UX was too clever. Replace it with:
- A 'wallet source' dropdown ('My wallet' / 'Custom'), defaulting to
'My wallet' for nsec users and matching the pre-dual-wallet form.
- An 'accept' dropdown beneath it (only shown when source is 'My
wallet') that picks which donation types to accept: all (default),
public only, or private only. The 'all' and 'private' options are
disabled when the active login can't derive a silent-payment code.
When source is 'Custom' we still surface separate bc1 and sp1 inputs
so the user can publish a dual-endpoint campaign with addresses they
hold elsewhere. At least one of them must parse.
Without nsec access the dropdowns are skipped entirely and the two
custom inputs are shown directly \u2014 same as before.
Edit mode still starts in 'Custom' with the existing values pre-filled
so a no-op edit doesn't surprise the user by re-deriving HD
endpoints (or burning a receive index).
The on-chain traceability and silent-payment privacy notices stay
where they matter — on the campaign detail page's donate panel, where
donors actually see them. The form page no longer needs them
duplicated under the picker.
A campaign may now declare up to two `w` tags — at most one mainnet
on-chain address (bc1q…/bc1p…) and at most one silent-payment code
(sp1…) — and the QR/payment panel combines them into a single BIP-21
URI (`bitcoin:<bc1>?sp=<sp1>`) when both are present. BIP-352-aware
wallets pick the SP parameter automatically; legacy wallets fall back
to the on-chain address.
The campaign form is reorganized around the dual-endpoint model. Users
with nsec access see two avatar chips — "My wallet" and "My private
wallet" — both selected by default and an "Add another address"
disclosure that reveals separate bc1 and sp1 inputs. A typed value
wins over the corresponding chip's HD-derived value, so a cold-storage
address can be substituted without giving up the SP code. Users
without nsec access (extension / bunker logins) see the two custom
inputs unconditionally. At least one of the four sources must resolve.
The on-chain receive-index cursor is still advanced only at publish
time, and now only when "My wallet" is selected AND no custom
on-chain value was provided — so the cursor never burns on a no-op
edit or on a publish where the user overrode the chip with their own
address.
`ParsedCampaign.wallet` is replaced by `ParsedCampaign.wallets`, a
`{ onchain?, sp? }` struct. Consumers (`useCampaignDonations`,
`useDonateCampaign`, `useProfileCampaignStats`, `useOnchainZaps`,
`CampaignCard`, `CampaignDetailPage`, profile rails) keep their
existing on-chain semantics by reading `wallets.onchain`. The
"Private campaign" badge and hidden-aggregates UI now trigger on
SP-only campaigns (no on-chain endpoint), matching the spec.
- Change non-nsec wallet fallback from 'View wallet' to 'Wallet details'
- Replace exact Blockbook/Esplora call counts in module JSDoc with
behavior-focused wording that won't drift when hook internals change
Logged-in users reach Help through the account menu (AccountSwitcher
dropdown), but logged-out users have no equivalent affordance — the
default sidebar order doesn't include Help, so it was buried in the
"More…" menu. Force it into the main list when there's no user, and
suppress the duplicate in the hidden-items menu.
- Replace removed useBitcoinWallet with useHdWallet + useBtcPrice
- Show graceful 'View wallet' fallback for non-nsec logins
- Drop includeArchived and recipientPubkeys (removed from useCampaigns)
- Remove beneficiary campaign query and 'Started for you' shelf
(campaigns are now self-authored; author = beneficiary)
- Simplify hero card from 4 stat tiles to 3
- Update stale JSDoc referencing Esplora and old query costs
Per-campaign 'raised' was the sum of verified kind 8333 donation receipts:
each receipt's tx was re-fetched and its outputs paying the campaign's `w`
address were summed. That counted only donations whose donor published a
receipt — direct on-chain payments were ignored — and required N `/tx/`
lookups per campaign view.
Source `totalSats` from a single `/address/{w}` lookup against the
configured Esplora endpoint (default: mempool.space) and use
`chain_stats.funded_txo_sum` (lifetime received). Any payment to the
address now counts, and the progress bar does not regress when the
beneficiary spends.
Kind 8333 receipts are still fetched and verified to power the donor list,
donor count, and per-tx breakdown — they just no longer drive the headline
number.
Silent-payment campaigns are unchanged (no observable balance).
`useProfileCampaignStats` and the `SortedByTopGrid` on the profile
campaigns tab switch to the same address-balance source.
The nsec paste guard (useNsecPasteGuard) bails out when the paste
target has id="nsec" or sits inside [data-nsec-allowed], but the
login form had neither marker, so pasting an nsec into the login
field triggered the "Secret key detected" toast and was blocked.
Removes the 'Donate' button that opened the PSBT-signing
DonateDialog above the wallet QR/address panel. Agora no longer
runs the in-app on-chain donate flow — donors pay from an
external wallet via the QR code, the same path silent-payment
campaigns already used.
The DonateDialog component and useDonateCampaign hook stay in
the tree for now; they're still wired into the profile rail's
campaign-donate dropdown.
The contrastForeground() helper relied on isDarkTheme(), which only
treats a color as dark when its luminance < 0.2. The default orange
primary (24 100% 50%) has luminance ~0.31, so it was classified as a
light background and got dark text — black letters on orange buttons
throughout the site. Same problem hit most saturated mid-lightness
brand colors (red, blue, purple, green).
Raise the threshold to 0.55 so saturated mid-lightness colors get
white text while genuinely light pastels (pink theme, sunset, light
mode background) still get dark text.