feat(badges): detail modal (2x image + tappable drawer) + non-blocking earn toast (TASK-19791)#2292
Conversation
release: dev → main (Red ATM kill · avalanche cleanup · deposit rails · pix/p2p fixes)
…he Bridge EUR flow #2276 made country-click go straight to /add-money/[country]/bank, but that is the Bridge (EUR/SEPA) bank page. Argentina/Brazil bank deposits route through Manteca (Pix/CBU), which the per-country method picker on /add-money/[country] was correctly resolving. Skipping the picker forced every country into the Bridge bank page, so AR/BR users saw a Euro selector. Revert to addMoneyCountryUrl (the method picker). The double-selection #2276 tried to remove is a separate, lower-priority UX nit; a correct fix must be per-country (skip only single-bank EU/NA countries), not a blanket shortcut.
…ression-revert hotfix(add-money): revert bank-step shortcut (AR/BR shown Euro selector)
POST /rain/cards/withdraw/submit is synchronous: it broadcasts AND awaits on-chain confirmation (waitForUserOperationReceipt + confirmIntentByTxHash) and, for a request/charge, settles the charge in the same call. The FE's submitWithdrawal sent it with no timeoutMs, so it used fetchWithSentry's default 10s budget. When confirmation exceeds 10s the FE aborts while the tx still lands and the charge settles: the payer sees "there was an issue with your request" on a payment that SUCCEEDED, retries, and double-sends (observed in prod: one payer -> same recipient x3). #2245 (73f73bc) routed request payments through this submit path for the first time, which is why request-pays only started timing out now. Match the 120s budget the verified-withdrawal path already uses for the same synchronous-confirm reason. Fixes PEANUT-UI-QP5 (rain/cards/withdraw/submit timed out after 10000ms).
…ubmit-timeout fix(rain): 120s timeout on collateral withdraw submit (was 10s default → duplicate sends)
Quadrilha-corn sticker + catalog entry (Arraiá Approved) for the June Festa Junina activation. Resolvable via UTM (utm_campaign=festa-junina) and invite code (festajunina); campaign-maps.test.ts guards that both resolve to a real BADGES entry. Companion API PR adds the award wiring.
It's 2026 — rename FESTA_JUNINA_2025 → FESTA_JUNINA_2026 in the BADGES catalog + campaign maps, and the asset to festa_junina_2026.svg. Invite/UTM tags unchanged (year-agnostic).
Completes the 2026 rename: BADGES key + asset path and the UTM/invite-code maps now reference FESTA_JUNINA_2026 (matching the renamed asset).
Quadrilha-corn sticker + catalog entry (Arraiá Approved) for the June Festa Junina activation. Resolvable via UTM (utm_campaign=festa-junina) and invite code (festajunina); campaign-maps.test.ts guards that both resolve to a real BADGES entry. Companion API PR adds the award wiring.
It's 2026 — rename FESTA_JUNINA_2025 → FESTA_JUNINA_2026 in the BADGES catalog + campaign maps, and the asset to festa_junina_2026.svg. Invite/UTM tags unchanged (year-agnostic).
Completes the 2026 rename: BADGES key + asset path and the UTM/invite-code maps now reference FESTA_JUNINA_2026 (matching the renamed asset).
Add festa_junina_2026 to BARE_VANITY_CAMPAIGNS so a logged-in user landing on a bare UTM (?utm_campaign=festa-junina) or ?campaign link auto-claims the badge — not just new signups. Vanity (no waitlist skip / app access), like touched_grass. Existing it.each tests auto-cover the new entry.
feat(badges): add Festa Junina 2026 event badge
… maps FE side of the Card closed-alpha tester badge: card_alpha.svg sticker asset, BADGES entry (peanut-pink card icon + copy 'You tested the Card while it was still held together with tape and hope.'), and campaign-maps wiring — cardalpha invite code, card-alpha UTM, and card_alpha as a bare-vanity campaign so a plain ?campaign=CARD_ALPHA link is claimable without an invite code. Pairs with the peanut-api-ts CARD_ALPHA registry PR.
Mirror the peanut-api-ts openapi refresh: CARD_ALPHA joins the badge-code union; regenerated api.generated.ts via gen:api so check:api stays green.
feat(badges): add Festa Junina 2026 event badge (prod hotfix)
Signed-off-by: ab <78670703+abalinda@users.noreply.github.com>
feat(badges): Closed Alpha Tester (CARD_ALPHA) — art + invite maps [→ main]
The badge preview rendered at 120px, which looked tiny relative to the modal panel. Double it to 240px so the badge is the clear focal point.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Code-analysis diffPainscore total: 5763.3 → 5787.99 (+24.69) 🆕 New findings (21)
…and 1 more. ✅ Resolved (12)
📈 Painscore deltas (top movers)
|
🧪 UI test report — ✅ all greenSuites
📊 Coverage (unit)
⏱ 10 slowest test cases
|
|
@coderabbitai review |
|
Warning Review limit reached
Next review available in: 20 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
WalkthroughThe badges UI adds shared celebration helpers, a global badge-earned toast, and a dedicated badge detail modal used from the drawer and selected-badge dialog. ChangesBadge celebration and detail UI flow
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested labels
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ Action performedReview finished.
|
The badge card in the 'Badge unlocked!' drawer was not clickable, so there was no way to inspect a freshly-earned badge. Make it open the badge detail modal. Extract that modal into a shared BadgeDetailModal so the Your Badges list and the unlock drawer render the exact same popup (and both get the larger badge image). Tapping closes the drawer first so the modal (z-20) isn't occluded by the drawer overlay (z-50).
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/Badges/BadgeStatusDrawer.tsx`:
- Around line 42-51: Make the interactive badge Card keyboard-accessible in
BadgeStatusDrawer: the current onClick-only behavior on Card opens the detail
modal but the rendered root still lacks role, tabIndex, and keyboard handling.
Update Card to forward accessibility props to its root element, and in
BadgeStatusDrawer wire the clickable card to open on Enter/Space via onKeyDown
while preserving the existing onClick/onClose/setIsDetailOpen flow.
🪄 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: c80c8bcf-7898-4f8a-be07-718730d7e046
📒 Files selected for processing (3)
src/components/Badges/BadgeDetailModal.tsxsrc/components/Badges/BadgeStatusDrawer.tsxsrc/components/Badges/index.tsx
Earning a badge was too subtle — it only surfaced passively in the
profile/activity feed. A fullscreen celebration was the obvious fix but
collides with onboarding: a /shhhhh card signup awards BETA_TESTER +
SHHHHH at once, stacking 2-3 fullscreen takeovers mid-flow ("I just
wanted my card").
Instead, surface it the non-blocking way: when the user lands on /home
with freshly-earned, not-yet-seen badges, fire ONE coalesced toast
("Badge unlocked: X" / "You unlocked N badges") that taps through to the
shared BadgeDetailModal added in this PR (or the badges list for
several). Gated to /home so it never appears mid-onboarding (/setup,
/shhhhh). WAITLIST_SKIP is excluded — it keeps its bespoke card
celebration.
"Surface once, while fresh": a per-user localStorage seen-set + a 7-day
freshness window (same house pattern as the card skip celebration). The
window means an old badge never re-toasts on a new device or on the day
this ships — so no backend column, no migration. Reuses the existing
useToast primitive + getBadgeIcon + BadgeDetailModal.
|
Added the non-intrusive badge-earn toast here (per @Hugo0's direction — bundling with the inspection modal, since the toast taps straight through to the new What the toast does: on The fullscreen-celebration variant is parked in #2297. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/components/Badges/useBadgeEarnToast.ts (1)
27-35: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winKey
seenhydration to the activeuserId.On logout → login in the same client session, the first render for user B still derives
pendingfrom user A'sseenSet. That makes already-seen badges eligible again for one commit before the Line 28-30 rehydration update lands. Gatependinguntil storage has been hydrated for the current user, or store{ userId, seen }together so stale data is never used across accounts.🤖 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/Badges/useBadgeEarnToast.ts` around lines 27 - 35, The pending badge calculation in useBadgeEarnToast is using a stale seen Set from the previous account on the first render after userId changes. Update the hook so pending is only derived after seen has been hydrated for the active userId, or keep userId and seen together in state and recompute from that single source. Make sure the useEffect hydration and the useMemo for pending both key off the same active-user state so loadSeenCodes and pickCelebrationBadges never mix data across accounts.src/components/Badges/__tests__/useBadgeEarnToast.test.ts (1)
16-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a user-switch rehydration test.
useBadgeEarnToastexplicitly reloads the seen-set whenuserIdchanges, but this suite only covers the initial mount path. A regression there would leak one account’s seen badges into the next account on the same device without failing any test.Suggested coverage
describe('useBadgeEarnToast', () => { @@ it('does not resurface badges already in the seen-set', () => { window.localStorage.setItem(celebrationStorageKey('user-a'), JSON.stringify(['SHHHHH'])) setUser('user-a', [{ code: 'SHHHHH', name: 'Shhh', description: null, earnedAt: freshIso() }]) const { result } = renderHook(() => useBadgeEarnToast()) expect(result.current.pending).toEqual([]) }) + + it('rehydrates seen codes when the signed-in user changes', () => { + window.localStorage.setItem(celebrationStorageKey('user-a'), JSON.stringify(['SHHHHH'])) + setUser('user-a', [{ code: 'SHHHHH', name: 'Shhh', description: null, earnedAt: freshIso() }]) + const { result, rerender } = renderHook(() => useBadgeEarnToast()) + expect(result.current.pending).toEqual([]) + + setUser('user-b', [{ code: 'SHHHHH', name: 'Shhh', description: null, earnedAt: freshIso() }]) + rerender() + + expect(result.current.pending.map((b) => b.code)).toEqual(['SHHHHH']) + }) })🤖 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/Badges/__tests__/useBadgeEarnToast.test.ts` around lines 16 - 59, Add a test in useBadgeEarnToast to cover rehydration on userId changes: start with one signed-in user, mark a badge as seen, then switch to a different user and verify the hook reloads state instead of leaking the first user’s seen-set. Extend the existing useBadgeEarnToast test suite by exercising the user-switch path through setUser and renderHook, and assert that pending badges are recalculated per account after the user change.
🤖 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/Badges/BadgeEarnToast.tsx`:
- Around line 41-94: The BadgeEarnToast effect only creates the toast on
HOME_PATH, but it never clears an already-shown badge-earn toast when the route
changes, so it can linger after navigating away. Update BadgeEarnToast to
dismiss TOAST_ID whenever pathname leaves /home, using the existing
toast/dismiss logic in the same component and keeping the current
useEffect/markSeen flow intact. Ensure the cleanup is tied to the pathname
dependency so the home-only guard still applies after navigation.
---
Nitpick comments:
In `@src/components/Badges/__tests__/useBadgeEarnToast.test.ts`:
- Around line 16-59: Add a test in useBadgeEarnToast to cover rehydration on
userId changes: start with one signed-in user, mark a badge as seen, then switch
to a different user and verify the hook reloads state instead of leaking the
first user’s seen-set. Extend the existing useBadgeEarnToast test suite by
exercising the user-switch path through setUser and renderHook, and assert that
pending badges are recalculated per account after the user change.
In `@src/components/Badges/useBadgeEarnToast.ts`:
- Around line 27-35: The pending badge calculation in useBadgeEarnToast is using
a stale seen Set from the previous account on the first render after userId
changes. Update the hook so pending is only derived after seen has been hydrated
for the active userId, or keep userId and seen together in state and recompute
from that single source. Make sure the useEffect hydration and the useMemo for
pending both key off the same active-user state so loadSeenCodes and
pickCelebrationBadges never mix data across accounts.
🪄 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: 8204efac-fae3-40cd-b52a-8ee62bacbaa8
📒 Files selected for processing (7)
src/app/ClientProviders.tsxsrc/components/Badges/BadgeEarnToast.tsxsrc/components/Badges/__tests__/badgeCelebration.utils.test.tssrc/components/Badges/__tests__/useBadgeEarnToast.test.tssrc/components/Badges/badgeCelebration.utils.tssrc/components/Badges/useBadgeEarnToast.tssrc/constants/analytics.consts.ts
On top of the toast core (523b046): adds the missing BadgeEarnToast component test (the /home gate, single-vs-coalesced label, detail-modal-vs-/badges tap), excludes BETA_TESTER (every signup earns it -> noise, not a moment), and updates the detection tests. 23 badge tests green, typecheck clean.
|
Code-review findings on the badge-earn toast (high-effort review of 🔴 1. Cold-start re-fire — 🔴 2. Staggered second badge dropped — 🔴 3. Private-mode infinite nag — 🟡 Minor: toast outlives |
… fixes) Addresses the 3 fire-once bugs from the high-effort code review: (1) cold-start re-fire - seen is now derived synchronously via useMemo, not a useState+useEffect that lagged the async user load by one render; (2) staggered drop - per-batch toast id (not a fixed id) so a 2nd badge earned within the window surfaces instead of being marked-seen-but-never-shown; (3) private-mode nag - in-memory fallback when localStorage writes throw so the toast does not re-fire every /home visit. Also dismiss on nav away from /home; compute getBadgeIcon once. +3 tests; 26 badge tests green, typecheck + eslint clean.
|
Addressed all 3 fire-once findings in
Minors also done: dismiss the toast on nav away from |
… union (festa/cardalpha + psyops/founding) Secret-scan pre-commit hook flagged 0x-hex in dev's supported-chains.ts / withdraw-crypto files — verified public contract addresses + bytes32 config already in dev, no private-key material. False positive.
Summary
Badge UX for the card launch (base:
dev):BadgeDetailModalso the Your Badges list and the unlock drawer render the identical popup./home, freshly-earned badges fire ONE coalesced toast ("Badge unlocked: X" / "You unlocked N badges") that taps through to the sameBadgeDetailModal(or/badgesfor several). FE-only: per-user localStorage seen-set + 7-day freshness window — no backend, no migration. ExcludesWAITLIST_SKIP(own card celebration) andBETA_TESTER(every signup earns it). Gated to/homeso it never stacks mid-onboarding.Files
Badges/BadgeDetailModal.tsx— shared modal (240px image)Badges/index.tsx,Badges/BadgeStatusDrawer.tsx— use the shared modal; drawer badge tappableBadges/BadgeEarnToast.tsx+useBadgeEarnToast.ts+badgeCelebration.utils.ts(+ tests) — the toast, mounted inClientProvidersconstants/analytics.consts.ts— 2 toast analytics eventsRisk
UI-only — no logic/data/contract changes, no migration. Toast is
/home-gated + once-per-device. 23 badge unit tests green; typecheck clean.QA
/badges→ tap any badge → detail modal shows the 240px image./home→ "Badge unlocked: …" toast → tap → detail modal (single) or/badges(several).BETA_TESTER/WAITLIST_SKIP→ no toast.