Release 2026-06-17: dev → main#2236
Conversation
Sod-tile 'Touched Grass' sticker for the met-us-IRL mailer. Adds the SVG, the BADGES icon/copy entry, and code/utm campaign mappings so ?code=touched_grass, ?campaignTag=TOUCHED_GRASS, and utm_campaign=touched-grass all award it.
Mortarboard 'Event Alumni' sticker (badge-draft). Adds the SVG, BADGES icon/copy entry, and code/utm mappings (alumni) so ?code=, ?campaignTag=, and ?utm_campaign= all award it.
Badge descriptions are user-facing; switch the Seedling line from third
person ('They shill...') to 'You shill...'. Pairs with the BE rename +
reactivation of the Seedling badge.
…e restart-identity modal getBadgeIcon fell back to PEANUTMAN_LOGO (StaticImageData, typed `any` by the svg shim) while shareAssetLayout types iconUrl as string and ShareAssetD3 feeds it to a raw <img src> — an unknown badge code (the recurring FE-BADGES-drop incident) would render a broken stamp. Unwrap .src and pin the contract with a test. UnlockedRegions' provider-rejection override only promoted fixable/blocked, so the restart-identity copy + handleRestartIdentity CTA already wired in the modal were unreachable from this predicate — those users fell back to the generic start flow. Include restart-identity. Plus two lint nits from the #2218 review: unused useRouter in the payment error boundary, missing alt on the careers mascot.
…st asset stub
/code-review pass on this PR converged on two structural fixes:
The predicate now reads state !== 'happy' instead of enumerating the three
non-happy members — the enumeration pattern is how restart-identity got missed
in the first place, and a future fourth state would have repeated it.
The jest asset stub (jest-transform-stub) flattened image imports to a bare
string while Next yields StaticImageData — tests of .src-reading code ran
against fiction, which is exactly how the original object-into-string fallback
shipped unnoticed. Point the mapper at a {src,width,height} stub and drop both
per-file mascot mocks this PR had added. jest-transform-stub stays in
devDependencies for now: removing it forces a pnpm lockfile re-resolve that
drags Sentry onto different otel peers — that cleanup belongs in a deps PR.
fix: string badge-icon fallback + reachable restart-identity modal (release-audit follow-ups)
PostHog's Persons-page free-text search is hardcoded server-side to properties.email / properties.name / person id / distinct_id — it never reads username, so no Peanut user was findable by username (0 of 2,658 persons have email or name set). Duplicate username into name on identify so the search box works. Existing persons were backfilled via ops/scripts/posthog/backfill-person-name.mjs (mono).
…searchable fix(analytics): make persons searchable by username in PostHog
The event-badge CTA is a bare ?campaign=EVENT_ALUMNI link (no invite code). That path only awarded the badge during new-account registration in useZeroDev, so the 2025 event cohorts — who already have accounts — clicked through and never got it (and were dumped into /setup signup). Treat EVENT_ALUMNI like the skip campaign so the logged-in auto-claim effect fires and awards on a returning visit.
200x200 on Peanut-yellow so it blends into the EmailLayout header. Served at peanut.me/email/badge-event-alumni.png — the imageUrl the event-badge template (email-campaigns 003) points at.
Brazil PIX sends from the withdraw/send screen used the offramp/withdraw
endpoint, which is gated on full Manteca KYC (a rail carrying mantecaUserId).
Users who only hold the QR-pool "pay" capability — a strictly broader cohort —
could reach the PIX entry but hit a verification wall they couldn't clear,
even though they can already pay any PIX key by pasting it into the QR scanner.
Reuse that exact path: wrap the key into a BR Code and hand off to /qr-pay,
where the amount is entered and the capability gate (canDo('pay', manteca))
is enforced. All Brazil-PIX send entry points funnel through
/withdraw/manteca?country=brazil&method=pix, so a single delegation at that
page flips the endpoint without touching the Argentina or bank flows. The
saved-account withdraw path (no method=pix) intentionally stays on the
withdraw endpoint.
- pixKeyToQrPayUrl: shared wrap→/qr-pay helper, now used by both the scanner
overlay and the new PIX-key entry (single source of truth).
- PixKeySend.view: lightweight key-entry surface for the send flow.
The PIX-key normalize block (trim → EMVCo passthrough → +55 phone canonicalize) was duplicated across the manteca withdraw input and the new send entry, and a third whitespace-only copy lived inline in the withdraw test. Promote it to one exported helper and point all three at it, so a future change to phone/EMVCo handling can't make the two entries disagree.
feat(withdraw): route Brazil PIX sends through the QR-payment endpoint
…epare intent
For a collateral-funded ("mixed") link, useSpendBundle already returns the Rain
prepare intent id as `intentId`, but useCreateLink dropped it — so /send-links
created a second SEND_LINK intent instead of reconciling with the prepare leg
(duplicate in history, wrong amount, uncancellable phantom).
Surface that id out of useCreateLink as `preparationId` and pass it to
sendLinksApi.create. Undefined for smart-only / non-card links, so those are
unchanged. Pairs with the backend adopt in peanut-api-ts.
…ion-id fix(send-links): thread preparationId so the backend can adopt the prepare intent
Branch was 141 commits behind dev. dev refactored the /invite campaign maps out of InvitesPage.tsx into campaign-maps.ts (+ campaign-maps.test.ts guard), which conflicted with this branch's inline edits to those maps. Resolved by migrating the branch's intent into dev's shape: - campaign-maps.ts: added the alumni/touched_grass invite-code + utm entries. - InvitesPage.tsx: took dev's structure (PeanutWavingHello mascot, imported maps), then fixed TOUCHED_GRASS claimability (see follow-up): it was never registered as a bare campaign, so every touched-grass link dead-ended at the "Invalid Invite Code" screen. Introduced classifyBareCampaign() to split "claimable without invite" (skip + event_alumni + touched_grass) from "promises a card-waitlist skip" (skip + event_alumni only) so the vanity touched_grass badge is claimable without showing misleading skip copy. - campaign-maps.test.ts: guards classifyBareCampaign + the new map entries (the existing guard already asserts every mapped code exists in BADGES). badge.utils.ts auto-merged cleanly — verified TOUCHED_GRASS + EVENT_ALUMNI entries and the second-person Seedling copy all survived (no silent drop).
FE side of the link-granted Loud Mouth badge (inverse of SHHHHH): the sticker SVG, its BADGES entry (icon path + profile copy), and the notsoshhh invite-code → NOT_SO_SHHHH campaign mapping. Backend wiring is the paired peanut-api-ts PR.
feat(badges): add TOUCHED_GRASS + EVENT_ALUMNI badges
Same class as the withdraw Continue throw + Send card: handlers whose failure
path produces no visible change on a control the user can't escape. From the
dead-button audit, the 4 remaining actionable ones:
- HIGH EnableAutoBalanceBanner ('Finish setting up your card'): the modal is
preventClose + hideModalCloseButton and 'void grant()' discarded the result,
so a cancelled/failed passkey (common on iOS/1Password) trapped the user with
a CTA that did nothing. Now surfaces the error and adds a 'Skip for now'
escape once a grant fails (the grant re-prompts on first card spend anyway).
+ tests.
- HIGH UnlockedRegions ('verify now' → nothing happens; customer-reported by
Federico): setSelectedRegion(null) closed the modal before the await, then any
initiate error landed in an off-screen inline <p>. Now a dismissible error
modal (Try again + Contact support) surfaces flow.error.
- MED CancelDepositActions: cancel failures were only console.error'd, so users
thought a deposit was cancelled when it wasn't. Added an error state + alert.
- MED useMultiPhaseKycFlow ToS confirm: the await had no try/catch, freezing the
'preparing' modal ~30s with no feedback. Wrapped it; on failure surface the
recovery state immediately (mirrors BridgeTosStep).
#5 (payQR non-MANTECA no-op) left as-is: currently unreachable, adding handling
to an impossible path is the opposite anti-pattern. Documented only.
Code-review catch: a ROW (rest-of-world) region has no provider/rail, so an initiate returns a terminal 'not available in your region yet'. The new error modal was showing 'Verification couldn't start' + 'Try again', which re-issues the identical ROW request and loops. Gate retriability on the region's provider: terminal ROW → neutral title + 'Got it'; transient → 'Try again' + support.
…ning fix(ui): harden 4 dead-button / silent-no-op handlers (card-setup lockout, KYC region verify, cancel-deposit, ToS confirm)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
WalkthroughThis PR adds a Brazil PIX key send flow (new ChangesBrazil PIX Send Flow
Bare Campaign Classification and New Badges
Send Link
Error Handling and UX Improvements
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
Code-analysis diffPainscore total: 5716.56 → 5732.02 (+15.46) 🆕 New findings (61)
…and 41 more. ✅ Resolved (59)
…and 39 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: 3
🧹 Nitpick comments (3)
src/utils/__tests__/withdraw.utils.test.ts (1)
281-310: ⚡ Quick winAdd a regression case for
+55phone keys with separators in shared normalization tests.Now that
normalizePixInputis the shared path, include a case like+55-11-99999-9999to lock canonical phone output behavior and prevent drift.🤖 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/utils/__tests__/withdraw.utils.test.ts` around lines 281 - 310, The test cases in the it.each array for the 'Pasted values with whitespace' describe block currently cover phone numbers with spaces but are missing coverage for phone numbers with dash/hyphen separators. Add a new test case to the it.each array with a phone number formatted with dashes (such as +55-11-99999-9999) to ensure the normalizePixInput function properly handles and normalizes this common phone formatting pattern alongside the existing whitespace test cases.src/components/Withdraw/views/PixKeySend.view.tsx (1)
31-33: ⚡ Quick winUse
normalizePixInputinside validation to keep a single normalization source.
validatePixDestinationreimplements normalization inline. ReusingnormalizePixInputhere avoids future drift between this view and other PIX entry points.Proposed fix
- const normalized = isPixEmvcoQr(value.trim()) ? value.trim() : value.replace(/\s/g, '') + const normalized = normalizePixInput(value)🤖 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/Withdraw/views/PixKeySend.view.tsx` around lines 31 - 33, The validatePixDestination function contains inline normalization logic that duplicates the normalization behavior. Replace the inline normalization logic (the ternary expression checking isPixEmvcoQr and conditionally trimming or removing spaces) with a call to the normalizePixInput function to ensure a single source of truth for PIX input normalization across all entry points.src/components/Invites/campaign-maps.test.ts (1)
47-58: ⚡ Quick winAdd regression tests for campaign alias forms.
Please add explicit expectations for
campaign=alumniandcampaign=touched-grassso bare-claim behavior is pinned for both accepted naming styles.Also applies to: 73-76
🤖 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/Invites/campaign-maps.test.ts` around lines 47 - 58, The test for classifyBareCampaign currently only verifies behavior for the uppercase snake_case campaign names (EVENT_ALUMNI and TOUCHED_GRASS) but does not test their lowercase kebab-case alias forms (alumni and touched-grass). Add additional expect calls within the test block to verify that classifyBareCampaign produces identical results when called with the alias forms alumni and touched-grass as it does with EVENT_ALUMNI and TOUCHED_GRASS respectively. Apply the same additions to the other test block mentioned in the comment.
🤖 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/Invites/campaign-maps.ts`:
- Around line 41-43: The campaign classification system does not normalize
campaign aliases before checking them against the BARE_VANITY_CAMPAIGNS and
WAITLIST_SKIP_CAMPAIGNS sets. This means that variations like 'alumni' or
'touched-grass' are not recognized as matching their canonical forms
'event_alumni' and 'touched_grass'. Create a normalization function that maps
campaign aliases to their canonical names, then apply this normalization to the
campaign parameter before it is classified in the classifyBareCampaign function
to ensure all recognized variants are properly handled and do not fall into the
invalid-invite path.
In `@src/hooks/useMultiPhaseKycFlow.ts`:
- Around line 339-342: The completeFlow() function on line 341 is called
unconditionally after confirmBridgeTosAndAwaitRails, but that helper can exit
after retries without throwing an error even when needsTos is still true,
meaning ToS propagation failed to settle. Modify the
confirmBridgeTosAndAwaitRails helper to throw an error when it exits due to
needsTos still being true after exhausting retries, so that the catch block
handles the failure and completeFlow() is only called when the operation truly
succeeds.
In `@src/utils/withdraw.utils.ts`:
- Around line 236-239: The normalizePixPhoneNumber function is not properly
canonicalizing phone numbers that start with the `+` prefix, leaving separators
intact in inputs like `+55-11-99999-9999`. To fix this, you need to ensure that
when normalizePixInput calls normalizePixPhoneNumber on a phone number
(determined by isPixPhoneNumber check), the returned value has all non-digit
separators removed while preserving the `+` country code prefix. Either update
the normalizePixPhoneNumber function to remove all separators from phone numbers
starting with `+`, or add additional logic in normalizePixInput after the
normalizePixPhoneNumber call to strip separators from the result while keeping
the canonical format intact.
---
Nitpick comments:
In `@src/components/Invites/campaign-maps.test.ts`:
- Around line 47-58: The test for classifyBareCampaign currently only verifies
behavior for the uppercase snake_case campaign names (EVENT_ALUMNI and
TOUCHED_GRASS) but does not test their lowercase kebab-case alias forms (alumni
and touched-grass). Add additional expect calls within the test block to verify
that classifyBareCampaign produces identical results when called with the alias
forms alumni and touched-grass as it does with EVENT_ALUMNI and TOUCHED_GRASS
respectively. Apply the same additions to the other test block mentioned in the
comment.
In `@src/components/Withdraw/views/PixKeySend.view.tsx`:
- Around line 31-33: The validatePixDestination function contains inline
normalization logic that duplicates the normalization behavior. Replace the
inline normalization logic (the ternary expression checking isPixEmvcoQr and
conditionally trimming or removing spaces) with a call to the normalizePixInput
function to ensure a single source of truth for PIX input normalization across
all entry points.
In `@src/utils/__tests__/withdraw.utils.test.ts`:
- Around line 281-310: The test cases in the it.each array for the 'Pasted
values with whitespace' describe block currently cover phone numbers with spaces
but are missing coverage for phone numbers with dash/hyphen separators. Add a
new test case to the it.each array with a phone number formatted with dashes
(such as +55-11-99999-9999) to ensure the normalizePixInput function properly
handles and normalizes this common phone formatting pattern alongside the
existing whitespace test cases.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 5521cf5a-9229-44f2-8894-88eef758e4c3
⛔ Files ignored due to path filters (3)
public/badges/event_alumni.svgis excluded by!**/*.svgpublic/badges/touched_grass.svgis excluded by!**/*.svgpublic/email/badge-event-alumni.pngis excluded by!**/*.png
📒 Files selected for processing (25)
package.jsonsrc/app/(mobile-ui)/withdraw/manteca/page.tsxsrc/app/[...recipient]/error.tsxsrc/components/Badges/__tests__/badge.utils.test.tssrc/components/Badges/badge.utils.tssrc/components/Create/useCreateLink.tsxsrc/components/Global/QRScannerOverlay/index.tsxsrc/components/Home/EnableAutoBalanceBanner.tsxsrc/components/Home/__tests__/EnableAutoBalanceBanner.test.tsxsrc/components/Invites/InvitesPage.tsxsrc/components/Invites/campaign-maps.test.tssrc/components/Invites/campaign-maps.tssrc/components/Jobs/index.tsxsrc/components/Profile/views/UnlockedRegions.view.tsxsrc/components/Send/link/views/Initial.link.send.view.tsxsrc/components/TransactionDetails/provider-actions/CancelDepositActions.tsxsrc/components/Withdraw/views/PixKeySend.view.tsxsrc/context/authContext.tsxsrc/hooks/useMultiPhaseKycFlow.tssrc/services/sendLinks.tssrc/utils/__mocks__/static-image.tssrc/utils/__tests__/pix.utils.test.tssrc/utils/__tests__/withdraw.utils.test.tssrc/utils/pix.utils.tssrc/utils/withdraw.utils.ts
💤 Files with no reviewable changes (1)
- src/app/[...recipient]/error.tsx
feat(badges): NOT_SO_SHHHH 'Loud Mouth' badge icon + invite mapping
Release — dev → main (2026-06-17, peanut-ui)
21 commits ahead of
main. Coordinated FE+BE release (pairs with peanut-api-ts release).Badges & EVENT_ALUMNI reactivation
feat(badges)add TOUCHED_GRASS + EVENT_ALUMNI badge art + mappingsfix(badges)rename → Seedling, second-person copyfix(invites)EVENT_ALUMNI auto-claims for returning logged-in usersfeat(email)EVENT_ALUMNI hero image for the event-reactivation mailerBrazil PIX sends
feat(withdraw)route Brazil PIX sends through the QR-payment endpointrefactor(pix)extract sharednormalizePixInputto prevent input driftSend-links
fix(send-links)threadpreparationIdso the backend can adopt the prepare intent (pairs with API adopt fix)Reliability / UX hardening
fix(ui)harden 4 dead-button / silent-no-op handlersfix(kyc)don't offer futile retry for terminal unsupported-regionfix(analytics)make persons searchable by username in PostHogfixrelease-audit follow-ups — badge-icon fallback + reachable restart-identity modalNotes
main → devso dev re-aligns with the hotfix commits (standard release-flow hygiene).