Skip to content

feat: share badge from the detail modal (per-badge share copy) - #2382

Closed
0xkkonrad wants to merge 6 commits into
devfrom
feat/badge-share-modal
Closed

feat: share badge from the detail modal (per-badge share copy)#2382
0xkkonrad wants to merge 6 commits into
devfrom
feat/badge-share-modal

Conversation

@0xkkonrad

Copy link
Copy Markdown
Contributor

Summary

The badge detail modal's "Got it!" button (a dead dismiss) is now "Share badge" — tapping it drops a funny, badge-specific, first-person line into the native share sheet (Web Share API, with clipboard-copy fallback + toast on desktop). The goal is a share that feels natural enough that people actually post it, turning earned badges into a growth loop. The modal's top-right ✕ still dismisses.

  • New BADGE_SHARE_LINES map + getBadgeShareText(code, displayName, profileUrl) in badge.utils.ts — kept next to BADGES as the FE source of truth. Composes ‹brag›\n\nJoin me on Peanut 👉 ‹the sharer's own profile URL›.
  • Reuses the existing ShareButton (same pattern as the unlock drawer's "Share Achievement"), so behavior matches the rest of the app.
  • Badge code is threaded into BadgeDetailModal from both call sites (Badges list + BadgeStatusDrawer), so the per-badge line works from both entry points.

Copy selection — driven by real usage

Lines were chosen against live prod holder counts (app.user_acknowledgments). Bespoke copy only pays off for badges people actually have, so rarely-earned badges are intentionally omitted from the map (CERTIFIED_YAPPER, TOKEN_NATION_SP_2026, FESTA_JUNINA_2026) — getBadgeShareText returns a generic I just unlocked the "X" badge on Peanut 🥜 fallback for any unlisted code, so they still share cleanly with zero upkeep.

Risks / breaking changes

  • None cross-repo. FE-only; no API/schema change. code is an optional prop (falls back to the generic line if absent).
  • Blast radius = the badge detail modal + the two components that render it. No money/state mutation.

QA

  1. Profile → Your Badges → tap any badge → modal shows Share badge.
  2. Mobile: opens the native share sheet pre-filled with the badge's line + your profile URL. Desktop: copies to clipboard, toasts "Text copied", modal dismisses on success.
  3. Also reachable via the badge-unlock drawer → tap the badge card → same modal.
  4. A badge with no mapped line (e.g. an omitted one) shares the generic fallback.

Unit tests: getBadgeShareText (mapped code, generic fallback, undefined code) in badge.utils.test.ts.

0xkkonrad added 2 commits July 7, 2026 20:49
The badge detail modal's dismiss-only CTA now shares the badge: a funny,
first-person, per-badge line is dropped into the native share sheet (Web
Share API, clipboard fallback) via the existing ShareButton. Copy lives in
a single BADGE_SHARE_LINES map so it stays the FE source of truth alongside
BADGES; getBadgeShareText() composes brag + join tagline + the sharer's own
profile URL (the growth loop). The modal's top-right close still dismisses.
Default lines are first-pass picks pending copy review.
Replace the first-pass share lines with Konrad's selected copy (chosen against
real prod holder counts). Three rarely-earned badges — CERTIFIED_YAPPER,
TOKEN_NATION_SP_2026, FESTA_JUNINA_2026 — are intentionally dropped from the map
and use getBadgeShareText's generic fallback; not worth bespoke copy. Test made
copy-agnostic so editing a line no longer breaks it.
@vercel

vercel Bot commented Jul 7, 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 Aug 5, 2026 11:00am

Request Review

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7008.7 → 7010.18 (+1.48)
Findings: 0 net (+6 new, -6 resolved)

🆕 New findings (6)

  • high hotspot — src/components/Badges/badge.utils.ts — 39 commits, +480/-157 lines since 6 months ago
  • high complexity — src/components/Badges/badge.utils.ts — CC 13, MI 45.54, SLOC 202
  • medium high-mdd — src/components/Badges/BadgeEarnToast.tsx:34 — BadgeEarnToast: MDD 32.5 (uses across many lines from declarations)
  • medium high-mdd — src/components/Badges/index.tsx:20 — Badges: MDD 26.5 (uses across many lines from declarations)
  • medium complexity — src/components/Badges/BadgeEarnToast.tsx — CC 16, MI 58.81, SLOC 80
  • low high-mdd — src/components/Badges/BadgeEarnToast.tsx:45 — : MDD 17.8 (uses across many lines from declarations)

