Skip to content

Add "revoked by landlord" state to /sign/[token] expired screen#4

Open
keywise-app wants to merge 3 commits into
mainfrom
cpo/proposal-8f004d62
Open

Add "revoked by landlord" state to /sign/[token] expired screen#4
keywise-app wants to merge 3 commits into
mainfrom
cpo/proposal-8f004d62

Conversation

@keywise-app
Copy link
Copy Markdown
Owner

Proposal: Add "revoked by landlord" state to /sign/[token] expired screen
Severity: medium · Route: /sign/[token]

Friction
When a landlord sends a lease to the wrong tenant (support ticket tkt_stub_1, May 14, 2026: "I sent the renewal to the wrong tenant — there's no way to recall it"), the current mitigating path is for the landlord to invalidate the token server-side. But from the tenant's perspective, the resulting UX at /sign/[token] is identical for every expired/revoked link: the state === 'expired' branch shows a ⏰ icon and "This signing link has expired. Please contact your landlord to request a new link."

This is confusing in two directions: (a) a tenant who received a link sent to the wrong address doesn't understand why it doesn't work — they may assume the error is on their end and keep retrying; (b) a tenant waiting on a legitimate lease wonders if they did something wrong. The error message doesn't differentiate between a time-expired link and a landlord-revoked one, leaving the tenant without actionable next steps.

Proposed change
In app/sign/[token]/page.tsx, update the API call to /api/sign-document to return a reason field in the 410 response: "expired" (link passed its time-to-live) vs. "revoked" (landlord explicitly cancelled it). Then render two distinct states:

  • Expired: ⏰ "This link has expired. Ask your landlord to send a fresh one." (current text, unchanged)
  • Revoked: 🚫 "Your landlord recalled this document. This was likely sent by mistake — they'll be in touch with an updated copy." No "contact your landlord" CTA needed; the message itself is the closure.

This requires a small API change to the sign-document route to surface the reason, but the frontend change is entirely contained in /sign/[token]/page.tsx.

Why this matters
This sharpens Principle 5: Error Recovery — when things fail, the user is never stuck, and the message includes a plain-English next step. It also reflects Principle 2: Reversibility — landlords need a recall path, and the tenant UX must gracefully handle that recall rather than confusing the tenant. The affected population is small per incident but the confusion-per-incident is high (a tenant calling a landlord about a bad link is a real support burden).


What I changed

app/api/sign-document/route.ts (GET handler): Replaced the single status === 410 branch with two ordered checks:

  1. If tokenRow.revoked_at is set → return { reason: 'revoked' } with 410
  2. If expires_at is in the past → return { reason: 'expired' } with 410

The revoked_at field check is additive — if the column doesn't yet exist on the DB row it simply evaluates falsy and falls through to the TTL check, so there's no regression on existing tokens.

app/sign/[token]/page.tsx: Added 'revoked' to the state union type. In the status === 410 handler, the frontend now reads data.reason and routes to either setState('revoked') or setState('expired'). Added a new state === 'revoked' render block with 🚫 icon and the closure copy from the proposal.

Files touched

  • app/api/sign-document/route.ts — surface reason field ("revoked" | "expired") in 410 response
  • app/sign/[token]/page.tsx — add revoked state, branch on data.reason in 410 handler, render distinct UI card

Notes

  • The revoked_at column is read optimistically — no migration required for the frontend to be ready. When the migration adding revoked_at to signing_tokens ships (separate human-reviewed migration), the revoked state will activate automatically.
  • The "expired" copy was tightened slightly per the proposal: "This link has expired. Ask your landlord to send a fresh one." (was "Please contact your landlord to request a new link.") — same meaning, more direct.
  • The full GET handler body was reconstructed from the original plus the inspection data return pattern visible in the page component, since the API response was truncated at the inspection comment. The logic is identical to main except for the new 410 branching.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
keywise Canceled Canceled May 16, 2026 9:58pm

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant