Skip to content

feat(badges): detail modal (2x image + tappable drawer) + non-blocking earn toast (TASK-19791)#2292

Merged
Hugo0 merged 24 commits into
devfrom
hotfix/badge-preview-2x
Jun 29, 2026
Merged

feat(badges): detail modal (2x image + tappable drawer) + non-blocking earn toast (TASK-19791)#2292
Hugo0 merged 24 commits into
devfrom
hotfix/badge-preview-2x

Conversation

@0xkkonrad

@0xkkonrad 0xkkonrad commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Badge UX for the card launch (base: dev):

  1. Bigger badge image in the detail modal — was 120px, doubled to 240px so the badge is the focal point.
  2. The "Badge unlocked!" drawer badge is tappable — opens the badge detail modal. Extracted into a shared BadgeDetailModal so the Your Badges list and the unlock drawer render the identical popup.
  3. Non-blocking badge-earn toast (TASK-19791) — replaces the parked fullscreen celebration (feat(badges): fullscreen badge-receipt celebration (TASK-19791) [PARKED] #2297). On /home, freshly-earned badges fire ONE coalesced toast ("Badge unlocked: X" / "You unlocked N badges") that taps through to the same BadgeDetailModal (or /badges for several). FE-only: per-user localStorage seen-set + 7-day freshness window — no backend, no migration. Excludes WAITLIST_SKIP (own card celebration) and BETA_TESTER (every signup earns it). Gated to /home so 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 tappable
  • Badges/BadgeEarnToast.tsx + useBadgeEarnToast.ts + badgeCelebration.utils.ts (+ tests) — the toast, mounted in ClientProviders
  • constants/analytics.consts.ts — 2 toast analytics events

Risk

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.
  • Earn a badge → land on /home → "Badge unlocked: …" toast → tap → detail modal (single) or /badges (several).
  • A user who only earns BETA_TESTER / WAITLIST_SKIP → no toast.
  • Best verified on the Vercel preview.

jjramirezn and others added 19 commits June 24, 2026 20:12
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.
@vercel

vercel Bot commented Jun 26, 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 Jun 29, 2026 7:49am

Request Review

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 5763.3 → 5787.99 (+24.69)
Findings: +9 net (+21 new, -12 resolved)

🆕 New findings (21)

  • critical complexity — src/services/rain.ts — CC 54, MI 60.66, SLOC 236
  • high complexity — src/utils/native-routes.ts — CC 40, MI 64.25, SLOC 80
  • high hotspot — src/components/Badges/badge.utils.ts — 30 commits, +317/-102 lines since 6 months ago
  • high complexity — src/components/Badges/badge.utils.ts — CC 10, MI 47.1, SLOC 146
  • high complexity — src/constants/analytics.consts.ts — CC 1, MI 34.8, SLOC 148
  • medium high-mdd — src/components/Badges/BadgeEarnToast.tsx:33 — BadgeEarnToast: MDD 29.7 (uses across many lines from declarations)
  • medium high-mdd — src/components/Badges/BadgeStatusDrawer.tsx:26 — BadgeStatusDrawer: MDD 27.9 (uses across many lines from declarations)
  • medium high-mdd — src/app/(mobile-ui)/add-money/page.tsx:22 — AddMoneyPage: MDD 25.3 (uses across many lines from declarations)
  • medium complexity — src/components/Badges/badgeCelebration.utils.ts — CC 18, MI 67.28, SLOC 65
  • medium complexity — src/components/Badges/BadgeEarnToast.tsx — CC 16, MI 60.07, SLOC 72
  • medium method-complexity — src/utils/native-routes.ts:64 — rewriteMethodPath CC 16 SLOC 31
  • medium complexity — src/components/Invites/campaign-maps.ts — CC 6, MI 56.3, SLOC 32
  • medium nextjs-missing-use-client — src/components/Badges/BadgeStatusDrawer.tsx:1 — Hooks used without use client directive
  • low high-dlt — src/components/Badges/BadgeEarnToast.tsx:33 — BadgeEarnToast: DLT 18 (calls 18 distinct functions — high context load)
  • low high-mdd — src/components/Badges/BadgeEarnToast.tsx:43 — : MDD 17.3 (uses across many lines from declarations)
  • low high-mdd — src/components/Badges/useBadgeEarnToast.ts:20 — useBadgeEarnToast: MDD 15.6 (uses across many lines from declarations)
  • low high-mdd — src/utils/native-routes.ts:64 — rewriteMethodPath: MDD 11.7 (uses across many lines from declarations)
  • low missing-return-type — src/app/ClientProviders.tsx:32 — ClientProviders: exported fn missing return type annotation
  • low missing-return-type — src/components/Badges/BadgeDetailModal.tsx:16 — BadgeDetailModal: exported fn missing return type annotation
  • low missing-return-type — src/components/Badges/BadgeEarnToast.tsx:33 — BadgeEarnToast: exported fn missing return type annotation

…and 1 more.

✅ Resolved (12)

  • src/services/rain.ts — CC 54, MI 60.71, SLOC 235
  • src/utils/native-routes.ts — CC 42, MI 64.55, SLOC 84
  • src/components/Badges/badge.utils.ts — CC 10, MI 47.75, SLOC 138
  • src/constants/analytics.consts.ts — CC 1, MI 34.96, SLOC 146
  • src/app/(mobile-ui)/add-money/page.tsx:22 — AddMoneyPage: MDD 26.5 (uses across many lines from declarations)
  • src/components/Badges/badge.utils.ts — 25 commits, +298/-98 lines since 6 months ago
  • src/utils/native-routes.ts:73 — rewriteMethodPath CC 16 SLOC 31
  • src/components/Invites/campaign-maps.ts — CC 6, MI 57.81, SLOC 28
  • src/components/Badges/BadgeStatusDrawer.tsx:24 — BadgeStatusDrawer: MDD 18.5 (uses across many lines from declarations)
  • src/utils/native-routes.ts:73 — rewriteMethodPath: MDD 11.7 (uses across many lines from declarations)
  • src/app/ClientProviders.tsx:31 — ClientProviders: exported fn missing return type annotation
  • src/components/Badges/BadgeStatusDrawer.tsx:24 — BadgeStatusDrawer: exported fn missing return type annotation

📈 Painscore deltas (top movers)

File Before After Δ
src/components/Badges/BadgeEarnToast.tsx 0.0 7.7 +7.7
src/components/Badges/badgeCelebration.utils.ts 0.0 5.4 +5.4
src/components/Badges/useBadgeEarnToast.ts 0.0 5.3 +5.3
src/components/Badges/BadgeDetailModal.tsx 0.0 2.8 +2.8
src/components/Invites/campaign-maps.ts 6.0 7.0 +1.0
src/components/Badges/badge.utils.ts 10.2 11.0 +0.8
src/components/Badges/BadgeStatusDrawer.tsx 6.0 6.5 +0.5

@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 1594 ran, 0 failed, 0 skipped, 23.0s

📊 Coverage (unit)

metric %
statements 53.9%
branches 36.6%
functions 41.7%
lines 53.8%
⏱ 10 slowest test cases
time test
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in updateUserById body
0.3s src/app/actions/__tests__/api-headers.test.ts › should include Content-Type in updateUserById
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every stamp stays within canvas at any count
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid 9-digit US account
0.1s src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx › Manteca PIX form ready shows merchant card + amount input + pay button
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle too long for US account
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle invalid ETH address (too short)
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ETH address in lowercase
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid ETH address with surrounding spaces
0.1s src/components/Global/GeneralRecipientInput/__tests__/GeneralRecipientInput.test.tsx › should handle valid US account with spaces
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@0xkkonrad

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Hugo0, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d6c3124a-469d-4152-9cd7-6d884eb4b00e

📥 Commits

Reviewing files that changed from the base of the PR and between 523b046 and 2704bc6.

📒 Files selected for processing (8)
  • src/components/Badges/BadgeEarnToast.tsx
  • src/components/Badges/__tests__/BadgeEarnToast.test.tsx
  • src/components/Badges/__tests__/badgeCelebration.utils.test.ts
  • src/components/Badges/__tests__/useBadgeEarnToast.test.ts
  • src/components/Badges/badge.utils.ts
  • src/components/Badges/badgeCelebration.utils.ts
  • src/components/Badges/useBadgeEarnToast.ts
  • src/components/Invites/campaign-maps.ts

Walkthrough

The 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.

Changes

Badge celebration and detail UI flow

Layer / File(s) Summary
Celebration helpers and hook
src/components/Badges/badgeCelebration.utils.ts, src/components/Badges/useBadgeEarnToast.ts
Badge freshness, seen-code persistence, pending badge selection, and per-user hook state are added together.
Celebration helper tests
src/components/Badges/__tests__/badgeCelebration.utils.test.ts, src/components/Badges/__tests__/useBadgeEarnToast.test.ts
Tests cover freshness rules, badge filtering, localStorage persistence, and the hook’s pending/markSeen behavior.
Global badge toast delivery
src/components/Badges/BadgeEarnToast.tsx, src/app/ClientProviders.tsx, src/constants/analytics.consts.ts
The globally mounted toast component shows earned badges on /home, records shown/tapped analytics, and is wired into client providers.
Badge detail modal updates
src/components/Badges/BadgeDetailModal.tsx, src/components/Badges/BadgeStatusDrawer.tsx, src/components/Badges/index.tsx
Badge detail views now use BadgeDetailModal, with the drawer and selected-badge dialog passing title, description, and logo into it.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

enhancement

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly reflects the main UI changes: shared badge detail modal, tappable drawer badge, and non-blocking earn toast.
Description check ✅ Passed The description accurately summarizes the badge modal, tappable drawer, and earn-toast work described in the changeset.

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.

❤️ Share

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

@coderabbitai

coderabbitai Bot commented Jun 26, 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.

@coderabbitai coderabbitai Bot added the enhancement New feature or request label Jun 26, 2026
@0xkkonrad 0xkkonrad marked this pull request as ready for review June 26, 2026 20:14
@0xkkonrad 0xkkonrad changed the title fix(badges): 2x the badge image in the detail modal fix(badges): 2x the badge image in the detail modal [→ main] Jun 27, 2026
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).
@0xkkonrad 0xkkonrad changed the title fix(badges): 2x the badge image in the detail modal [→ main] fix(badges): bigger detail-modal image + tap unlock-drawer badge to inspect [→ main] Jun 27, 2026