✅ Resolved (6)

  • src/components/Badges/badge.utils.ts — 37 commits, +375/-116 lines since 6 months ago
  • src/components/Badges/badge.utils.ts — CC 10, MI 46.25, SLOC 157
  • src/components/Badges/BadgeEarnToast.tsx:34 — BadgeEarnToast: MDD 32.2 (uses across many lines from declarations)
  • src/components/Badges/index.tsx:20 — Badges: MDD 25.6 (uses across many lines from declarations)
  • src/components/Badges/BadgeEarnToast.tsx — CC 16, MI 59.06, SLOC 79
  • src/components/Badges/BadgeEarnToast.tsx:45 — : MDD 17.3 (uses across many lines from declarations)

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: cf6b0c13-964c-4eae-8a5d-1e365a75cf54

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a getBadgeShareText utility that generates badge-specific first-person share copy with a fallback for unknown codes, and updates BadgeDetailModal to be a client component that surfaces a ShareButton (instead of a dismiss CTA) using this text, propagating a new code prop from BadgeStatusDrawer and index.tsx.

Changes

Badge share feature

Layer / File(s) Summary
Badge share text utility and tests
src/components/Badges/badge.utils.ts, src/components/Badges/__tests__/badge.utils.test.ts
Adds BADGE_SHARE_LINES map and exported getBadgeShareText(code, displayName, profileUrl) function with generic fallback and profile link; tests cover known, unknown, and undefined codes.
BadgeDetailModal share-first UX
src/components/Badges/BadgeDetailModal.tsx
Converts to 'use client', adds optional code prop, derives profileUrl from useUserStore username and BASE_URL, and replaces the "Got it!" CTA with ShareButton content that closes the modal on successful share.
Badge code propagation to modal callers
src/components/Badges/index.tsx, src/components/Badges/BadgeStatusDrawer.tsx
Adds code field to BadgeView, maps b.code into badge objects, and passes code to BadgeDetailModal from both call sites.

Estimated code review effort: 2 (Simple) | ~15 minutes

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: Hugo0, abalinda

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: sharing badges from the detail modal with per-badge copy.
Description check ✅ Passed The description matches the changeset and explains the new share flow, helper, and badge-code wiring.
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.

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.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 2503 ran, 0 failed, 0 skipped, 41.8s

📊 Coverage (unit)

metric %
statements 62.5%
branches 45.7%
functions 51.6%
lines 63.0%
⏱ 10 slowest test cases
time test
3.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.1s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.4s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
0.4s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.4s src/utils/__tests__/auth-token.test.ts › is none — never guarded — when only the guarded marker is present
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.3s src/utils/__tests__/sentry.utils.test.ts › still lets a per-call timeoutMs win over the default
0.3s src/utils/__tests__/auth-token.test.ts › authReady does not park — hydrates the plain token without an unlock
0.3s src/utils/__tests__/auth-token.test.ts › ignores the guarded marker and falls back to the plain token
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@coderabbitai coderabbitai Bot added the enhancement New feature or request label Jul 7, 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.

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

277-278: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Aliased badge lines are duplicated literals, not a shared reference.

BUG_WHISPERER/SUPPORT_SURVIVOR and CARD_PIONEER/FOUNDING_PIONEER repeat the exact same string. Since the comment states these "share a line" by design, referencing a single constant would guarantee they stay in sync if the copy is ever edited.

♻️ Example using shared constants
+const BUG_WHISPERER_LINE = 'I found a real bug in Peanut, reported it, and stuck around. Someone owes me a beer 🐛🍺'
+const FOUNDING_PIONEER_LINE = 'I was building Peanut before it had a launch. Founding Pioneer 🛠️'
+
 const BADGE_SHARE_LINES: Record<string, string> = {
     ...
-    BUG_WHISPERER: 'I found a real bug in Peanut, reported it, and stuck around. Someone owes me a beer 🐛🍺',
-    SUPPORT_SURVIVOR: 'I found a real bug in Peanut, reported it, and stuck around. Someone owes me a beer 🐛🍺',
+    BUG_WHISPERER: BUG_WHISPERER_LINE,
+    SUPPORT_SURVIVOR: BUG_WHISPERER_LINE,
     ...
-    CARD_PIONEER: 'I was building Peanut before it had a launch. Founding Pioneer 🛠️',
-    FOUNDING_PIONEER: 'I was building Peanut before it had a launch. Founding Pioneer 🛠️',
+    CARD_PIONEER: FOUNDING_PIONEER_LINE,
+    FOUNDING_PIONEER: FOUNDING_PIONEER_LINE,
}

Also applies to: 301-302

🤖 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/badge.utils.ts` around lines 277 - 278, The aliased
badge entries in badge.utils.ts are duplicated string literals instead of
sharing a single source of truth. Update the badge mapping so BUG_WHISPERER and
SUPPORT_SURVIVOR, as well as CARD_PIONEER and FOUNDING_PIONEER, reference shared
constants or a shared value defined once in the same module, keeping the badge
labels in sync if they change later.
src/components/Badges/BadgeStatusDrawer.tsx (1)

77-88: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Drawer's own Share button bypasses the new getBadgeShareText helper.

This inline generateText still hardcodes a generic message, while the same badge's BadgeDetailModal (opened from the Card below) now uses getBadgeShareText for badge-specific copy. Users get inconsistent share text for the same badge depending on which button they tap.

♻️ Suggested consolidation
                         <div className="pb-4">
                             <ShareButton
                                 title=""
-                                generateText={() =>
-                                    Promise.resolve(
-                                        `I earned ${displayName} badge on Peanut!\n\nJoin Peanut now and start earning points, unlocking achievements and moving money worldwide\n\n${profileLink}`
-                                    )
-                                }
+                                generateText={() => Promise.resolve(getBadgeShareText(badge.code, displayName, profileLink))}
                             >
                                 Share Achievement
                             </ShareButton>
                         </div>
🤖 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/BadgeStatusDrawer.tsx` around lines 77 - 88, The
ShareButton inside BadgeStatusDrawer is still building its own generic share
message instead of using the shared getBadgeShareText helper, causing
inconsistent copy with BadgeDetailModal. Update the ShareButton’s generateText
to call getBadgeShareText for the current badge so both share entry points use
the same badge-specific text. Keep the change localized to BadgeStatusDrawer and
reuse the existing badge share helper rather than hardcoding the template
inline.
🤖 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.

Nitpick comments:
In `@src/components/Badges/badge.utils.ts`:
- Around line 277-278: The aliased badge entries in badge.utils.ts are
duplicated string literals instead of sharing a single source of truth. Update
the badge mapping so BUG_WHISPERER and SUPPORT_SURVIVOR, as well as CARD_PIONEER
and FOUNDING_PIONEER, reference shared constants or a shared value defined once
in the same module, keeping the badge labels in sync if they change later.

In `@src/components/Badges/BadgeStatusDrawer.tsx`:
- Around line 77-88: The ShareButton inside BadgeStatusDrawer is still building
its own generic share message instead of using the shared getBadgeShareText
helper, causing inconsistent copy with BadgeDetailModal. Update the
ShareButton’s generateText to call getBadgeShareText for the current badge so
both share entry points use the same badge-specific text. Keep the change
localized to BadgeStatusDrawer and reuse the existing badge share helper rather
than hardcoding the template inline.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e65f9ee6-177c-431a-97a4-732082a29dca

📥 Commits

Reviewing files that changed from the base of the PR and between ec8bf47 and 16912f7.

📒 Files selected for processing (5)
  • src/components/Badges/BadgeDetailModal.tsx
  • src/components/Badges/BadgeStatusDrawer.tsx
  • src/components/Badges/__tests__/badge.utils.test.ts
  • src/components/Badges/badge.utils.ts
  • src/components/Badges/index.tsx

0xkkonrad added 2 commits July 7, 2026 21:07
CodeRabbit caught that the badge-unlock drawer's own 'Share Achievement'
button still hardcoded the generic message while the detail modal (opened
from the same drawer) now shares badge-specific copy — the same badge gave
two different share texts depending on which button was tapped. Route the
drawer through getBadgeShareText so both surfaces are consistent.
/code-review found the third BadgeDetailModal render site — the badge-earn
toast on /home (the unlock moment, the most shareable surface) — wasn't
threading the badge code, so tapping Share there always got the generic
fallback instead of the badge-specific line. Add code to ModalBadge and
forward it, matching the badges-list and unlock-drawer call sites.
@0xkkonrad

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

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

@0xkkonrad

Copy link
Copy Markdown
Contributor Author

