Skip to content

feat: consent-ledger echo + re-consent click-through (tos-v1 phase 2) - #2456

Open
jjramirezn wants to merge 5 commits into
devfrom
feat/tos-v1-consent-client
Open

feat: consent-ledger echo + re-consent click-through (tos-v1 phase 2)#2456
jjramirezn wants to merge 5 commits into
devfrom
feat/tos-v1-consent-client

Conversation

@jjramirezn

@jjramirezn jjramirezn commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Client half of the consent ledger (peanut-api #1209, tos-v1 phase 2): every acceptance surface now echoes the exact legal-document revisions it displayed, and the ToS §17 re-consent click-through ships.

  • Generated versions: scripts/generate-legal-versions.mjs (predev + prebuild, output committed) derives slug → { version, hash } from the src/content legal frontmatter (last_updated + sha256 of each en.md). No hardcoded versions; bumping the content submodule refreshes the constants on next build.
  • Signup echo: the ZeroDev SDK owns the /passkeys/register/verify body, so useZeroDev.handleRegister sends displayed terms+privacy versions via the x-accepted-legal header (passkeyServerHeaders).
  • Card apply echo: rainApi.applyForCard gains acceptedDocuments; the card page sends the exact set CardTermsScreen showed (US: card-terms-us + card-esign + card-privacy; international: card-terms-international + card-esign).
  • Re-consent: new ReConsentModal (mobile-ui layout) checks GET /users/consent/status once per session; when a published version passed the user's last provably-accepted one, a blocking checkbox click-through lists the updated docs and records acceptance via POST /users/consent/accept.
  • New client-side service src/services/consent.ts (no server actions).

Risks / breaking changes

  • Cross-repo: pairs with peanut-api hot-fix: poland guest link claim issue #1209. Either merge order is safe — the echoes are optional fields/headers the old backend ignores, and a 404 on consent-status just logs (fail-open, app never locks).
  • All pre-ledger users see the re-consent modal once after the backend ships — by design (nobody has provable consent; accepting backfills the ledger). If product wants a quieter rollout, gate the modal mount.
  • The modal is deliberately unclosable until accepted (ToS §17); a failed status fetch keeps the app usable.

QA

  1. pnpm dev → predev regenerates src/constants/legal-versions.generated.ts (7 docs).
  2. Fresh signup → backend rows for terms + privacy @ 2026-07-15 with the generated hashes.
  3. Card apply (US + international) → regional doc-set rows, surface='card-apply'.
  4. Pre-ledger user (or backdated row) → modal on app load; doc links open (web: new tab, native: in-app browser); accept → surface='re-consent' rows; modal never returns.
  5. Backend down → app loads normally, console warning only.

🤖 Generated with Claude Code

Design notes / accepted trade-offs

  • Generated constants are committed so CI and static analysis see them without running the generator; predev/prebuild regenerate, and the output is byte-stable under prettier by construction.
  • Version skew self-heals: if content ships before this app redeploys, a re-consent acceptance records the older displayed version (backend drift-warns), the modal reappears next session, and the next FE deploy trues it up.
  • Reveals, not blocking: legal-doc links now live in two maps (CardTermsScreen's LINKS, absolute URLs for native, and ReConsentModal's DOC_LABELS, relative for DocsLink). Unifying them into one legal-links const is a small follow-up.

Summary by CodeRabbit

  • New Features
    • Added prompts for existing users to review and accept updated legal documents before continuing.
    • Added clear document links, acceptance checkbox, loading states, and error handling.
    • Legal document consent is now recorded during account registration and card applications.
  • Bug Fixes
    • Ensured consent reflects the user’s residency when applying for a card.
  • Analytics
    • Added tracking for re-consent modal display and completion actions.

The backend's new consent_records ledger stores what the user was
actually shown, so every acceptance surface now echoes the displayed
document versions instead of letting the server assume: signup sends
terms+privacy via the x-accepted-legal header (the ZeroDev SDK owns the
register/verify body), card apply sends the exact regional doc set from
CardTermsScreen, and versions+hashes are generated from the src/content
legal frontmatter at build time — no hardcoded versions anywhere.

Adds the ToS §17 re-consent flow: a blocking click-through modal when a
published document version moves past the user's last provably-accepted
one. Fails open — a dead consent endpoint never locks the app.
@vercel

vercel Bot commented Jul 20, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Jul 24, 2026 12:44pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cd41036e-d360-45a3-98ba-c02d21030623

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds generated legal-document versions and consent APIs, sends consent records during signup and card applications, and introduces a session-scoped re-consent modal with analytics and acceptance handling.

Changes

Consent ledger flow

Layer / File(s) Summary
Legal version generation
package.json, scripts/generate-legal-versions.mjs
Development and build hooks generate legal document versions, hashes, and TypeScript exports from legal content.
Consent service contracts
src/services/consent.ts
Defines consent data types, signup and card document selections, and status and acceptance API calls.
Signup and card consent submission
src/hooks/useZeroDev.ts, src/app/(mobile-ui)/card/page.tsx, src/services/rain.ts
Passkey registration and card applications include accepted legal-document records in their requests.
Re-consent modal flow
src/components/Global/ReConsentModal/index.tsx, src/app/(mobile-ui)/layout.tsx, src/constants/analytics.consts.ts
Checks consent status, displays required documents, records acceptance, and emits re-consent modal analytics.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ReConsentModal
  participant consentApi
  participant PostHog
  User->>ReConsentModal: Authenticate
  ReConsentModal->>consentApi: getStatus()
  consentApi-->>ReConsentModal: Consent status
  ReConsentModal->>PostHog: Record MODAL_SHOWN
  User->>ReConsentModal: Accept documents
  ReConsentModal->>consentApi: accept(documents)
  consentApi-->>ReConsentModal: Acceptance recorded
  ReConsentModal->>PostHog: Record MODAL_DISMISSED
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: hugo0

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: consent-ledger echo and re-consent flow for ToS v1 phase 2.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/tos-v1-consent-client

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 6231.08 → 6248.85 (+17.77)
Findings: +4 net (+45 new, -41 resolved)

🆕 New findings (45)

  • critical complexity — src/app/(mobile-ui)/card/page.tsx — CC 107, MI 56.8, SLOC 422
  • critical complexity — src/services/rain.ts — CC 66, MI 60.12, SLOC 260
  • high hotspot — src/app/(mobile-ui)/card/page.tsx — 44 commits, +1189/-491 lines since 6 months ago
  • high method-complexity — src/app/(mobile-ui)/layout.tsx:37 — CC 38 SLOC 65
  • high hotspot — src/constants/analytics.consts.ts — 36 commits, +323/-7 lines since 6 months ago
  • high hotspot — src/hooks/useZeroDev.ts — 31 commits, +246/-175 lines since 6 months ago
  • high complexity — src/constants/analytics.consts.ts — CC 1, MI 32.83, SLOC 175
  • medium high-mdd — src/app/(mobile-ui)/card/page.tsx:54 — CardPage: MDD 111.7 (uses across many lines from declarations)
  • medium high-mdd — src/app/(mobile-ui)/layout.tsx:37 — Layout: MDD 70.8 (uses across many lines from declarations)
  • medium high-mdd — src/hooks/useZeroDev.ts:47 — useZeroDev: MDD 58.3 (uses across many lines from declarations)
  • medium high-dlt — src/app/(mobile-ui)/card/page.tsx:54 — CardPage: DLT 50 (calls 50 distinct functions — high context load)
  • medium high-dlt — src/hooks/useZeroDev.ts:47 — useZeroDev: DLT 49 (calls 49 distinct functions — high context load)
  • medium method-complexity — src/services/rain.ts:328 — rainRequest CC 28 SLOC 61
  • medium high-mdd — src/hooks/useZeroDev.ts:59 — handleRegister: MDD 25.0 (uses across many lines from declarations)
  • medium complexity — src/components/Global/ReConsentModal/index.tsx — CC 24, MI 60.39, SLOC 118
  • medium hotspot — src/services/rain.ts — 24 commits, +840/-95 lines since 6 months ago
  • medium high-mdd — src/services/rain.ts:328 — rainRequest: MDD 23.2 (uses across many lines from declarations)
  • medium method-complexity — src/app/(mobile-ui)/card/page.tsx:491 — CC 16 SLOC 51
  • medium react-effect-derives-state — src/app/(mobile-ui)/layout.tsx:62 — useEffect with empty deps + setState — derived state anti-pattern
  • medium react-direct-dom — src/app/(mobile-ui)/layout.tsx:76 — direct DOM: document.querySelector

…and 25 more.

✅ Resolved (41)

  • src/app/(mobile-ui)/card/page.tsx — CC 106, MI 56.91, SLOC 419
  • src/services/rain.ts — CC 64, MI 60.26, SLOC 258
  • src/app/(mobile-ui)/card/page.tsx — 43 commits, +1181/-489 lines since 6 months ago
  • src/app/(mobile-ui)/layout.tsx:36 — CC 38 SLOC 65
  • src/constants/analytics.consts.ts — 35 commits, +322/-7 lines since 6 months ago
  • src/hooks/useZeroDev.ts — 30 commits, +241/-174 lines since 6 months ago
  • src/constants/analytics.consts.ts — CC 1, MI 32.9, SLOC 174
  • src/app/(mobile-ui)/card/page.tsx:53 — CardPage: MDD 110.2 (uses across many lines from declarations)
  • src/app/(mobile-ui)/layout.tsx:36 — Layout: MDD 70.8 (uses across many lines from declarations)
  • src/hooks/useZeroDev.ts:46 — useZeroDev: MDD 57.7 (uses across many lines from declarations)
  • src/app/(mobile-ui)/card/page.tsx:53 — CardPage: DLT 49 (calls 49 distinct functions — high context load)
  • src/hooks/useZeroDev.ts:46 — useZeroDev: DLT 47 (calls 47 distinct functions — high context load)
  • src/services/rain.ts:327 — rainRequest CC 28 SLOC 61
  • src/hooks/useZeroDev.ts:58 — handleRegister: MDD 24.7 (uses across many lines from declarations)
  • src/components/Card/share-asset/ShareAssetD3.tsx — 24 commits, +1525/-1137 lines since 6 months ago
  • src/services/rain.ts:327 — rainRequest: MDD 23.2 (uses across many lines from declarations)
  • src/app/(mobile-ui)/card/page.tsx:485 — CC 16 SLOC 51
  • src/app/(mobile-ui)/layout.tsx:61 — useEffect with empty deps + setState — derived state anti-pattern
  • src/app/(mobile-ui)/layout.tsx:75 — direct DOM: document.querySelector
  • src/app/(mobile-ui)/layout.tsx:36 — Layout: DLT 25 (calls 25 distinct functions — high context load)

…and 21 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/components/Global/ReConsentModal/index.tsx 0.0 7.2 +7.2
src/constants/legal-versions.generated.ts 0.0 4.8 +4.8
src/services/consent.ts 0.0 4.7 +4.7

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2062 ran, 0 failed, 0 skipped, 30.6s

📊 Coverage (unit)

metric %
statements 60.2%
branches 43.6%
functions 49.2%
lines 60.5%
⏱ 10 slowest test cases
time test
3.0s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
0.9s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in validateInviteCode
0.2s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › keeps stickers off the username pill (final pass respects the keep-out)
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle invalid ETH address (missing 0x prefix)
0.1s src/utils/__tests__/url.utils.test.ts › uses the public BASE_URL in Capacitor, not the localhost WebView origin
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid 9-digit US account
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

The single-line entries exceeded printWidth 120, so every predev run
left a prettier-dirty tree; one field per line keeps the generated file
byte-stable under formatting.
@jjramirezn

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

A logout followed by login as a different account in the same SPA
session skipped the second user's consent-status check.
@jjramirezn

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@jjramirezn

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@jjramirezn

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@package.json`:
- Around line 15-16: Update the package scripts for dev:clean, dev:fallback, and
analyze so each supported entrypoint runs scripts/generate-legal-versions.mjs
before its existing command, ensuring legal version and hash constants are
regenerated regardless of how development or analysis is started.

In `@src/components/Global/ReConsentModal/index.tsx`:
- Around line 35-48: Update the ReConsentModal effect around lastCheckedUserId,
outdatedDocs, and checked to reset consent UI state whenever the authenticated
userId changes, ensuring prior users’ modal and checkbox state is not reused.
Reset checked after a successful consent submission, and add cancellation or a
userId/token guard to the consentApi.getStatus response before calling
setOutdatedDocs so late responses cannot update state for another user.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b279ef57-4a5c-4541-a76e-09baf3eaf5e7

📥 Commits

Reviewing files that changed from the base of the PR and between 6c20a70 and a0bccaf.

⛔ Files ignored due to path filters (1)
  • src/constants/legal-versions.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (9)
  • package.json
  • scripts/generate-legal-versions.mjs
  • src/app/(mobile-ui)/card/page.tsx
  • src/app/(mobile-ui)/layout.tsx
  • src/components/Global/ReConsentModal/index.tsx
  • src/constants/analytics.consts.ts
  • src/hooks/useZeroDev.ts
  • src/services/consent.ts
  • src/services/rain.ts

Comment thread package.json Outdated
Comment thread src/components/Global/ReConsentModal/index.tsx
Every dev/build entrypoint now regenerates the legal-version constants
(dev:clean, dev:fallback, analyze bypassed the predev hook and could
ship stale version/hash echoes). ReConsentModal state is fully isolated
per account: switching users clears the doc list, checkbox, and error,
a slow status response for the previous account is discarded, and the
checkbox resets after a successful acceptance.
@jjramirezn
jjramirezn marked this pull request as ready for review July 20, 2026 21:50
@jjramirezn
jjramirezn requested a review from Hugo0 July 20, 2026 21:50

@Hugo0 Hugo0 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — lean and internally consistent: one service file, one modal, one build-time generator, no speculative abstractions. Backend-decided needsAcceptance (no client date math), fail-open status fetch, fail-closed acceptance is the right split. Verified the committed legal-versions.generated.ts is byte-identical to the content submodule at the pinned gitlink, and the card doc sets exactly match what CardTermsScreen actually displays. Wire contract checked field-by-field against peanut-api #1209 — matches.

Asks before/alongside merge:

  1. Add tests — this modal gates the entire app and has zero. Minimum set (patterns in-tree, see StaleCardApproval/__tests__/ReEnableModal.test.tsx):
    • fail-open: status-fetch failure must NOT show/lock the modal (the load-bearing safety property, currently unasserted)
    • accept failure keeps modal + shows retry error
    • account-switch reset (the regression CodeRabbit already caught once)
    • cardConsentDocuments(true/false) doc sets — silent drift here mis-ledgers legal consent, the exact bug class this PR exists to prevent
  2. Swap the two console.errors for Sentry captures. If POST /accept 500s systematically, every user is stuck behind an undismissable retry modal and prod would be silent about it.
  3. Product sign-off on the big-bang wave: every existing user gets the modal once after the backend ships (deliberate — it backfills the ledger). Fine legally, but someone should consciously OK the UX hit, or gate the mount for a quieter rollout.
  4. CORS cross-check with #1209: x-accepted-legal must be in the API's Access-Control-Allow-Headers before native release — on Capacitor the passkey call is cross-origin and a rejected preflight fails the whole signup, not just the header.

Notes (non-blocking):

  • Known nag-loop if content ships before the FE redeploys (accepting records the older version → modal returns every session until deploy). Acknowledged in the PR body; keep deploy order in mind.
  • Modal can pop mid-flow (mounted in layout, shows when the async fetch resolves) — low probability, worth knowing.
  • Generator's last_updated regex is m-flagged over the whole file; anchoring it to the frontmatter block is a one-line tightening.
  • eslint failure is base debt (only touched-file hit is pre-existing useZeroDev.ts:161); required ci-success is green.

…ent failures to Sentry

Review follow-ups (Hugo on #2456):

- ReConsentModal tests: fail-open on status failure (must never lock the
  app), accept failure keeps the retry path, stale-response discard on
  account switch (regression already caught once in review), undisplayable
  slug filtering, once-per-user-per-session check.
- consent service tests: pin the exact signup/card doc sets per region and
  that every echoed entry carries its own generated version+hash — silent
  drift here mis-ledgers legal consent.
- Both catch blocks now Sentry.captureException instead of console.error:
  a systematic /accept failure traps every user behind an undismissable
  modal, and prod must be able to see that.
@jjramirezn

Copy link
Copy Markdown
Contributor Author

Review asks addressed (@Hugo0) in f06fd71:

1. TestsReConsentModal/__tests__/index.test.tsx (7 tests, ReEnableModal.test.tsx pattern):

  • fail-open: a failed status check never shows/locks the modal (the load-bearing safety property)
  • accept failure keeps the modal, shows the error, leaves retry enabled
  • account-switch: stale status response from the previous user is discarded; state resets per user, one check per user per session
  • undisplayable-slug filtering (docs this build doesn't know never trigger the modal)

Plus src/services/__tests__/consent.test.ts pinning the exact signupConsentDocuments / cardConsentDocuments(true|false) slug sets and that every entry carries its own generated version+hash — the silent-drift-mis-ledgers-consent class.

2. Sentry — both console.errors are now Sentry.captureException with feature: 're-consent' tags (action: 'status' / 'accept'), so a systemic /accept failure trapping users behind the modal is visible in prod.

3. Big-bang wave — product call is yours; flagging that it stands: every existing user sees the modal once after the backend deploys.

4. CORS — verified on the API side (no change needed): our @fastify/cors config sets no allowedHeaders, so it reflects the preflight's requested headers for permitted origins (incl. capacitor://); the native app's existing x-username header already proves the path. Details on peanutprotocol/peanut-api-ts#1209.

Erasure runbook (the API-side ask): https://github.com/peanutprotocol/mono/pull/53.

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.

2 participants