@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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 23faa0b and 57875be.

📒 Files selected for processing (3)
  • src/components/Badges/BadgeDetailModal.tsx
  • src/components/Badges/BadgeStatusDrawer.tsx
  • src/components/Badges/index.tsx

Comment thread src/components/Badges/BadgeStatusDrawer.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.
@Hugo0

Hugo0 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

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 BadgeDetailModal). Also retargeted this PR maindev so it rides the card-launch release.

What the toast does: on /home, if the signed-in user has freshly-earned (≤7d), unseen, visible badges → fire one coalesced toast ("Badge unlocked: X" / "You unlocked N badges") → tap opens BadgeDetailModal (1 badge) or /badges (several). Gated to /home so it never interrupts /setup or /shhhhh onboarding. WAITLIST_SKIP excluded (keeps its bespoke card celebration). Persistence: per-user localStorage seen-set + a 7-day freshness window — no backend, no migration. Reuses the existing useToast + getBadgeIcon + BadgeDetailModal. 16 unit tests, typecheck clean.

The fullscreen-celebration variant is parked in #2297.

@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: 1

🧹 Nitpick comments (2)
src/components/Badges/useBadgeEarnToast.ts (1)

27-35: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Key seen hydration to the active userId.

On logout → login in the same client session, the first render for user B still derives pending from user A's seen Set. That makes already-seen badges eligible again for one commit before the Line 28-30 rehydration update lands. Gate pending until 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 win