Thanks @coderabbitai — both nitpicks triaged:

  • BadgeStatusDrawer share consistency (77-88): fixed in b7c742f. Good catch — this PR introduced the divergence (the drawer's "Share Achievement" button kept the generic copy while the detail modal moved to getBadgeShareText). Routed the drawer through getBadgeShareText too, so the same badge now shares the same text from both surfaces.
  • Aliased duplicated literals in badge.utils.ts (277-278, 301-302): declining (as CodeRabbit rated it, 🔵 trivial / 💤 low value). Keeping the map a flat, greppable literal that matches the BADGES map directly above it — it's only two alias pairs, both documented in the header comment, and extracting constants adds indirection for negligible sync risk.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@0xkkonrad Thanks for the quick follow-up and the clear rationale on both points.

  • Good catch on the drawer/modal divergence — glad routing both through getBadgeShareText closed that gap. Consistent badge copy across surfaces now.
  • Makes sense on keeping BADGE_SHARE_LINES flat and literal — matching the BADGES map style above it is a reasonable tradeoff for just two documented alias pairs. Noted for future reviews.

✏️ Learnings added
Learnt from: 0xkkonrad
URL: https://github.com/peanutprotocol/peanut-ui/pull/2382

Timestamp: 2026-07-07T21:23:58.500Z
Learning: In src/components/Badges/badge.utils.ts, the `BADGE_SHARE_LINES` map (and similar maps like `BADGES`) is intentionally kept as a flat, greppable object literal with duplicated string keys for alias pairs (e.g., CARD_PIONEER/FOUNDING_PIONEER, BUG_WHISPERER/SUPPORT_SURVIVOR) rather than using shared constants to alias duplicate values. This is a deliberate style choice to match the `BADGES` map convention and avoid indirection for a small number of documented aliases; do not flag this as duplication needing refactoring.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@0xkkonrad
0xkkonrad marked this pull request as ready for review July 7, 2026 21:28
Temporarily align the two conflict sites with dev so GitHub can create a verified server-side base merge without rewriting PR history.
Restore the original PR tree after GitHub rejected its server-side dev merge because current dev contains historical unsigned commits.
@0xkkonrad

Copy link
Copy Markdown
Contributor Author

Fresh replacement: #2609. This branch could not inherit current dev: the organization-wide required-signatures rule rejects historical unsigned commits already present on dev, and non-fast-forward updates are also prohibited. I restored this branch to its original tree and rebuilt the reviewed change as one signed commit directly on current dev; QA and CI are continuing on #2609.

@abalinda

abalinda commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Branch-update status (saves you the next blocked attempt): repo ruleset 6457257 enforces required_signatures on all branches, and every base-merge path introduces dev's pre-ruleset unsigned history commits to this ref — so a local merge push (54 flagged), REST /merges, and the PR update-branch flow are all rejected. Signing our own commits doesn't help; the historical ones can't be re-signed. That's what blocked both attempts this morning.

The completed reconciliation is on feat/badge-share-i18n (new refs pass the ruleset, so it pushed clean). It contains this PR + origin/dev merged (incl. both temp/restore commits folded in) + the share copy moved into the app i18n catalogs:

  • dev's i18n migration had already translated the share flow (badges.shareText), so the hardcoded EN map here would have silently un-translated sharing for es/pt users.
  • Brags now live under badges.share.lines.<CODE> — en source, es-419/pt-BR full key parity, es-AR voseo deltas — with t.has() falling back to a generic line for unmapped codes (rare badges stay zero-upkeep). badges.shareText removed; modal CTA label is badges.share.cta; getBadgeShareText(t, …) takes the badges translator.
  • Verified: typecheck clean, full jest 190 suites / 2501 tests green (incl. catalog parity + ICU compile), prettier clean, new typo-guard test (every share line maps to a real BADGES code).

Two working endgames:

  1. Keep this PR: someone with ruleset bypass runs git push origin feat/badge-share-i18n:feat/badge-share-modal — plain fast-forward, no force, history preserved.
  2. Supersede: open a fresh PR from feat/badge-share-i18n and close this one.

Aleks has the details; es/pt lines are transcreations awaiting native review in commit 6a56020.

@0xkkonrad

Copy link
Copy Markdown
Contributor Author

Superseded by #2609. The replacement is rebuilt directly on the live dev tip, is 0 commits behind, has verified signatures, is mergeable/clean, and all CI plus the manual CodeRabbit review are green. The old branch remains preserved; closing this PR to keep review on the current implementation.

@0xkkonrad 0xkkonrad closed this Aug 5, 2026
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.

2 participants