Handbook
This is the 21.gifts app handbook: screens, functions, and HTTP endpoints. The api handbook lives in 21gifts/api.
21.gifts app handbook
This handbook is mandatory. Every exported function, every UI screen, and every HTTP endpoint in this repository must have a section here. CI (npm run handbook:check) fails the PR if a heading is missing or the section is a stub.
- Screens:
## Screen: /path - Screen variants:
### Variant: idunder each screen (every distinct UI state) - Functions:
## Function: name - Endpoints:
## Endpoint: METHOD /path
Do not merge a PR that adds a screen, export, or endpoint without updating this handbook in the same PR. Undeclared gaps are rejected, not discussed.
Every screen variant also needs a committed screenshot under docs/handbook/images/ (and public/handbook-images/). Default screens plus every exported function need a Playwright Linux baseline. npm run screenshot:check fails the PR when either is missing. The variant list lives in scripts/screen-variants.mjs.
See screens.md, functions.md, and endpoints.md.
Screens
Screen: /
- URL:
/— public marketing landing (no auth gate). - What the user sees: Dark 21.gifts header with a language switcher, headline about peer-to-peer Bitcoin gifts, How it works (login and Wallet of Satoshi address) / Why / FAQ, CTAs Ask for help (
/login) and Send help (/donate). - Actions: Read the pitch, change language, open login or donate, jump to in-page sections, open Stats, open Legal & Privacy, open the Handbook.
- Calls:
Home(src/app/(marketing)/page.tsx) insideMarketingLayout,LanguageSwitcher.
Variant: default
Desktop/wide layout: section nav is visible in the header (How it works, Why, FAQ, Stats, Handbook, Log in). No hamburger.

Variant: mobile-nav
Narrow viewport: header shows the Menu button. Open it to reveal the same links stacked. Tapping a link closes the menu.

Screen: /legal
- URL:
/legal— imprint and privacy./legal.htmlpermanently redirects here. - What the user sees: Dark 21.gifts header with a language switcher, Legal Notice (Switzerland, [email protected]) and Privacy Policy (no analytics; no cookies unless the visitor chooses a language — then a
localecookie; session in localStorage; Cloudflare TLS; login on this origin). Legal body copy stays English. - Actions: Change language. Read the legal body. Header Log in goes to
/login. - Calls:
LegalPageinsideMarketingLayout,LanguageSwitcher.
Variant: default
The only state: imprint plus privacy, marketing chrome.

Screen: /stats/[day]
- URL:
/stats/YYYY-MM-DD— public list of outbound gifts that UTC day. Invalid dates 404. - What the user sees: Dark 21.gifts header, All stats back to
/stats, heading Gifts on {day}, a UTC day date input, then either the gift table (Time, Recipient, Sats, BTC, USD), empty copy No gifts recorded on this day., Loading…, or Try again. Stats body copy stays English. - Actions: Pick another UTC day in the date input (navigates to
/stats/{next}). Open All stats. Change language. Header Log in goes to/login. - Calls:
GiftDayPage,DayLoader,GiftDayTable,fetchGiftDay(GET /gifts?day=). - Auth: None.
Variant: default
Loaded day with at least one gift row (recipient alice).

Variant: empty
No gifts that UTC day. Copy No gifts recorded on this day.

Variant: loading
Waiting on GET /gifts. Copy Loading…

Variant: error
Fetch failed. Button Try again.

Screen: /stats
- URL:
/stats— public gift totals (no auth gate). - What the user sees: Dark 21.gifts header with a language switcher, heading Gifts, four KPI cards (total spent in BTC and USD with a sats caption, gifts, people, period), then diagrams: Total spend over time (one cumulative chart; days with spend are markers on the series, not a wrapping date list), By person and By month. Each diagram has a BTC/USD control that defaults to BTC; over time switches the series, person and month rescale bar size while labels stay both units. Empty database copy: No gifts recorded yet. Stats body copy stays English.
- Actions: Change language. Read the charts. Open a spend day (
/stats/{YYYY-MM-DD}) from Total spend over time by clicking a day with spend. Switch Total spend over time / By person / By month between BTC and USD. Header Stats stays on this page; Log in goes to/login. - Calls:
StatsPage,StatsLoader,StatsDashboard,fetchGiftStats(same-originGET /gifts/stats),LanguageSwitcher.
Variant: default
Loaded stats with one cumulative over-time chart visible. Scale defaults to BTC.

Variant: usd-scale
Inverted ranking fixture (June tall in BTC / short in USD, July the reverse). Scale switched to USD on Total spend over time, By person, and By month.

Variant: empty
Zero gifts. KPI zeros and No gifts recorded yet.

Variant: loading
Waiting on GET /gifts/stats. Copy Loading…

Variant: error
Fetch failed. Copy Could not load gift stats. Please try again. and Try again.

Screen: /login
- URL:
/login— login only. - What the user sees: Light language switcher top-right on the page (not the marketing header). Idle Log in, or signed-in account. Error is terminal until Try again.
- Actions: Change language. Log in (existing login, or create one when the browser has none). When signed in, set a display name, link/unlink a Wallet of Satoshi address, and log out. No client redirect to
/. - Calls:
LoginCard,NameForm,LightningAddressForm,usePasskeyLogin,useAuthStore,LanguageSwitcher.
Variant: idle
Logged out. Heading Sign in to 21.gifts, one Log in button.

Variant: starting
Transient after a login click, before the ceremony finishes: spinner and Preparing your login….

Variant: error
Login begin or finish failed. Copy Something went wrong. Please try again. and Try again.

Variant: signed-in
Session present, no name and no Wallet of Satoshi address yet. Signed in, role, name form, address form, Log out.

Variant: signed-in-named
Signed in with a display name set. Shows the name plus Edit, and the empty address form.

Variant: signed-in-linked
Signed in with an address on the account. Name form (set or Edit) plus the address with Edit / Unlink (no verification UI).

Screen: /donate
- URL:
/donate— guest Bitcoin gift. No login required. - What the user sees: Light language switcher top-right on the page (not the marketing header). Page heading Send a gift, form heading Send Bitcoin, Wallet of Satoshi address field, sat amount (no comment), Continue, then a QR and Open Wallet of Satoshi — or a validation/range error on the form.
- Actions: Change language. Enter a Wallet of Satoshi address and amount, continue, pay with Wallet of Satoshi.
- Calls:
DonateForm,resolveLightningAddress,requestDonateInvoice,satsToMsat,QrCode,isAndroidUserAgent,walletOfSatoshiHref,walletOfSatoshiIntentHref,LanguageSwitcher.
Variant: form
Empty/idle form, submit enabled.

Variant: busy
Payment request in flight: spinner on Continue, extra Cancel button.

Variant: validation-error
Submit with a blank address (or invalid amount). An alert explains what to fix; no payment QR yet.

Variant: invoice
Successful create: Pay N sats to address, Bitcoin payment QR, Open Wallet of Satoshi.

Variant: invoice-android
Same payment card, but Open Wallet of Satoshi is an Android Intent that pins package com.livingroomofsatoshi.wallet. The pixels match the desktop invoice variant.

Screen: /handbook
- URL:
/handbook— public app handbook (no auth gate). - What the user sees: Localized heading Handbook and intro chrome, language switcher in the marketing header, intro with a link to the api handbook on GitHub (
21gifts/api), in-page nav (Overview / Screens / Functions / Endpoints) each with a link icon, then the fourdocs/handbook/markdown files rendered as HTML (English bodies). Every markdown heading has a sibling link icon. - Actions: Change language, read the docs, jump via the section nav, copy a chapter or heading URL (click the link icon → check icon for 1.2s, hash updates), follow the api handbook link, follow in-page markdown links.
- Calls:
HandbookPage,HandbookIntro,HandbookCopyLink,loadHandbookDocuments,HandbookMarkdown(parseHandbookMarkdown),LanguageSwitcher.
Variant: default
Idle copy buttons: every heading and chapter shows the link icon.

Variant: copied
After tapping the link icon on a heading or chapter, that button shows the check icon and data-copied, and location.hash is that id. Other copy buttons stay idle link icons.

Screen: /404
- URL: any unknown path (App Router
not-found.tsx). There is nopage.tsxfor/404; Playwright usespage.goto('/404')which hits this screen. - What the user sees: Marketing chrome with a language switcher, heading 404, This page does not exist., Back home.
- Actions: Change language, go home, or use header/footer links.
- Calls:
NotFound,MarketingHeader,MarketingFooter,LanguageSwitcher.
Variant: default
The only state.

Functions
Function: DonateForm
- Purpose: Renders the guest donate form (Wallet of Satoshi address and sat amount only; no comment) and, after success, the Bitcoin payment QR. All visitor-facing copy goes through
useTranslations. - Inputs: Form state: address and whole-sat amount. Validation and request failures are typed keys (
address/amount/range/request) souseTranslationsre-renders them after a locale change. - Returns / side effects: React element. Side effects: HTTP to the api then GET the payee LNURL-pay callback.
- Used by: Screen
/donate.
Function: DonatePage
- Purpose: Next.js page for
/donatewith localized heading and a light language switcher. - Inputs: None. Calls
getRequestLocale()for the page title. - Returns / side effects: The donate screen wrapped in the root layout; switcher top-right.
- Used by: Route
/donate.
Function: GET
- Purpose: Shared export name for App Router GET handlers. Healthz uses
export function GET; same-origin api proxies re-export unique functions asGET. - Inputs: Incoming
Requeston proxy routes; none on healthz. - Returns / side effects:
Response. Healthz is{ status: 'ok' }200; proxies return the upstream api response. - Used by: Container probes, browser/wallet same-origin calls.
Function: HandbookCopyLink
- Purpose: Client button beside a handbook heading or chapter. Copies
origin + pathname + #idto the clipboard, setslocation.hash, and flashes a check icon for 1.2s (textareaexecCommandfallback). - Inputs:
targetId(DOM id without#) andlabel(interpolated intohandbook.copyLinkviauseTranslationsas{ label }). - Visible UI: Idle
Link2icon; copiedCheckicon. No visible "Copy link" or "Copied" text (titleandaria-labelkeep the accessible name). - Returns / side effects: A
<button type="button">. Clipboard write; hash update. No network. - Used by:
HandbookPage(page title and chapter nav) andHandbookMarkdown(every heading).
Function: HandbookMarkdown
- Purpose: Render parsed handbook markdown as Tailwind-styled headings, paragraphs, lists, links, and images. Every heading has a sibling
HandbookCopyLink. - Inputs:
markdownstring andidPrefixfor heading ids. - Returns / side effects: React fragment. No network.
- Used by:
HandbookPagefor each handbook document.
Function: HandbookIntro
- Purpose: Server-presentational chrome for the
/handbooktitle, intro sentence, and section-navaria-label(already-translated copy). - Inputs:
title,introBefore,introAfter,navAria(already-translated strings),headingAction(node beside the h1, e.g. copy-link), andchildren(the section links). - Returns / side effects: Heading, intro with the api-handbook GitHub link, and a nav whose accessible name comes from
navAria. No network. - Used by:
HandbookPage.
Function: HandbookPage
- Purpose: Async Next.js page for
/handbook. Resolves locale viagetRequestLocale, loads the four app handbook files from disk, and renders them with a link to the api handbook. Title copy-link useshandbook.title; chapter copy-links usehandbook.chapterLabelwith{ title }; intro chrome via already-translated props onHandbookIntro; markdown bodies stay English. - Inputs: None (calls
getRequestLocale(); readsdocs/handbook/from disk at request time; the standalone image copies that tree). - Returns / side effects: The handbook screen inside
MarketingLayout. - Used by: Route
/handbook.
Function: StatsLoader
- Purpose: Client loader for
/stats. Fetches gift totals on mount and retry, ignores stale responses after unmount, and rendersStatsDashboard. - Inputs: None.
- Returns / side effects: React element. Calls
fetchGiftStats. - Used by:
StatsPage.
Function: StatsDashboard
- Purpose: Renders gift KPIs (BTC + USD totals with a sats caption) and SVG diagrams (cumulative spend over time, by person, by month), plus loading/error/empty states. Total spend over time links each non-zero UTC day on the chart (not as a wrapping text list) to
/stats/{day}. Each of Total spend over time, By person, and By month has a BTC/USD control (default BTC). Over time shows one cumulative series. Person and month rescale bar size while labels stay both units. - Inputs:
stats,error,loading,onRetry. - Returns / side effects: React element. No network.
- Used by:
StatsLoader.
Function: StatsPage
- Purpose: Next.js page for
/stats. RendersStatsLoader. - Inputs: None.
- Returns / side effects: The statistics screen inside
MarketingLayout. RendersStatsLoader. - Used by: Route
/stats.
Function: GiftDayPage
- Purpose: Next.js page for
/stats/[day]. Invalid UTC days callnotFound(). Valid days renderDayLoader. - Inputs:
paramsPromise{ day }. - Returns / side effects: The day screen inside
MarketingLayout. - Used by: Route
/stats/[day].
Function: DayLoader
- Purpose: Client loader for
/stats/[day]. FetchesGET /gifts?day=, date input navigates, retry on error. - Inputs:
dayUTCYYYY-MM-DD. - Returns / side effects: React element. Calls
fetchGiftDay. - Used by:
GiftDayPage.
Function: GiftDayTable
- Purpose: Table of individual gifts on one UTC day (time, recipient, sats, BTC, USD), or empty copy.
- Inputs:
day: GiftDay. - Returns / side effects: React element. No network.
- Used by:
DayLoader.
Function: fetchGiftDay
- Purpose: GET
/gifts?day=and parse the per-day gift list payload. - Inputs: UTC
daystring. - Returns / side effects:
GiftDay. Throws visitor copy on non-OK or invalid JSON. - Used by:
DayLoader.
Function: isUtcDay
- Purpose: Validate a UTC calendar day string
YYYY-MM-DD. - Inputs: Candidate
day. - Returns / side effects:
trueonly for a real calendar date. No I/O. - Used by:
GiftDayPage,DayLoader.
Function: proxyGiftsGet
- Purpose: Same-origin proxy helper for api
GET /gifts(forwardsday). - Inputs: Incoming
Request. - Returns / side effects: Upstream
Response. - Used by: Route GET
/gifts.
Function: Home
- Purpose: Next.js page for
/. Marketing landing: pitch, how it works, why, FAQ, CTAs to/loginand/donate, all viatranslatefor the negotiated locale. - Inputs: None. Calls
getRequestLocale(). - Returns / side effects: The home screen element.
- Used by: Route
/.
Function: LanguageSwitcher
- Purpose: Native language
<select>that persists the visitor's override in alocalecookie and refreshes the App Router tree. - Inputs:
tone(darkfor marketing chrome,lightfor login/donate). Reads current locale viauseTranslations. - Returns / side effects: Select with native option labels (English/Deutsch/Español/Filipino). On change writes
locale=<code>; Path=/; Max-Age=31536000; SameSite=Laxand; Secureon HTTPS, thenrouter.refresh(). Never set on first visit. - Used by:
MarketingHeader(always visible),/login, and/donate.
Function: NameForm
- Purpose: Logged-in form to set or edit a display name.
- Inputs: Reads
useAuthStore. User input: name string. Visitor-facing copy viauseTranslations. Empty and request failures are typed keys so they re-render after a locale change. - Returns / side effects: React element or
nullwhen logged out. POST/me/nameon save. - Used by:
LoginCardsigned-in view on screen/login(not on/).
Function: LightningAddressForm
- Purpose: Logged-in form to link, edit, or unlink a Wallet of Satoshi address.
- Inputs: Reads
useAuthStore. User input: address string. Visitor-facing copy viauseTranslations. Request failures usela.errorRequestso they re-render after a locale change. - Returns / side effects: React element or
nullwhen logged out. - Used by:
LoginCardsigned-in view on screen/login(not on/).
Function: LocaleProvider
- Purpose: Client context provider that exposes the negotiated locale and a bound
thelper to visitor-facing components. - Inputs:
locale,messagesfor that locale, andchildren. - Returns / side effects: React provider element. No network; does not write cookies.
- Used by:
RootLayoutwraps every page; consumed viauseTranslations(see that function).
Function: LoginCard
- Purpose: Login UI: one Log in button (existing login, or create when the browser has none), then signed-in view with
NameFormandLightningAddressForm. Visitor-facing copy viauseTranslations. - Inputs: Uses
usePasskeyLoginanduseAuthStore. Rehydrates vialoadSession+fetchMewithout overwriting a newer in-page session or restoring after logout. A rejected token callsclearAuthwhen the in-memory session is absent or still that token. Successful hydration cancels an in-flight login ceremony. Logout cancels an in-flight login ceremony. Unmount invalidates in-flight hydration. - Returns / side effects: React element covering idle/starting/error/signed-in. Does not navigate away from
/login. - Used by: Screen
/login.
Function: LoginPage
- Purpose: Next.js page for
/loginwith localized heading and a light language switcher. - Inputs: None. Calls
getRequestLocale()for the page title. - Returns / side effects: Renders
LoginCardandLanguageSwitcher(top-right). - Used by: Route
/login.
Function: QrCode
- Purpose: SVG QR for a string (LNURL or bolt11).
- Inputs:
value(required) andlabel(required accessible name, already translated). - Returns / side effects: React element.
- Used by:
DonateForm.
Function: RootLayout
- Purpose: Root HTML shell: negotiated
lang(en/de/es/fil), global CSS, English metadata (title, icons, Open Graph, Twitter), andLocaleProviderwith the request catalog. - Inputs:
childrenReact nodes. CallsgetRequestLocale()forhtml langand messages. - Returns / side effects: The document wrapper for every route.
- Used by: All screens.
Function: clearSession
- Purpose: Removes the bearer token from
localStorage. - Inputs: None.
- Returns / side effects: void. No-op during SSR (
windowundefined). - Used by:
useAuthStore.clearAuth.
Function: fetchGiftStats
- Purpose: GET
/gifts/statsand parse the public gift totals payload. - Inputs: None.
- Returns / side effects:
GiftStats. Throws visitor copy when the api is down or the body is invalid. - Used by:
StatsLoader.
Function: fetchMe
- Purpose: GET
/mewith the bearer session. - Inputs:
sessionToken. - Returns / side effects:
Accountornullon 401. - Used by:
LoginCardsession hydration.
Function: formatBtcTick
- Purpose: Formats a parsed BTC chart-axis value with up to 8 decimals, trailing zeros trimmed.
- Inputs:
btcnumber (layout scale only). - Returns / side effects: Trimmed decimal string (e.g.
0.015). - Used by:
StatsDashboardBTC-over-time chart.
Function: formatMsatAsSats
- Purpose: Formats millisatoshis as an English sat string (
1 sat/{n} sats). - Inputs:
msatnumber. - Returns / side effects: Decimal string in sats.
- Used by: Unit tests (
lnurl-pay.test.ts). The donate UI formats amounts via catalog keysdonate.satOne/donate.satsinstead.
Function: formatUsdDisplay
- Purpose: Formats an API USD amount string (
"1425.00") as en-US currency for the stats hero. - Inputs:
usdstring fromGET /gifts/stats. - Returns / side effects: Locale currency string such as
$1,425.00. - Used by:
StatsDashboard.
Function: formatUsdTick
- Purpose: Formats a parsed USD chart-axis value as a grouped dollar label.
- Inputs:
usdnumber (layout scale only). - Returns / side effects: Label such as
$1,234. - Used by:
StatsDashboardUSD-over-time chart.
Function: getApiUrl
- Purpose: Reads
NEXT_PUBLIC_API_URLvia the typed config accessor. - Inputs: None.
- Returns / side effects: Origin string. Throws if unset/empty (entrypoint must substitute).
- Used by:
proxyApiRequest(server-side upstream origin).
Function: getCatalog
- Purpose: Return the message catalog for a supported UI locale without indexed-access gaps.
- Inputs:
locale(en/de/es/fil). - Returns / side effects: The
Messagesobject for that locale. Exhaustive switch overLocale. - Used by:
RootLayout,Home,/login,/donate,NotFound,MarketingFooter,HandbookPage, and therenderWithLocaletest helper.
Function: getRequestLocale
- Purpose: Resolve the UI locale for the current request without writing cookies.
- Inputs: Reads the
localecookie and theAccept-Languageheader vianext/headers(both async in Next 15). - Returns / side effects: A supported locale (
en/de/es/fil). Valid cookie wins; invalid/missing cookie falls through toparseAcceptLanguage; unmatched →en. - Used by:
RootLayout,Home,/login,/donate,NotFound,MarketingFooter, andHandbookPage. Lives insrc/lib/request-locale.tsso client components can import locale constants withoutnext/headers.
Function: isAndroidUserAgent
- Purpose: Detects Android so the WoS CTA can use an Intent URL.
- Inputs:
userAgentstring. - Returns / side effects:
trueiff/Android/imatches. - Used by:
DonateForm.
Function: loadHandbookDocuments
- Purpose: Read the four app handbook markdown files from disk (README, screens, functions, endpoints).
- Inputs: Optional
rootDir; defaults to<cwd>/docs/handbook. - Returns / side effects:
HandbookDocument[]in that order. Throws when the directory or a required file is missing. - Used by:
HandbookPage.
Function: loadSession
- Purpose: Reads the bearer token from
localStorage. - Inputs: None.
- Returns / side effects: Token string or
null. SSR-safe. - Used by:
LoginCardon mount.
Function: parseAcceptLanguage
- Purpose: Negotiate a supported UI locale from an RFC 7231
Accept-Languageheader. - Inputs: Raw header string (may be empty). Splits on commas. A missing
qdefaults to 1. A bareq, empty/invalid qvalue, or duplicateqdiscards that language-range. Maps primary subtags (en/de/es/fil, andtl→fil). - Returns / side effects: Among valid mapped ranges with
q > 0, highestq, then earlier header position, thenLOCALESorder. No positive assignment →en. Pure function — no I/O. - Used by:
getRequestLocalewhen no validlocalecookie is present.
Function: parseHandbookMarkdown
- Purpose: Parse handbook markdown into headings, paragraphs, and lists with inline code, strong, links, and images.
- Inputs:
markdownstring andidPrefixfor ids and in-page hashes. - Returns / side effects:
HandbookBlock[]. Drops unsafe hrefs (.., unknown schemes). - Used by:
HandbookMarkdown.
Function: parseSupportedLocale
- Purpose: Accept a string only when it is exactly one of
en/de/es/fil. - Inputs: Raw cookie or
<select>value, orundefined. - Returns / side effects: That locale, or
null. Pure function — no I/O. - Used by:
getRequestLocale(cookie) andLanguageSwitcher(option value).
Function: requestDonateInvoice
- Purpose: GET an LNURL-pay callback with
amountmillisatoshis and return the bolt11 string. - Inputs:
{ callback, amountMsat, fetchImpl? }. Does not resolve a Lightning Address. - Returns / side effects: bolt11
string, or throws. - Used by:
DonateForm.
Function: resolveLightningAddress
- Purpose: GET
/lightning-address?address=on the 21.gifts api. - Inputs:
address. - Returns / side effects: Resolved LNURL-pay metadata (callback, min/max).
- Used by:
DonateFormbefore paying.
Function: satsToMsat
- Purpose: Converts whole sats to millisatoshis.
- Inputs:
satsnumber. - Returns / side effects:
sats * 1000. - Used by:
DonateForm(converts sats before callingrequestDonateInvoice).
Function: saveSession
- Purpose: Writes the bearer token to
localStorage. - Inputs:
tokenstring. - Returns / side effects: void. SSR no-op.
- Used by:
useAuthStore.setAuth.
Function: setName
- Purpose: POST
/me/name. - Inputs:
sessionToken,name. - Returns / side effects: Updated
Account. - Used by:
NameForm.
Function: setLightningAddress
- Purpose: POST
/me/lightning-address. - Inputs:
sessionToken,address. - Returns / side effects: Updated
Account. - Used by:
LightningAddressForm.
Function: translate
- Purpose: Look up a catalog key and replace
{name}placeholders fromvars. - Inputs:
catalog(Messages),key(MessageKey), optionalvarsmap of string/number values. - Returns / side effects: Interpolated string. Throws on a missing key or missing
{name}— no silent English fallback. - Used by: Server pages (
Home, login/donate headings,NotFound,MarketingFooter,HandbookPage) and thethelper fromLocaleProvider/useTranslations.
Function: unlinkLightningAddress
- Purpose: DELETE
/me/lightning-address. - Inputs:
sessionToken. - Returns / side effects: Updated
Accountwith address cleared. - Used by:
LightningAddressForm.
Function: uppercaseLnurl
- Purpose: Uppercases a bech32 LNURL or BOLT11 payment request.
- Inputs:
lnurlstring. - Returns / side effects: Uppercase string.
- Used by:
walletOfSatoshiHrefandwalletOfSatoshiIntentHref(DonateForm).
Function: useAuthStore
- Purpose: Zustand store for
session+account. Hydration is explicit (no module-initlocalStorage). - Inputs: Hook. Methods
setAuth,setAccount,clearAuth. - Returns / side effects: Auth state object.
- Used by:
LoginCard,NameForm,LightningAddressFormon/login(not/).
Function: useTranslations
- Purpose: Client hook returning
{ locale, t }from the nearestLocaleProvider. - Inputs: None (React context).
- Returns / side effects: Active locale and a
t(key, vars?)bound to that catalog. Throws if used outsideLocaleProvider. - Used by:
MarketingHeader,LanguageSwitcher,LoginCard,LightningAddressForm,DonateForm,NameForm,HandbookCopyLink.
Function: walletOfSatoshiHref
- Purpose: iOS/desktop WoS deep link.
- Inputs: Bech32 LNURL or BOLT11 payment request.
- Returns / side effects:
walletofsatoshi:lightning:+ uppercase payload. - Used by:
DonateFormwhen not Android.
Function: walletOfSatoshiIntentHref
- Purpose: Android Chrome Intent pinning the WoS package.
- Inputs: Bech32 LNURL or BOLT11 payment request.
- Returns / side effects:
intent:lightning:…#Intent;scheme=walletofsatoshi;package=com.livingroomofsatoshi.wallet;…;end. - Used by:
DonateFormon Android.
Function: DELETE
- Purpose: App Router DELETE export on
/me/lightning-address(re-export ofproxyMeLightningAddressDelete). - Inputs: Incoming
Request. - Returns / side effects: Upstream api
Response. - Used by: Same-origin
unlinkLightningAddress.
Function: LegalPage
- Purpose: Next.js page for
/legal(imprint and privacy). - Inputs: None.
- Returns / side effects: The legal screen.
- Used by: Route
/legal.
Function: MarketingFooter
- Purpose: Footer for marketing pages: wordmark, localized section links, legal, GitHub.
- Inputs: None. Resolves locale via
getRequestLocaleand reads copy from the catalog viatranslate. - Returns / side effects: Footer element.
- Used by:
MarketingLayout,NotFound.
Function: MarketingHeader
- Purpose: Sticky marketing header with wordmark, section nav, always-visible
LanguageSwitcher(tone="dark"), login CTA, and mobile menu. - Inputs: None (internal open state). Reads copy via
useTranslations. - Returns / side effects: Header element; toggles nav on small screens. Language select stays visible when the hamburger is closed.
- Used by:
MarketingLayout,NotFound.
Function: MarketingLayout
- Purpose: Async dark full-page shell for
/,/legal,/handbook, and/stats. - Inputs:
children. AwaitsMarketingFooter()(does not render it as a JSX child). - Returns / side effects: Wrapper div with header, page, and awaited footer.
- Used by: Marketing route group.
Function: NotFound
- Purpose: Async app-wide 404 screen with marketing chrome and a localized link home.
- Inputs: None. Calls
getRequestLocale()for body/back-link copy; awaitsMarketingFooter(). - Returns / side effects: 404 element with
MarketingHeaderand awaited footer (not rendered as JSX child). - Used by: Next.js
not-found.tsx.
Function: POST
- Purpose: Shared App Router POST export name.
/me/namere-exportsproxyMeNamePost;/me/lightning-addressre-exportsproxyMeLightningAddressPost;/auth/passkey/{register,authenticate}/{begin,finish}re-export the four passkey proxy POSTs. - Inputs: Incoming
Request. - Returns / side effects: Upstream api
Response. - Used by: Same-origin name save, address link, and passkey begin/finish.
Function: proxyApiRequest
- Purpose: Forwards an App Router request to
getApiUrl()+ path, copying query, body, and authorization / content-type / user-agent / origin headers. - Inputs:
request,apiPathbeginning with/. - Returns / side effects: Upstream
Response, or 502 JSON if fetch throws. - Used by: All same-origin api proxy route handlers.
Function: proxyGiftsStatsGet
- Purpose: Same-origin proxy helper for api
GET /gifts/stats. - Inputs: Incoming
Request. - Returns / side effects: Upstream
ResponseviaproxyApiRequest. - Used by: Route GET
/gifts/stats.
Function: proxyLightningAddressGet
- Purpose: Proxies GET
/lightning-address. - Inputs:
Requestwithaddressquery. - Returns / side effects: Upstream
Response. - Used by: Route GET
/lightning-address.
Function: proxyMeNamePost
- Purpose: Proxies POST
/me/name. - Inputs:
Requestwith JSON body. - Returns / side effects: Upstream
Response. - Used by: Route POST
/me/name.
Function: proxyMeGet
- Purpose: Proxies GET
/me. - Inputs:
Requestwith Bearer token. - Returns / side effects: Upstream
Response. - Used by: Route GET
/me.
Function: proxyMeLightningAddressDelete
- Purpose: Proxies DELETE
/me/lightning-address. - Inputs:
Request. - Returns / side effects: Upstream
Response. - Used by: Route DELETE
/me/lightning-address.
Function: proxyMeLightningAddressPost
- Purpose: Proxies POST
/me/lightning-address. - Inputs:
Requestwith JSON body. - Returns / side effects: Upstream
Response. - Used by: Route POST
/me/lightning-address.
Function: base64UrlToBytes
- Purpose: Decode a base64url string to bytes for WebAuthn options.
- Inputs: Base64url string (padding optional).
- Returns / side effects:
Uint8Array. No network. - Used by:
creationOptionsFromJSON,requestOptionsFromJSON.
Function: bytesToBase64Url
- Purpose: Encode bytes as unpadded base64url for WebAuthn JSON.
- Inputs:
Uint8Array. - Returns / side effects: Base64url string. No network.
- Used by:
credentialToJSON.
Function: creationOptionsFromJSON
- Purpose: Turn api creation-options JSON into
navigator.credentials.createinput, includingexcludeCredentialswhen present. - Inputs: Record from
POST /auth/passkey/register/begin. - Returns / side effects:
PublicKeyCredentialCreationOptions. Uses native parse when present. Throws if a descriptor list is present but not an array, or is non-empty but has no validpublic-keyentries (invalid type or id is skipped; all skipped → TypeError), including before native parse. - Used by:
usePasskeyLogin.register.
Function: credentialToJSON
- Purpose: Serialise a
PublicKeyCredentialfor the api finish body. - Inputs: Browser credential from create/get.
- Returns / side effects: JSON record. Uses native
toJSONwhen present. - Used by:
usePasskeyLogin.
Function: finishPasskeyAuthentication
- Purpose: POST
/auth/passkey/authenticate/finishand parse the session. - Inputs:
challengeIdand credential JSON. - Returns / side effects:
{ token, account }. Throws on non-2xx. - Used by:
usePasskeyLogin.authenticate.
Function: finishPasskeyRegistration
- Purpose: POST
/auth/passkey/register/finishand parse the session. - Inputs:
challengeIdand credential JSON. - Returns / side effects:
{ token, account }withlinkingKeynull. Throws on non-2xx. - Used by:
usePasskeyLogin.register.
Function: proxyAuthPasskeyAuthenticateBeginPost
- Purpose: Proxies POST
/auth/passkey/authenticate/begin. - Inputs: Incoming
Request. - Returns / side effects: Upstream
Response. - Used by: Route POST
/auth/passkey/authenticate/begin.
Function: proxyAuthPasskeyAuthenticateFinishPost
- Purpose: Proxies POST
/auth/passkey/authenticate/finish. - Inputs: Incoming
Requestwith JSON body. - Returns / side effects: Upstream
Response. - Used by: Route POST
/auth/passkey/authenticate/finish.
Function: proxyAuthPasskeyRegisterBeginPost
- Purpose: Proxies POST
/auth/passkey/register/begin. - Inputs: Incoming
Request. - Returns / side effects: Upstream
Response. - Used by: Route POST
/auth/passkey/register/begin.
Function: proxyAuthPasskeyRegisterFinishPost
- Purpose: Proxies POST
/auth/passkey/register/finish. - Inputs: Incoming
Requestwith JSON body. - Returns / side effects: Upstream
Response. - Used by: Route POST
/auth/passkey/register/finish.
Function: requestOptionsFromJSON
- Purpose: Turn api request-options JSON into
navigator.credentials.getinput. MapsallowCredentialswhen present; otherwise discoverable[]. - Inputs: Record from
POST /auth/passkey/authenticate/begin. - Returns / side effects:
PublicKeyCredentialRequestOptions. Uses native parse when present. Throws if a descriptor list is present but not an array, or is non-empty but has no validpublic-keyentries (invalid type or id is skipped; all skipped → TypeError), including before native parse. - Used by:
usePasskeyLogin.authenticate.
Function: startPasskeyAuthentication
- Purpose: POST
/auth/passkey/authenticate/beginand parse options. - Inputs: None.
- Returns / side effects:
{ challengeId, options }. Throws on non-2xx. - Used by:
usePasskeyLogin.authenticate.
Function: startPasskeyRegistration
- Purpose: POST
/auth/passkey/register/beginand parse options. - Inputs: None.
- Returns / side effects:
{ challengeId, options }. Throws on non-2xx. - Used by:
usePasskeyLogin.register.
Function: usePasskeyLogin
- Purpose: Client hook for passkey login.
loginuses an existing passkey; it creates one only when the browser reports no credential (NotAllowedError).cancelaborts an in-flight WebAuthn prompt. - Inputs: None (reads
useAuthStore). - Returns / side effects:
{ status, login, register, authenticate, retry, cancel }.retryrepeatsloginwhen the visitor used the single button. Calls WebAuthn and the api. Unmount aborts an in-flight prompt. - Used by:
LoginCard.
HTTP endpoints (Next.js route handlers)
Endpoint: GET /healthz
- Purpose: Liveness JSON
{ status: 'ok' }fromsrc/app/healthz/route.ts. - Errors: None if the process is up (always 200).
- Used by: Container probes and Playwright smoke.
- Auth: Public.
Endpoint: POST /auth/passkey/authenticate/begin
- Purpose: Same-origin proxy of api
POST /auth/passkey/authenticate/begin. - Errors: Upstream status, or 502 if the api is unreachable.
- Used by:
startPasskeyAuthentication. - Auth: Public.
Endpoint: POST /auth/passkey/authenticate/finish
- Purpose: Same-origin proxy of api
POST /auth/passkey/authenticate/finish. - Errors: Upstream status, or 502 if the api is unreachable.
- Used by:
finishPasskeyAuthentication. - Auth: Public.
Endpoint: POST /auth/passkey/register/begin
- Purpose: Same-origin proxy of api
POST /auth/passkey/register/begin. - Errors: Upstream status, or 502 if the api is unreachable.
- Used by:
startPasskeyRegistration. - Auth: Public.
Endpoint: POST /auth/passkey/register/finish
- Purpose: Same-origin proxy of api
POST /auth/passkey/register/finish. - Errors: Upstream status, or 502 if the api is unreachable.
- Used by:
finishPasskeyRegistration. - Auth: Public.
Endpoint: GET /gifts
- Purpose: Same-origin proxy of api
GET /gifts?day=YYYY-MM-DD(individual outbound gifts that UTC day). - Errors: Upstream 400/503, or 502 if the api is unreachable.
- Used by:
fetchGiftDayon/stats/[day]. - Auth: Public.
Endpoint: GET /gifts/stats
- Purpose: Same-origin proxy of api
GET /gifts/stats(aggregated outbound gift totals). - Errors: Upstream 503, or 502 if the api is unreachable.
- Used by:
fetchGiftStatson/stats. - Auth: Public.
Endpoint: GET /lightning-address
- Purpose: Same-origin proxy of public LUD-16 resolve.
- Errors: Upstream 400/502, or 502 if the api is unreachable.
- Used by:
resolveLightningAddresson/donate. - Auth: Public.
Endpoint: POST /me/name
- Purpose: Same-origin proxy to set or replace the display name.
- Errors: Upstream 400, or 502 if the api is unreachable.
- Used by:
setName. - Auth: Bearer.
Endpoint: GET /me
- Purpose: Same-origin proxy of the signed-in account.
- Errors: Upstream 401, or 502 if the api is unreachable.
- Used by:
fetchMe. - Auth: Bearer.
Endpoint: POST /me/lightning-address
- Purpose: Same-origin proxy to link or replace a Wallet of Satoshi address.
- Errors: Upstream 400, or 502 if the api is unreachable.
- Used by:
setLightningAddress. - Auth: Bearer.
Endpoint: DELETE /me/lightning-address
- Purpose: Same-origin proxy to unlink a Wallet of Satoshi address.
- Errors: Upstream status, or 502 if the api is unreachable.
- Used by:
unlinkLightningAddress. - Auth: Bearer.