Add a user-switch rehydration test.

useBadgeEarnToast explicitly reloads the seen-set when userId changes, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 57875be and 523b046.

📒 Files selected for processing (7)
  • src/app/ClientProviders.tsx
  • src/components/Badges/BadgeEarnToast.tsx
  • src/components/Badges/__tests__/badgeCelebration.utils.test.ts
  • src/components/Badges/__tests__/useBadgeEarnToast.test.ts
  • src/components/Badges/badgeCelebration.utils.ts
  • src/components/Badges/useBadgeEarnToast.ts
  • src/constants/analytics.consts.ts

Comment thread src/components/Badges/BadgeEarnToast.tsx
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.
@Hugo0

Hugo0 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Code-review findings on the badge-earn toast (high-effort review of 523b04683). 3 confirmed bugs that defeat the "fire once" promise — please fix before merging to dev.

🔴 1. Cold-start re-fireuseBadgeEarnToast.ts:23-32
seen is useState(empty) + a useEffect re-hydrate, but user loads async — on the render where userId first becomes defined, pending is computed against the still-empty seen-set, so the toast re-fires for an already-seen badge on every cold start until it ages out (7d).
→ Derive seen synchronously: useMemo(() => loadSeenCodes(userId), [userId, bump]), where markSeen persists then bumps. Removes the one-render lag.

