21.gifts

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: id under 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) inside MarketingLayout, LanguageSwitcher.

Variant: default

Desktop/wide layout: section nav is visible in the header (How it works, Why, FAQ, Stats, Handbook, Log in). No hamburger.

21.gifts home

Variant: mobile-nav

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

21.gifts home mobile nav

  • URL: /legal — imprint and privacy. /legal.html permanently 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 locale cookie; 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: LegalPage inside MarketingLayout, LanguageSwitcher.

Variant: default

The only state: imprint plus privacy, marketing chrome.

21.gifts legal

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).

21.gifts gifts on a day

Variant: empty

No gifts that UTC day. Copy No gifts recorded on this day.

21.gifts empty day

Variant: loading

Waiting on GET /gifts. Copy Loading…

21.gifts day loading

Variant: error

Fetch failed. Button Try again.

21.gifts day error

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-origin GET /gifts/stats), LanguageSwitcher.

Variant: default

Loaded stats with one cumulative over-time chart visible. Scale defaults to BTC.

21.gifts stats

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.

21.gifts stats USD scale

Variant: empty

Zero gifts. KPI zeros and No gifts recorded yet.

21.gifts stats empty

Variant: loading

Waiting on GET /gifts/stats. Copy Loading…

21.gifts stats loading

Variant: error

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

21.gifts stats error

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.

21.gifts login idle

Variant: starting

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

21.gifts login starting

Variant: error

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

21.gifts login error

Variant: signed-in

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

21.gifts login signed in

Variant: signed-in-named

Signed in with a display name set. Shows the name plus Edit, and the empty address form.

21.gifts login signed in named

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).

21.gifts login signed in linked

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.

21.gifts donate form

Variant: busy

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

21.gifts donate busy

Variant: validation-error

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

21.gifts donate validation error

Variant: invoice

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

21.gifts donate invoice

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.

21.gifts donate invoice Android

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 four docs/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.

21.gifts handbook

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.

21.gifts handbook copied

Screen: /404

  • URL: any unknown path (App Router not-found.tsx). There is no page.tsx for /404; Playwright uses page.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.

21.gifts not found

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) so useTranslations re-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 /donate with 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 as GET.
  • Inputs: Incoming Request on 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.
  • Purpose: Client button beside a handbook heading or chapter. Copies origin + pathname + #id to the clipboard, sets location.hash, and flashes a check icon for 1.2s (textarea execCommand fallback).
  • Inputs: targetId (DOM id without #) and label (interpolated into handbook.copyLink via useTranslations as { label }).
  • Visible UI: Idle Link2 icon; copied Check icon. No visible "Copy link" or "Copied" text (title and aria-label keep the accessible name).
  • Returns / side effects: A <button type="button">. Clipboard write; hash update. No network.
  • Used by: HandbookPage (page title and chapter nav) and HandbookMarkdown (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: markdown string and idPrefix for heading ids.
  • Returns / side effects: React fragment. No network.
  • Used by: HandbookPage for each handbook document.

Function: HandbookIntro

  • Purpose: Server-presentational chrome for the /handbook title, intro sentence, and section-nav aria-label (already-translated copy).
  • Inputs: title, introBefore, introAfter, navAria (already-translated strings), headingAction (node beside the h1, e.g. copy-link), and children (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 via getRequestLocale, loads the four app handbook files from disk, and renders them with a link to the api handbook. Title copy-link uses handbook.title; chapter copy-links use handbook.chapterLabel with { title }; intro chrome via already-translated props on HandbookIntro; markdown bodies stay English.
  • Inputs: None (calls getRequestLocale(); reads docs/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 renders StatsDashboard.
  • 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. Renders StatsLoader.
  • Inputs: None.
  • Returns / side effects: The statistics screen inside MarketingLayout. Renders StatsLoader.
  • Used by: Route /stats.

Function: GiftDayPage

  • Purpose: Next.js page for /stats/[day]. Invalid UTC days call notFound(). Valid days render DayLoader.
  • Inputs: params Promise { day }.
  • Returns / side effects: The day screen inside MarketingLayout.
  • Used by: Route /stats/[day].

Function: DayLoader

  • Purpose: Client loader for /stats/[day]. Fetches GET /gifts?day=, date input navigates, retry on error.
  • Inputs: day UTC YYYY-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 day string.
  • 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: true only for a real calendar date. No I/O.
  • Used by: GiftDayPage, DayLoader.

Function: proxyGiftsGet

  • Purpose: Same-origin proxy helper for api GET /gifts (forwards day).
  • 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 /login and /donate, all via translate for 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 a locale cookie and refreshes the App Router tree.
  • Inputs: tone (dark for marketing chrome, light for login/donate). Reads current locale via useTranslations.
  • Returns / side effects: Select with native option labels (English/Deutsch/Español/Filipino). On change writes locale=<code>; Path=/; Max-Age=31536000; SameSite=Lax and ; Secure on HTTPS, then router.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 via useTranslations. Empty and request failures are typed keys so they re-render after a locale change.
  • Returns / side effects: React element or null when logged out. POST /me/name on save.
  • Used by: LoginCard signed-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 via useTranslations. Request failures use la.errorRequest so they re-render after a locale change.
  • Returns / side effects: React element or null when logged out.
  • Used by: LoginCard signed-in view on screen /login (not on /).

Function: LocaleProvider

  • Purpose: Client context provider that exposes the negotiated locale and a bound t helper to visitor-facing components.
  • Inputs: locale, messages for that locale, and children.
  • Returns / side effects: React provider element. No network; does not write cookies.
  • Used by: RootLayout wraps every page; consumed via useTranslations (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 NameForm and LightningAddressForm. Visitor-facing copy via useTranslations.
  • Inputs: Uses usePasskeyLogin and useAuthStore. Rehydrates via loadSession + fetchMe without overwriting a newer in-page session or restoring after logout. A rejected token calls clearAuth when 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 /login with localized heading and a light language switcher.
  • Inputs: None. Calls getRequestLocale() for the page title.
  • Returns / side effects: Renders LoginCard and LanguageSwitcher (top-right).
  • Used by: Route /login.

Function: QrCode

  • Purpose: SVG QR for a string (LNURL or bolt11).
  • Inputs: value (required) and label (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), and LocaleProvider with the request catalog.
  • Inputs: children React nodes. Calls getRequestLocale() for html lang and 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 (window undefined).
  • Used by: useAuthStore.clearAuth.

Function: fetchGiftStats

  • Purpose: GET /gifts/stats and 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 /me with the bearer session.
  • Inputs: sessionToken.
  • Returns / side effects: Account or null on 401.
  • Used by: LoginCard session hydration.

Function: formatBtcTick

  • Purpose: Formats a parsed BTC chart-axis value with up to 8 decimals, trailing zeros trimmed.
  • Inputs: btc number (layout scale only).
  • Returns / side effects: Trimmed decimal string (e.g. 0.015).
  • Used by: StatsDashboard BTC-over-time chart.

Function: formatMsatAsSats

  • Purpose: Formats millisatoshis as an English sat string (1 sat / {n} sats).
  • Inputs: msat number.
  • Returns / side effects: Decimal string in sats.
  • Used by: Unit tests (lnurl-pay.test.ts). The donate UI formats amounts via catalog keys donate.satOne / donate.sats instead.

Function: formatUsdDisplay

  • Purpose: Formats an API USD amount string ("1425.00") as en-US currency for the stats hero.
  • Inputs: usd string from GET /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: usd number (layout scale only).
  • Returns / side effects: Label such as $1,234.
  • Used by: StatsDashboard USD-over-time chart.

Function: getApiUrl

  • Purpose: Reads NEXT_PUBLIC_API_URL via 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 Messages object for that locale. Exhaustive switch over Locale.
  • Used by: RootLayout, Home, /login, /donate, NotFound, MarketingFooter, HandbookPage, and the renderWithLocale test helper.

Function: getRequestLocale

  • Purpose: Resolve the UI locale for the current request without writing cookies.
  • Inputs: Reads the locale cookie and the Accept-Language header via next/headers (both async in Next 15).
  • Returns / side effects: A supported locale (en/de/es/fil). Valid cookie wins; invalid/missing cookie falls through to parseAcceptLanguage; unmatched → en.
  • Used by: RootLayout, Home, /login, /donate, NotFound, MarketingFooter, and HandbookPage. Lives in src/lib/request-locale.ts so client components can import locale constants without next/headers.

Function: isAndroidUserAgent

  • Purpose: Detects Android so the WoS CTA can use an Intent URL.
  • Inputs: userAgent string.
  • Returns / side effects: true iff /Android/i matches.
  • 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: LoginCard on mount.

Function: parseAcceptLanguage

  • Purpose: Negotiate a supported UI locale from an RFC 7231 Accept-Language header.
  • Inputs: Raw header string (may be empty). Splits on commas. A missing q defaults to 1. A bare q, empty/invalid qvalue, or duplicate q discards that language-range. Maps primary subtags (en/de/es/fil, and tlfil).
  • Returns / side effects: Among valid mapped ranges with q > 0, highest q, then earlier header position, then LOCALES order. No positive assignment → en. Pure function — no I/O.
  • Used by: getRequestLocale when no valid locale cookie is present.

Function: parseHandbookMarkdown

  • Purpose: Parse handbook markdown into headings, paragraphs, and lists with inline code, strong, links, and images.
  • Inputs: markdown string and idPrefix for 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, or undefined.
  • Returns / side effects: That locale, or null. Pure function — no I/O.
  • Used by: getRequestLocale (cookie) and LanguageSwitcher (option value).

Function: requestDonateInvoice

  • Purpose: GET an LNURL-pay callback with amount millisatoshis 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: DonateForm before paying.

Function: satsToMsat

  • Purpose: Converts whole sats to millisatoshis.
  • Inputs: sats number.
  • Returns / side effects: sats * 1000.
  • Used by: DonateForm (converts sats before calling requestDonateInvoice).

Function: saveSession

  • Purpose: Writes the bearer token to localStorage.
  • Inputs: token string.
  • 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 from vars.
  • Inputs: catalog (Messages), key (MessageKey), optional vars map 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 the t helper from LocaleProvider / useTranslations.

Function: unlinkLightningAddress

  • Purpose: DELETE /me/lightning-address.
  • Inputs: sessionToken.
  • Returns / side effects: Updated Account with address cleared.
  • Used by: LightningAddressForm.

Function: uppercaseLnurl

  • Purpose: Uppercases a bech32 LNURL or BOLT11 payment request.
  • Inputs: lnurl string.
  • Returns / side effects: Uppercase string.
  • Used by: walletOfSatoshiHref and walletOfSatoshiIntentHref (DonateForm).

Function: useAuthStore

  • Purpose: Zustand store for session + account. Hydration is explicit (no module-init localStorage).
  • Inputs: Hook. Methods setAuth, setAccount, clearAuth.
  • Returns / side effects: Auth state object.
  • Used by: LoginCard, NameForm, LightningAddressForm on /login (not /).

Function: useTranslations

  • Purpose: Client hook returning { locale, t } from the nearest LocaleProvider.
  • Inputs: None (React context).
  • Returns / side effects: Active locale and a t(key, vars?) bound to that catalog. Throws if used outside LocaleProvider.
  • 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: DonateForm when 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: DonateForm on Android.

Function: DELETE

  • Purpose: App Router DELETE export on /me/lightning-address (re-export of proxyMeLightningAddressDelete).
  • 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 getRequestLocale and reads copy from the catalog via translate.
  • 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. Awaits MarketingFooter() (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; awaits MarketingFooter().
  • Returns / side effects: 404 element with MarketingHeader and awaited footer (not rendered as JSX child).
  • Used by: Next.js not-found.tsx.

Function: POST

  • Purpose: Shared App Router POST export name. /me/name re-exports proxyMeNamePost; /me/lightning-address re-exports proxyMeLightningAddressPost; /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, apiPath beginning 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 Response via proxyApiRequest.
  • Used by: Route GET /gifts/stats.

Function: proxyLightningAddressGet

  • Purpose: Proxies GET /lightning-address.
  • Inputs: Request with address query.
  • Returns / side effects: Upstream Response.
  • Used by: Route GET /lightning-address.

Function: proxyMeNamePost

  • Purpose: Proxies POST /me/name.
  • Inputs: Request with JSON body.
  • Returns / side effects: Upstream Response.
  • Used by: Route POST /me/name.

Function: proxyMeGet

  • Purpose: Proxies GET /me.
  • Inputs: Request with 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: Request with 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.create input, including excludeCredentials when 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 valid public-key entries (invalid type or id is skipped; all skipped → TypeError), including before native parse.
  • Used by: usePasskeyLogin.register.

Function: credentialToJSON

  • Purpose: Serialise a PublicKeyCredential for the api finish body.
  • Inputs: Browser credential from create/get.
  • Returns / side effects: JSON record. Uses native toJSON when present.
  • Used by: usePasskeyLogin.

Function: finishPasskeyAuthentication

  • Purpose: POST /auth/passkey/authenticate/finish and parse the session.
  • Inputs: challengeId and credential JSON.
  • Returns / side effects: { token, account }. Throws on non-2xx.
  • Used by: usePasskeyLogin.authenticate.

Function: finishPasskeyRegistration

  • Purpose: POST /auth/passkey/register/finish and parse the session.
  • Inputs: challengeId and credential JSON.
  • Returns / side effects: { token, account } with linkingKey null. 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 Request with 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 Request with 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.get input. Maps allowCredentials when 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 valid public-key entries (invalid type or id is skipped; all skipped → TypeError), including before native parse.
  • Used by: usePasskeyLogin.authenticate.

Function: startPasskeyAuthentication

  • Purpose: POST /auth/passkey/authenticate/begin and parse options.
  • Inputs: None.
  • Returns / side effects: { challengeId, options }. Throws on non-2xx.
  • Used by: usePasskeyLogin.authenticate.

Function: startPasskeyRegistration

  • Purpose: POST /auth/passkey/register/begin and 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. login uses an existing passkey; it creates one only when the browser reports no credential (NotAllowedError). cancel aborts an in-flight WebAuthn prompt.
  • Inputs: None (reads useAuthStore).
  • Returns / side effects: { status, login, register, authenticate, retry, cancel }. retry repeats login when 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' } from src/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: fetchGiftDay on /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: fetchGiftStats on /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: resolveLightningAddress on /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.
Handbook — 21.gifts