chore: back-merge main → dev (SP-153 + hotfixes) - #2586
Conversation
ENS names can hold a different address per chain (ENSIP-11). The withdraw flow resolved every name to its mainnet record and sent on the selected chain — funds loss when the name points elsewhere on that chain (reported by an external ENS-savvy tester with test.ses.eth: mainnet and Arbitrum records differ). - resolveEns/validateAndResolveRecipient accept the destination chainId and forward it to /ens/:name?chainId= (api-ts #1236). - Switching chains after typing a name re-resolves it for the new chain instead of silently keeping the old address. - The resolved address is now shown under the input as soon as the name validates, and again in the compatibility warning modal — the user sees where funds go BEFORE any warning/confirm step. (cherry picked from commit f2c0aac)
…flight Between switching chains and the new resolution landing, recipient.address still holds the previous chain's address and Review stayed clickable — the exact wrong-chain send this PR fixes, in a narrow race window. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 679c359)
crypto-withdraw-confirm.test.tsx (new on dev) mocks general.utils with only the functions the page used at the time; the compatibility-modal change now also renders printableAddress from that module. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> (cherry picked from commit 4e56b63)
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. (cherry picked from commit e1bdcf9)
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. (cherry picked from commit c0a3448)
A logout followed by login as a different account in the same SPA session skipped the second user's consent-status check. (cherry picked from commit a0bccaf)
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. (cherry picked from commit be5545b)
…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. (cherry picked from commit f06fd71)
Lock/cancel sign a withdrawal whose purpose is draining Rain collateral back to the wallet, but since cb302d3 removed the smartBalance:0n input, routing fell through to live-balance strategy selection: any user whose wallet USDC covered their spending power routed smart-only, tripped the modals' own strategy check, and could neither lock nor cancel ('Unexpected withdrawal strategy', prod, 3 users affected). Also fail closed when the card overview hasn't loaded: undefined read as zero spending power, silently skipping the withdrawal and getting the action rejected server-side ('Withdrawal signature required'). Port of the dev-based #2570 (closed in favor of this main-based hotfix; that branch holds the i18n-ified variant for the dev back-merge).
external-wallet withdrawals have no recipient username or parsed identifier, so the drawer's userName was undefined and the header rendered the literal string. fall back to the recipient address (shortened by printableUserHandle) like recipientName already does.
…lash a cold mount (mobile reopening the pwa reloads the page) showed the raw address for a beat while the lookup ran — pay the 404, engage the client fallback, resolve, flip. persist resolved names for 24h and paint them immediately on mount while the lookup revalidates. an authoritative server "no name" evicts the entry so stale names can't mask reality.
coderabbit: JSON.parse('null') passes the try/catch but null[address]
throws during render. treat any non-object root as an empty cache.
…k href in sync code-review findings: (1) with the server route erroring (today's prod state) the only eviction path was server null, which never fires — a client lookup settling '' (justaname's not-found) now also masks and evicts, so a released/transferred ens name can't keep painting from cache. (2) AddressLink's effect never reset urlAddress on the name→address downgrade, leaving the href pointing at a name the address may no longer own — reachable now that cached names can be evicted mid-mount. (3) server '' responses treated same as null.
…0729-155858 content: publish latest to production (src/content → peanut-content@fc31f07)
…0729-160449 content: publish latest to production (src/content → peanut-content@f33d67f)
…ollateral-only fix(card): force collateral-only routing on lock/cancel
…undefined-recipient fix(withdraw): stop success receipt showing 'Sent to undefined'
fix(ens): warm-cache resolved names to stop the address→name flash on mount
The modal shipped as a hard gate: preventClose, hidden close button, and a single Accept CTA. It mounts in the mobile-ui layout, so it covered the whole app, including /withdraw. That conflicts with the terms it enforces. §17.2 gives material changes a 30-day runway and offers the click-through as a way to accept sooner, by choice. We published on 2026-07-15, so the new terms take effect around 2026-08-14; a hard gate enforces them about two weeks early. §17.3 says a user who does not agree must stop using the Services. For a non-custodial wallet that must still leave a path to their own funds. So the prompt now defers instead of blocking: - "Not now" (plus close, backdrop and Escape) dismisses it. The checkbox does not gate the exit. - A dismissal snoozes the prompt for 3 days per user, in localStorage. It never writes a ledger row, because a refusal is not consent. - Accept reports modal_cta_clicked, not modal_dismissed. Both events existed already. We need the accept-vs-refuse ratio to measure the rollout. - Copy no longer says "To keep using Peanut". That claim is no longer true. Accept stays the primary action: purple, shadow, first in the stack.
…interval "Not now" now snoozes until the documents actually take effect — the frontmatter version plus the 30 days our own ToS §17.2 promises — instead of a flat 3 days. A document posted today buys the user its full notice period. Past that date §17.3 already makes continued use acceptance, so the prompt only still asks in order to record explicit consent. That earns a gentle cadence, not a prompt on every app open, so the snooze floors at MIN_SNOOZE_DAYS.
The committed file said card-terms-international was 2026-06-01. The content submodule this branch pins says 2026-07-14 — the §8.4 Nigerian Users clause. `predev`/`prebuild` regenerate this file, so any dev server or Vercel build already produced the newer value and left the tree dirty. WARNING, cross-repo: peanut-api CURRENT_LEGAL_VERSIONS still says 2026-06-01 for the same document. sanitizeEchoedDocuments treats a client version newer than the server's as an attack and clamps it, dropping the hash. So the ledger would record 2026-06-01 for users who were shown 2026-07-14, and the clause update would never trigger re-consent. The constant needs the same bump in peanut-api-ts#1255.
fix(re-consent): make the terms prompt escapable, per our own ToS §17
[TASK-20903] Prod release SP-153 (cherry-picks) — ENS + consent client → main
/card-terms-us etc. 404'd while /en/card-terms-us worked — same redirect the terms/privacy slugs already have.
hotfix: redirect bare card legal slugs to /en pages
Brings dev up to date with everything that shipped to production on 29 July:
the consent-ledger echo and escapable re-consent modal (tos-v1 phase 2),
chain-aware ENS resolution with the visible resolved address in withdraw, and
four hotfixes (card legal redirects, ENS warm cache, withdraw receipt, card
lock collateral routing).
Four conflicts, all the same shape: dev had translated strings that main still
carries in English, or each side added a different dependency to the same
callback. Resolutions keep both sides:
- GeneralRecipientInput / card page: dependency arrays unioned. Verified each
dep is really used — handleApply reads both `pendingTerms` and `t`.
- withdraw/crypto page + Initial.withdraw.view: kept dev's `t(...)` calls over
main's hardcoded English, and kept main's comment explaining why the resolved
address is surfaced before the review step.
One test needed a fix that neither branch could have caught alone.
LockCardModal.test.tsx arrived with main's collateral-routing hotfix and was
written against the pre-i18n component, so it has no next-intl context. dev had
since added useTranslations('card') to both modals. Git merged both cleanly and
the result threw on render. Wrapped the test in NextIntlClientProvider with the
real en.json — a key-echoing stub would break the assertions, which match on
user-visible strings ("Slide to Lock", "Card locked").
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe PR adds legal-document version generation, consent submission and re-consent prompting, card lock/cancel safeguards, ENS name caching, receipt fallbacks, localized legal redirects, and a withdrawal destination clarification. ChangesLegal consent and document versioning
Card lock and cancellation controls
Name resolution and receipt display
Supporting UI clarification
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant ReConsentModal
participant consentApi
participant localStorage
User->>ReConsentModal: Open application layout
ReConsentModal->>consentApi: getStatus()
consentApi-->>ReConsentModal: Consent status
User->>ReConsentModal: Accept updated documents
ReConsentModal->>consentApi: accept(documents)
User->>ReConsentModal: Select Not now
ReConsentModal->>localStorage: Store snooze timestamp
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
Code-analysis diffPainscore total: 6739.08 → 6764.48 (+25.4) 🆕 New findings (61)
…and 41 more. ✅ Resolved (56)
…and 36 more. 📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/features/payments/shared/components/PaymentSuccessView.tsx (1)
175-181: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winRaw address fallback may bypass the already-formatted
recipientName.
recipientName(lines 117-128) already resolves throughresolveRecipientDisplay(...)using the samechargeDetails.requestLink?.recipientAddresswhen no username/identifier exists. Placing the rawchargeDetails.requestLink?.recipientAddressahead ofrecipientNamein theuserNamefallback means the drawer could show an untruncated raw address while the receipt header shows the formattedrecipientNamefor the same case — a display inconsistency between the two receipt surfaces.Consider reordering to prefer the already-resolved
recipientName(which itself falls back to the address) instead of duplicating the raw address ahead of it:♻️ Suggested reorder
userName: user?.username || parsedPaymentData?.recipient?.identifier || - chargeDetails.requestLink?.recipientAddress || - recipientName, + recipientName || + chargeDetails.requestLink?.recipientAddress,Please confirm whether
resolveRecipientDisplay/TransactionDetailsDraweralready format/truncate raw addresses before deciding if this reorder is needed.🤖 Prompt for 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. In `@src/features/payments/shared/components/PaymentSuccessView.tsx` around lines 175 - 181, Update the userName fallback in PaymentSuccessView to prefer the already-resolved recipientName before chargeDetails.requestLink?.recipientAddress, preserving the existing username and parsed recipient identifier precedence. Reuse recipientName’s resolveRecipientDisplay formatting and avoid displaying the raw address directly when the formatted value is available.src/hooks/usePrimaryNameServer.ts (1)
36-51: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winStale cache entries are never pruned, only ignored.
getCachedNameskips expired entries butreadNameCache/writeCachedNamenever remove them, soens-primary-name-cachegrows monotonically with every distinct address ever viewed (e.g. transaction history counterparties) and is only ever trimmed when the same address resolves again. Low practical risk given the small per-entry size, but a cheap fix.♻️ Proposed fix: prune expired entries on read
function readNameCache(): NameCache { if (typeof window === 'undefined') return {} try { // guard the root shape — JSON.parse("null") etc. passes but would blow up on lookup const cache: unknown = JSON.parse(window.localStorage.getItem(CACHE_KEY) ?? '{}') - return cache && typeof cache === 'object' && !Array.isArray(cache) ? (cache as NameCache) : {} + if (!cache || typeof cache !== 'object' || Array.isArray(cache)) return {} + const now = Date.now() + return Object.fromEntries( + Object.entries(cache as NameCache).filter(([, entry]) => now - entry.ts < PRIMARY_NAME_TTL_MS) + ) } catch { return {} } }🤖 Prompt for 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. In `@src/hooks/usePrimaryNameServer.ts` around lines 36 - 51, Prune expired cache entries when loading the cache in readNameCache, using PRIMARY_NAME_TTL_MS and each entry’s timestamp, and persist the cleaned cache through the existing cache-writing flow. Keep invalid or unavailable storage behavior returning an empty cache, and preserve getCachedName’s current lookup semantics.
🤖 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 `@src/components/Card/LockCardModal.tsx`:
- Around line 69-74: The loading failure messages in LockCardModal and
CancelCardModal are hard-coded English and must use the existing card
translations. In both files, update the overview-loading error paths to throw
the same appropriate t('errors.…') translation key, reusing each modal’s
existing t function and preserving the current fail-closed behavior.
---
Nitpick comments:
In `@src/features/payments/shared/components/PaymentSuccessView.tsx`:
- Around line 175-181: Update the userName fallback in PaymentSuccessView to
prefer the already-resolved recipientName before
chargeDetails.requestLink?.recipientAddress, preserving the existing username
and parsed recipient identifier precedence. Reuse recipientName’s
resolveRecipientDisplay formatting and avoid displaying the raw address directly
when the formatted value is available.
In `@src/hooks/usePrimaryNameServer.ts`:
- Around line 36-51: Prune expired cache entries when loading the cache in
readNameCache, using PRIMARY_NAME_TTL_MS and each entry’s timestamp, and persist
the cleaned cache through the existing cache-writing flow. Keep invalid or
unavailable storage behavior returning an empty cache, and preserve
getCachedName’s current lookup semantics.
🪄 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: d6e4d53a-9463-41c9-8411-713d985a9dfd
⛔ Files ignored due to path filters (1)
src/constants/legal-versions.generated.tsis excluded by!**/*.generated.*
📒 Files selected for processing (21)
package.jsonredirects.jsonscripts/generate-legal-versions.mjssrc/app/(mobile-ui)/card/page.tsxsrc/app/(mobile-ui)/layout.tsxsrc/app/(mobile-ui)/withdraw/crypto/page.tsxsrc/components/Card/CancelCardModal.tsxsrc/components/Card/LockCardModal.tsxsrc/components/Card/__tests__/LockCardModal.test.tsxsrc/components/Global/AddressLink/index.tsxsrc/components/Global/ReConsentModal/__tests__/index.test.tsxsrc/components/Global/ReConsentModal/index.tsxsrc/components/Global/ReConsentModal/utils.tssrc/constants/analytics.consts.tssrc/features/payments/shared/components/PaymentSuccessView.tsxsrc/hooks/__tests__/usePrimaryNameServer.test.tsxsrc/hooks/usePrimaryNameServer.tssrc/hooks/useZeroDev.tssrc/services/__tests__/consent.test.tssrc/services/consent.tssrc/services/rain.ts
| // An unloaded overview reads as zero spending power below, which | ||
| // would skip the withdrawal and get the lock rejected by the | ||
| // backend ("Withdrawal signature required"). Fail closed instead. | ||
| if (!overview) { | ||
| throw new Error('Card details still loading — please retry in a moment') | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Localize the loading failure message.
Both modals expose a hard-coded English error through setError, bypassing the existing card translations.
src/components/Card/LockCardModal.tsx#L69-L74: replace the literal with at('errors.…')key.src/components/Card/CancelCardModal.tsx#L63-L68: use the same translated key.
Based on supplied library context, card errors are localized through t(...).
📍 Affects 2 files
src/components/Card/LockCardModal.tsx#L69-L74(this comment)src/components/Card/CancelCardModal.tsx#L63-L68
🤖 Prompt for 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.
In `@src/components/Card/LockCardModal.tsx` around lines 69 - 74, The loading
failure messages in LockCardModal and CancelCardModal are hard-coded English and
must use the existing card translations. In both files, update the
overview-loading error paths to throw the same appropriate t('errors.…')
translation key, reusing each modal’s existing t function and preserving the
current fail-closed behavior.
Back-merge
main→dev. Brings dev up to date with everything that shipped to production on 29 July. Pairs with peanut-api-ts#1258.What comes across (27 commits)
Conflicts (4), all resolved as unions
Every one was the same shape: dev had translated a string main still carries in English, or each side added a different dependency to the same callback.
GeneralRecipientInputtkept alongsidechainIdcard/page.tsx[advanceFromApplyResponse, pendingTerms, t]. VerifiedhandleApplyreally reads bothpendingTerms(line 356) andt(line 367)withdraw/crypto/page.tsxt(...)over main's hardcoded English; kept main's comment explaining why the resolved address is surfacedInitial.withdraw.view.tsxuseTranslationsimport andt('resolvesTo')Confirmed after resolving: 0 conflict markers, the chain-switch re-resolution effect survived, and the re-consent modal is still the escapable version — 0 occurrences of
preventClose/hideModalCloseButton,Not nowpresent.LockCardModal.test.tsxarrived with main's collateral-routing hotfix (#2571) and was written against the pre-i18n component. dev had since addeduseTranslations('card')to both modals. Git merged both sides cleanly and the result threw on render — 6 failures, alluseTranslationswith no next-intl context.This is the class of break a back-merge exists to surface: two independently-correct changes that never touch the same line.
Fixed by wrapping the test in
NextIntlClientProviderwith the realsrc/i18n/app/messages/en.json, matching the convention inActivationCTAs.test.tsx. A key-echoing stub (useTranslations: () => k => k) would have made the suite pass while silently gutting it — the assertions match user-visible strings (Slide to Lock,Card locked,Card canceled), anden.jsoncarries exactly those.devare now redundant#2456(consent FE) and#2501(ENS) — their content shipped tomainas cherry-picks, so it lands here with different SHAs. Once this merges, both should be closed, not merged.QA
tsc --noEmit: 0 errors.prettier --check .clean.Risk
No new product code — every line already runs in production; this only moves
devto match. The one authored change is the test wrapper. The real risk is the opposite: leavingdev27 commits behind, so the next release branch silently omits today's work.Summary by CodeRabbit
New Features
Bug Fixes