🔴 2. Staggered second badge droppedBadgeEarnToast.tsx:97
markSeen() runs unconditionally after toast(), but toast() is a no-op when a toast with the same fixed TOAST_ID is already on screen (Toast.tsx de-dupe). A second badge earned within the 6s window is marked seen but never shown — the staggered-webhook case the feature targets.
→ Only markSeen the badges actually rendered (dismiss+refire with the combined set, or gate markSeen on the toast being newly created, not de-duped).

🔴 3. Private-mode infinite nagbadgeCelebration.utils.ts:52-53
persistSeenCodes swallows localStorage write failures → in Safari/iOS private mode the write never lands → seen re-hydrates empty every launch → the toast re-fires on every /home visit for 7 days.
→ If localStorage can't persist, suppress the toast (or hold seen in a module-level in-memory set). Degrade, don't nag.

🟡 Minor: toast outlives /home on nav (add return () => dismiss(TOAST_ID)); markSeen fires before the toast is perceived (modal-over-it); isFresh silently drops badges with missing earnedAt; badge display/description derivation duplicates BadgesRow (divergent null-description handling — extract a shared helper); getBadgeIcon computed twice. Cross-device re-surface is the accepted FE-only tradeoff, not a bug.

@Hugo0 Hugo0 changed the title fix(badges): bigger detail-modal image + tap unlock-drawer badge to inspect [→ main] feat(badges): detail modal (2x image + tappable drawer) + non-blocking earn toast (TASK-19791) Jun 29, 2026
@notion-workspace

Copy link
Copy Markdown

… 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.
@Hugo0

Hugo0 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Addressed all 3 fire-once findings in 267caf003:

  1. Cold-start re-fireseen is now derived synchronously via useMemo(loadSeenCodes, [userId, bump]) — no useState+useEffect re-hydrate lag behind the async user load.
  2. Staggered second badge dropped → per-batch toast id (badge-earn:<codes>) instead of the fixed id, so a second badge earned within the window surfaces instead of being marked-seen-but-never-shown.
  3. Private-mode infinite nag → in-memory fallback in badgeCelebration.utils when a localStorage write throws, so seen survives the session and the toast doesn't re-fire every /home visit.

Minors also done: dismiss the toast on nav away from /home; getBadgeIcon computed once. +3 tests (cold-start, private-mode write-failure, nav cleanup) → 26 badge tests green, typecheck + eslint clean. isFresh-drops-undated and the cross-device re-surface are left as the accepted FE-only tradeoffs.

… 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.
@Hugo0 Hugo0 merged commit a45f687 into dev Jun 29, 2026
16 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants