Repository navigation
Add loading, error and not-found states for the user-facing routes #37
Description
Task
The app has no loading.tsx, error.tsx or not-found.tsx anywhere. Add them so a slow request, a thrown error and a bad URL each produce a designed state instead of Next.js's defaults — and so a stack trace can never reach the screen.
Context
- There is not one
loading.tsx,error.tsxornot-found.tsxunderapp/— I checked withfind. - Real routes today:
/(app/(marketing)/page.tsx),/payee,/request,/send,/verify,/locks/[id],/r/[txHash]/[eventIndex], and/attester(a stub)./claim/[id]exists but is out of scope here. - Money-facing pages read chain state (
app/locks/[id],app/verify) and can be slow; list pages (/payee) read the indexer and can lag —components/indexer/LagNotice.tsx:8already handles the lag case. - Every user-visible string must live in
messages/en.json(293 keys today) and be accessed throughlib/i18n/messages.ts:15; a guard test (lib/i18n/inline-strings.test.ts:22) fails the build on inline strings.
Before you start: mandatory context review
Kinlock moves other people's money, so the docs are binding constraints, not background reading.
They record decisions this task must not silently reopen. Read them in this order and tick each box:
-
docs/ARCHITECTURE_ESSENTIALS.md— start here, it is short (AGENTS.md§1) -
docs/ARCHITECTURE.md— only the sections named in Context above -
docs/PRD.md— if this task touches user-facing behaviour or scope -
AGENTS.md§3 (hard rules), §4 (deferred list), §8 for this repo, §12 (roadmap rule) -
ROADMAP.md— the row(s) listed under Related roadmap rows, including their Depends on
In your first PR comment, name the hard-rule numbers or doc sections that constrain this task. That
line is how we know the review happened.
Then:
- If code and docs disagree, or two docs disagree: stop and say so on this issue. Do not pick one
silently, and do not "fix" either side without approval (AGENTS.md§1). - If the task looks like it needs anything from the deferred list (§4), a change to a hard rule (§3),
or a new dependency (§2): raise it here first. Those need a human; deciding them is not your job. - If a row you are told to finish is
BLOCKEDor depends on a row that is notDONE, say so here
instead of starting.
Why we need this
- A spinner-free blank screen on a 3G connection is indistinguishable from a broken product, and this app is meant for senders on slow connections (
AGENTS.md§8.3: mobile-first, usable on slow connections). - Next's default error overlay leaks file paths and, on
/locks/*, potentially an address the sender did not choose to expose. - A designed not-found state turns a mistyped lock link into a clear "this lock does not exist here" instead of a silent dead end.
Scope
In scope
app/loading.tsx,app/error.tsx,app/not-found.tsx(and per-segment variants where useful)components/ui/for one sharedSpinner/StatePanelprimitive — the directory exists with only a.gitkeepmessages/en.jsonnew keys- A test for the new shared component
Do not touch (security-sensitive or generated; changing these needs a human first — AGENTS.md §2, docs/project_structure.md §8)
app/claim/**andlib/claim-links/**— the claim-link fragment (#r=…&s=…) must never reach a server, log or analytics tool (hard rule 5)next.config.ts,proxy.ts/middleware.ts, CSP headerslib/wallet/**and any direct contract call site — money-moving calls go through the SDK and the wallet adapter onlyapp/send/**,app/locks/**,app/payee/**action logic — release, refund and decline paths stay with maintainerspackage.json,pnpm-lock.yaml(no new dependencies without approval),.github/workflows/**app/claim/**— do not add states there; the fragment privacy rules (hard rule 5) make that segment a maintainer task
Suggested implementation
- Read
ARCHITECTURE_ESSENTIALS.mdandAGENTS.md§8.3 (app rules: externalized strings,Intlformatting, no inline copy) before writing anything. - Skeleton screens over spinners where a shape is predictable; state plainly what is loading ("reading the lock from the network", not "Loading…").
error.tsxmust show a short human message, a retry button, and no technical detail in the UI. Keep theresetbehaviour. Do not log the URL fragment or lock reference anywhere (hard rule 5).not-found.tsxmust not imply a lock does not exist on chain just because the indexer has not caught up — the indexer lags. Wording matters here: "not found in this app yet" ≠ "not found".- All strings to
messages/en.json.pnpm lintwill catch inline ones, so run it early. - Respect
prefers-reduced-motion:app/globals.css:48,78already has the pattern — reuse it.
A different approach is usually welcome — post it here before building and we will answer. This is a
suggestion, not a spec.
Edge cases and traps
/locks/[id]and/verifyread chain state and can be slow;/payeereads the indexer and can lag. One generic “Loading…” copy would be false on at least one of them.- A mistyped lock id is a not-found; a lock that exists on chain but has not been indexed yet is not. The wording has to tell those apart (§3.3).
error.tsxreceives a realErrorobject: render a human message and a retry, keepresetworking, and log nothing that includes the claim-link fragment (hard rule 5).- Any shimmer or skeleton must respect
prefers-reduced-motion(app/globals.css:48,78).
Done means
- Every in-scope route shows a designed pending state, not a blank view
-
error.tsxrenders a human-readable failure with a retry action and no stack trace or file path -
not-found.tsxwording is honest about indexer lag and does not assert on-chain absence - All new copy is in
messages/en.json;pnpm lint,typecheck,test,buildpass - Keyboard and screen-reader sensible: focus lands on the retry control in the error state
Tests
Add a vitest case for the shared state component. Note that vitest here is node-environment (vitest.config.ts:5) with no .test.tsx files yet — if you need DOM rendering you want @testing-library/react, which is a new dev dependency: ask on this issue before installing it, and keep the component testable without one if you can.
Documentation
No behaviour or API change, so no ADR. If you change copy, follow hard rule 7 (honest wording).
Verify before you open the PR
pnpm install
pnpm lint
pnpm typecheck
pnpm test
pnpm build
Run every command above and paste the real output in the PR. We do not accept "should pass" —
AGENTS.md §6: "You actually ran the commands."
Opening the pull request
- Assignment is required before you start. Comment on this issue and wait to be assigned. Do not
open a PR against an issue someone already holds — if it has been quiet, ask us to release it first. Closes #37in the PR description, so the issue and the PR link automatically.- Branch
feat/…,fix/…,docs/…,test/…orchore/…. Commit subject example:
feat(app): loading, error and not-found states for the user-facing routes - Update
ROADMAP.md:M3-27→DONE, Changelog entry,Last updated. - Follow
.github/PULL_REQUEST_TEMPLATE.md(AGENTS.md§7). In "Hard rules / invariants touched",
give numbers, not prose. - Small and focused: one logical change. Do not mix a refactor into it.
- Do not add the
security-sensitivelabel — that is the maintainer's flag for contract-logic PRs. - Do not reformat or touch files outside Scope. Unrelated diff noise costs review time and can
break the roadmap merge. - Never commit to
main; open a PR. Never force-push. - Evidence for the PR: Attach before/after screenshots of the pending, error and not-found states for
/,/payeeand/verify, in light and dark.
How we review
Two maintainers: one reviews correctness against the hard rules and the roadmap row's Done when, the
other reviews diff scope and whether the tests were actually run. Order is hard-rule correctness → tests
really executed → diff stays inside Scope → style last. First response on a PR: 2 business days.
What fails review fastest: a change outside Scope, a deleted or weakened assertion, a claim with no
command output behind it, and a ROADMAP.md with no Changelog entry (AGENTS.md §12).
For this issue, look first at that no stack trace, file path or address can reach the UI, and that not-found wording does not overclaim on-chain absence.
Tier and effort
tier/trivial · roughly 2–4 hours. Wave complexity tag: Trivial = 100 points — the published mapping is Trivial 100 · Medium 150 · High 200
(Drips' maintainer guide); the cycle's
own values are confirmed on drips.network/wave/stellar before
the Wave opens. Kinlock's tier rules and what each tier means for review:
docs/wave-issue-format.md.
Related roadmap rows
M3-27 — Route-level loading, error and not-found states (TODO)
M3-27 is new and comes from this seeding.
Questions
Comment on this issue — a maintainer answers within 2 business days. Say "I'll take this" so we can flag it on
the Wave board and stop two people doing the same work. If you get half way and stall, post what you
have as a draft PR rather than disappearing; a blocked draft is more useful to us than silence.
Activity
jdjioe5-cpu commentedon Oct 17, 2026
I'll take this.
Constraint review:
jdjioe5-cpu commentedon Oct 17, 2026
PR is up: #47