Skip to content

Add loading, error and not-found states for the user-facing routes #37

Closed

Description

@abrcrmb

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.tsx or not-found.tsx under app/ — I checked with find.
  • 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:8 already handles the lag case.
  • Every user-visible string must live in messages/en.json (293 keys today) and be accessed through lib/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 BLOCKED or depends on a row that is not DONE, 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 shared Spinner/StatePanel primitive — the directory exists with only a .gitkeep
  • messages/en.json new 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/** and lib/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 headers
  • lib/wallet/** and any direct contract call site — money-moving calls go through the SDK and the wallet adapter only
  • app/send/**, app/locks/**, app/payee/** action logic — release, refund and decline paths stay with maintainers
  • package.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

  1. Read ARCHITECTURE_ESSENTIALS.md and AGENTS.md §8.3 (app rules: externalized strings, Intl formatting, no inline copy) before writing anything.
  2. Skeleton screens over spinners where a shape is predictable; state plainly what is loading ("reading the lock from the network", not "Loading…").
  3. error.tsx must show a short human message, a retry button, and no technical detail in the UI. Keep the reset behaviour. Do not log the URL fragment or lock reference anywhere (hard rule 5).
  4. not-found.tsx must 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".
  5. All strings to messages/en.json. pnpm lint will catch inline ones, so run it early.
  6. Respect prefers-reduced-motion: app/globals.css:48,78 already 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 /verify read chain state and can be slow; /payee reads 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.tsx receives a real Error object: render a human message and a retry, keep reset working, 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.tsx renders a human-readable failure with a retry action and no stack trace or file path
  • not-found.tsx wording is honest about indexer lag and does not assert on-chain absence
  • All new copy is in messages/en.json; pnpm lint, typecheck, test, build pass
  • 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 #37 in the PR description, so the issue and the PR link automatically.
  • Branch feat/…, fix/…, docs/…, test/… or chore/…. 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-sensitive label — 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 /, /payee and /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

  1. added
    accessibilityBarrier affecting people with disabilities
    good first issueSmall, self-contained, no security-critical files
    waveScoped for Drips Wave contributors
    area:appOwnership: kinlock-app
    tier/trivialWave tier: self-contained, under about an hour, one file or two adjacent files
    on Oct 8, 2026
  2. jdjioe5-cpu commented on Oct 17, 2026

    @jdjioe5-cpu

    I'll take this.

    Constraint review:

    • Constrained by Hard Rule 5 (claim link fragment #r=...&s=... privacy: error.tsx will never log or expose URL fragments or lock references; app/claim is strictly untouched), Hard Rule 7 (honest wording: not-found state will not assert on-chain absence when indexer may lag), and AGENTS.md §8.3 (mobile-first, slow connections, externalized strings in messages/en.json, prefers-reduced-motion respected).
    • Roadmap row: M3-27.
  3. jdjioe5-cpu commented on Oct 17, 2026

    @jdjioe5-cpu

    PR is up: #47

    • Created shared Spinner and StatePanel in components/ui/ with motion-safe animations and accessibility roles.
    • Root app/loading.tsx and route-specific loading states for /locks and /payee.
    • Client app/error.tsx with autofocus retry and zero technical trace leakage.
    • Honest app/not-found.tsx acknowledging indexer lag.
    • All strings in messages/en.json.
    • Roadmap M3-27 marked as DONE.
    • All 186 tests passing, lint clean, typecheck clean, and production build verified.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    accessibilityBarrier affecting people with disabilitiesarea:appOwnership: kinlock-appgood first issueSmall, self-contained, no security-critical filestier/trivialWave tier: self-contained, under about an hour, one file or two adjacent fileswaveScoped for Drips Wave contributors

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions