feat: consent-ledger echo + re-consent click-through (tos-v1 phase 2) - #2456
feat: consent-ledger echo + re-consent click-through (tos-v1 phase 2)#2456jjramirezn wants to merge 5 commits into
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAdds 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. ChangesConsent ledger flow
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
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Code-analysis diffPainscore total: 6231.08 → 6248.85 (+17.77) 🆕 New findings (45)
…and 25 more. ✅ Resolved (41)
…and 21 more. 📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
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.
|
@coderabbitai review |
✅ Action performedReview finished.
|
A logout followed by login as a different account in the same SPA session skipped the second user's consent-status check.
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
src/constants/legal-versions.generated.tsis excluded by!**/*.generated.*
📒 Files selected for processing (9)
package.jsonscripts/generate-legal-versions.mjssrc/app/(mobile-ui)/card/page.tsxsrc/app/(mobile-ui)/layout.tsxsrc/components/Global/ReConsentModal/index.tsxsrc/constants/analytics.consts.tssrc/hooks/useZeroDev.tssrc/services/consent.tssrc/services/rain.ts
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.
Hugo0
left a comment
There was a problem hiding this comment.
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:
- 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
- Swap the two
console.errors for Sentry captures. IfPOST /accept500s systematically, every user is stuck behind an undismissable retry modal and prod would be silent about it. - 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.
- CORS cross-check with #1209:
x-accepted-legalmust be in the API'sAccess-Control-Allow-Headersbefore 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_updatedregex 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); requiredci-successis 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.
|
Review asks addressed (@Hugo0) in f06fd71: 1. Tests —
Plus 2. Sentry — both 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 Erasure runbook (the API-side ask): https://github.com/peanutprotocol/mono/pull/53. |
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.
scripts/generate-legal-versions.mjs(predev + prebuild, output committed) derivesslug → { version, hash }from thesrc/contentlegal frontmatter (last_updated+ sha256 of eachen.md). No hardcoded versions; bumping the content submodule refreshes the constants on next build./passkeys/register/verifybody, souseZeroDev.handleRegistersends displayed terms+privacy versions via thex-accepted-legalheader (passkeyServerHeaders).rainApi.applyForCardgainsacceptedDocuments; the card page sends the exact set CardTermsScreen showed (US: card-terms-us + card-esign + card-privacy; international: card-terms-international + card-esign).ReConsentModal(mobile-ui layout) checksGET /users/consent/statusonce 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 viaPOST /users/consent/accept.src/services/consent.ts(no server actions).Risks / breaking changes
QA
pnpm dev→ predev regeneratessrc/constants/legal-versions.generated.ts(7 docs).terms+privacy@2026-07-15with the generated hashes.surface='card-apply'.surface='re-consent'rows; modal never returns.🤖 Generated with Claude Code
Design notes / accepted trade-offs
CardTermsScreen'sLINKS, absolute URLs for native, andReConsentModal'sDOC_LABELS, relative for DocsLink). Unifying them into one legal-links const is a small follow-up.Summary by CodeRabbit