Skip to content

feat(referral): attributed share links + invite nudge on every payment type - #2680

Merged
kushagrasarathe merged 11 commits into
devfrom
feat/attributed-share-links
Aug 14, 2026
Merged

feat(referral): attributed share links + invite nudge on every payment type#2680
kushagrasarathe merged 11 commits into
devfrom
feat/attributed-share-links

Conversation

@0xkkonrad

@0xkkonrad 0xkkonrad commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Task

⚠️ Notion MCP unavailable in this session — TASK link to be attached by Konrad (CPO growth-audit items 2+3, referral loop).

What

Two linked referral-loop fixes (CPO growth-audit items 2 + 3):

1. Unattributed pride-shares + the guest dead end. The highest-intent share moments
(profile pill, badge shares, all 18 card win captions) shared bare peanut.me/<u> links —
which credit nobody — or no link at all. A logged-out friend clicking one landed on
"Peanut is invite-only. Go beg your friend for an invite link!" with no signup path that
credits the sharer.

2. The receipt share button barely existed. The receipt referral nudge was gated on a
hand-kept ['send','withdraw','bank_withdraw'] direction list, silently excluding every QR
pay and card spend (both direction: 'qr_payment') — the two highest-frequency transaction
types (~41k QR events, ~3.8k card spends vs ~1.4k send links). The QR success screen said
"Invite friends to earn even more" with no button. Six months of this surface: ~39 sharers.

How

The guest profile becomes the crediting door (retroactively attributes every bare
profile link already in the wild): the guest card now says "{username} has an invite for
you" with a Claim-your-invite CTA that writes the owner's invite code (session cookie, same
contract as InvitesPage's claim — durable cookies caused the #2346 lockout) BEFORE the
pwa-sunset store handoff can swallow the click, then routes to /invite?code=<owner>.

Per-surface link decisions:

  • Badge shares (detail modal + unlock drawer) → attributed /invite?code= (free-form text,
    no label constraint; logged-in recipients get redirected to the profile anyway)
  • ProfileHeader pill → stays peanut.me/<u> (the label IS the value) but via shareableUrl
    (the old non-fallback BASE_URL import rendered undefined/<u> on preview builds), gated
    self-profile-only, desktop copy now toasts
  • Card win captions → gain peanut.me/<handle> in the caption text (matches the pill baked
    into the PNG; works on both native-share-with-file and the twitter intent). The anti-dox
    hideUsername toggle drops the link; rejection-appeal shares deliberately stay link-free
  • Receipt + QR success → attributed invite link as before

Receipt coverage, kind-based: new hasReferralNudge predicate (allow-list of outbound
kinds + inbound-direction block, table-tested over all 14 IntentKinds). The 2-CTA ceiling
that justified excluding QR pay is resolved by demotion, not exclusion: below two stacked
action buttons the nudge keeps the proven filled treatment; at two it becomes an underlined
text row. QR success screen gets the invite row (opens InviteFriendsModal), hidden while a
reward is still claimable, deliberately not gated on isActivated (first QR pay = flag
still false server-side).

Analytics: link_type (invite_code | profile | none) on all referral events, six
new registered REFERRAL_SOURCES, receipt INVITE_LINK_SHARED moved inside the try
(outcome, not intent) + a new REFERRAL_CTA_SHOWN impression leg, variant property to
measure the button→text-link demotion.

i18n: 3 new keys × 4 catalogs (es-AR voseo overrides included).

Review guide

Read in this order: analytics/i18n foundation → transaction-predicates + receipt → guest
door + profile/badges → card share-asset. The plan, constraints dossier and adversarial
review live in mono/inbox/referral-fixes-aug-12/.

Flags for the reviewer

  1. Copy pass wanted on the 3 new keys: en "Claim your invite" / "Invite friends & earn" /
    "Peanut is invite-only — and {username} has an invite for you." (+ es-419/es-AR/pt-BR).
  2. PR fix: open profiles for every real receipt peer #2673 overlap: it also touches transaction-predicates.ts + its test (different
    hunks — hasUserProfile). Mergeable either order; whoever lands second rebases trivially.
  3. Follow-up ticket needed: openStore() does not carry a deferred invite payload, so a
    store bounce from ANY surface loses attribution once pwa-sunset flips to 100%
    (TASK-21044). The door writes the cookie pre-intercept so the code is there to carry.
  4. ONRAMP deliberately excluded from hasReferralNudge ("every payment type" ≠ deposits).
  5. Known pre-existing, untouched: InviteFriendsModal with empty username builds a dead
    /invite?code= QR (Profile passes ?? ''); QRBottomDrawer says "Share your profile"
    over a /pay/<u> URL.

Testing

  • 29 new unit tests (predicate table over all 14 kinds, QR-success branches, guest door incl.
    cookie-on-intercepted-path, ProfileHeader gating, share-caption composition)
  • Full suite 232 suites / 2966 tests green; typecheck 0; prettier clean; knip: no new findings
  • Live QA on the dev server: guest door flow + authenticated receipt surfaces (report in
    mono/inbox/referral-fixes-aug-12/qa/)

Screenshots

Captured on the Nutcracker harness (seeded activated user, 375×667). Assets branch pr-assets-2680 — delete post-merge.

Surface Before → After
QR-pay receipt — nudge as underlined text row (2 stacked CTAs → demoted variant) qr
Withdraw receipt — filled button preserved (proven treatment) offramp
Card-spend receipt — share affordance where there was none (filled variant) card
Badge detail modal badge
Badge share → clipboard text carries /invite?code= shared

| Guest door on peanut.me/ (Vercel preview, 12/12 checks) | door |
| /invite landing after the door click — cookie written, inviter named | landing |

⚠️ QR-success invite row: not reachable live (Manteca sandbox is init-only per the harness TRUST entry); 8 unit tests cover all perk branches.

Design notes / accepted trade-offs

  • bank_claim gets no nudge (yet): the direction has two possible viewers (paying sender on an externally-claimed link, and a Peanut user viewing their own claim-to-bank) and the predicate can't see the viewer role. Missed nudge chosen over wrong-side nudge; role-aware follow-up welcome.
  • The guest door validates before persisting the inviteCode cookie (session scope). An unresolvable code still navigates to /invite, which owns the error screen — nothing is written.
  • Nudge demotion, not exclusion: at two stacked action CTAs the receipt nudge becomes an underlined text row. variant rides the analytics so the demotion's cost is measurable.
  • QR-success invite row is deliberately NOT gated on isActivated — on a first QR pay the flag is still false server-side; the payment that just succeeded is the gate.
  • Card captions keep the profile-URL form (must match the PNG pill baked into the share asset); anti-dox hideUsername drops the link entirely.
  • Legacy (pre-intent) history rows lose the receipt nudge — intended: hasReferralNudge keys on extraDataForDrawer.kind, which pre-intent rows don't carry, so old send/withdraw receipts no longer nudge. All new activity is intent-based; trading the dead tail for a kind-exhaustive predicate is deliberate.
  • Pre-existing, mentioned not fixed: PublicProfile's unreachable allSetTitle ternary; DIRECTION_TO_SIGN says '+' for bank_request_fulfillment while its strategy is SENDER-only; InviteFriendsModal with an empty username builds a dead /invite QR (Profile passes ?? '').

Follow-ups (filed in the readiness report, none blocking)

  1. openStore() must carry buildDeferredPayload() before pwa-sunset flips to 100% (TASK-21044) — store bounces lose attribution on every surface today.
  2. Shared TextLinkRow component — three copies of the underlined text-row pattern now exist.
  3. generateInviteCodeLink could return invitePath — five inline /invite?code= literals repo-wide.
  4. GuestJoinCard extraction so logged-in profile views skip the migration-flag hook.
  5. Docs: 5 customer-facing files go stale with this PR (product/rewards.md "Invitee Experience", product/app.md, help/referrals + help/waitlist-invite-codes ×3 locales, help-center-index) → separate update-content pass, never bundled here.

Review round (13 Aug) — multi-agent review + live QA

Two confirmed defects in the guest door, both fixed in e256c5f3e:

  1. Popup-block: interceptGuestCta() ran after the awaited validate, so the store handoff's window.open sat in a promise continuation — iOS drops the gesture and the CTA silently died. The intercept now fires synchronously inside the click; the cookie write still lands because the handoff opens _blank and this tab lives on (store-hop attribution proper is TASK-21044).
  2. Fallback misattribution: the door persisted/navigated with the raw profile handle while the API's typo-fallback can resolve a waitlisted handle to a different real user (maria23maria). Crediting now requires the resolved inviter to be the profile owner themself; mismatches write nothing and report link_type: 'none'. The API-side source is removed in peanutprotocol/peanut-api-ts#1342 (data-backed ladder hardening — legacy suffix codes still drive ~16% of accepts, so suffix recovery stays; the handle-rewriting rules go).

Design intent, for the record: every public profile is deliberately an invite door — bare peanut.me/<u> links credit their owner retroactively. This is the design (Konrad, Aug 2026), not an accident to be cleaned up later.

Live QA on the harness (guest door end-to-end incl. pt-BR, receipt nudge button/text_link variants, badge share links, share-pill toast): all pass. Known open follow-ups from review, not blocking: guest-card pre-auth flash guard (isFetchingUser on the render gate), REFERRAL_CTA_SHOWN impression for the QR-success row, guest-side title for the invite-gate modal.

Trim round (e0d812ba6): −340 net lines with the same behavior. The receipt nudge and profile pill now route through the shared ShareButton (mobile copies + toasts before the sheet, like every other share surface; a double failure surfaces "Sharing failed" instead of dying silently). Repeated rule-tellings collapsed to one home each; mock-permutation tests are test.each tables; rewardClaimed reverted as out-of-scope.

Review round (14 Aug, Kush): 13abe813f fixes two share-outcome undercounts found in review — ShareButton now reports success when the share sheet is cancelled after the copy already landed (the link is on the clipboard), and the profile-pill impression re-arms when the pill hides (the reused route instance undercounted self → other → self). Both feed the variant/link_type comparison this PR measures. Full suite green.

…rfaces

Foundation for the referral share fixes: registers the new REFERRAL_SOURCES
tags (profile_header, public_profile_guest, badge_detail, badge_unlock,
qr_pay_success, and the pre-existing transaction_receipt wire literal),
documents the link_type property, and adds the guest-profile door +
QR-success invite CTA copy to all four locale catalogs.
The receipt's referral nudge was gated on a hand-kept direction allow-list
(send/withdraw/bank_withdraw) that silently excluded every QR pay and card
spend — both arrive as direction 'qr_payment', and they are the two
highest-frequency transaction types (~41k QR events vs ~1.4k send links).

- new kind-based hasReferralNudge predicate (+ inbound-direction block so
  role-polymorphic kinds never nudge the receiving side), table-tested over
  all 14 IntentKinds
- 2-CTA ceiling resolved by demotion, not exclusion: below two stacked
  action buttons the nudge keeps the filled treatment, at two it becomes an
  underlined text row above the support link
- QR-pay success screen gets the invite row the 'Invite friends to earn
  even more' line always lacked, opening InviteFriendsModal; hidden while
  a reward is still claimable, deliberately not gated on isActivated
- receipt INVITE_LINK_SHARED moves inside the try (outcome, not intent),
  gains link_type/variant, and a REFERRAL_CTA_SHOWN impression leg
The 18 card win captions shipped with no URL at all — a viral share with
zero path back to the product. The sharer's own peanut.me/<handle> now rides
the caption text on both share paths (native share-with-file and the desktop
twitter intent) via one composed string, so mobile and desktop post the same
line.

- composeShareCaption(caption, url?) in share.utils.ts; caption unchanged
  when no url
- anti-dox respected: the hideUsername toggle and an unknown username both
  drop the link (derived per render, not state, so a post-mount toggle flip
  is honored); never links the 'anon' placeholder
- link_type property on all CARD_SHARE_ASSET_SHARED captures, incl. the
  rejection-appeal ones (which deliberately stay link-free)
A logged-out visitor on peanut.me/<username> used to hit a dead end: 'Peanut
is invite-only. Go beg your friend for an invite link!' — with no signup path
that credits the profile owner. Every bare profile link already shared in the
wild (profile pill, badge brags, card captions) leaked its referral.

- guest card now says '{username} has an invite for you' with a Claim-your-
  invite CTA that writes the owner's invite code (session cookie, same
  contract as InvitesPage's claim) BEFORE the pwa-sunset store handoff can
  swallow the click, then routes to /invite?code=<owner>
- ProfileHeader: shareableUrl instead of the no-fallback BASE_URL (preview/
  staging shared 'undefined/<user>'), self-profile-only guard on the share
  pill, toast on the silent desktop clipboard path
- badge shares (detail modal + unlock drawer) switch from the bare profile
  URL to the attributed invite link; detail modal keeps close-on-share
- referral analytics on all four surfaces (REFERRAL_CTA_SHOWN/CLICKED,
  INVITE_LINK_SHARED with source + link_type)
Live QA caught a leftover dead end: a guest tapping Request on the fixed
profile page still got the old 'Beg for an invite' modal — one tap from the
crediting door, back to the uncredited path. Guests now get the same
invitedLine + Claim-your-invite CTA in that modal; the beg flow stays for
logged-in-without-access visitors, whose signup can no longer credit anyone.
@vercel

vercel Bot commented Aug 12, 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 14, 2026 8:30am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds referral-aware sharing and invite CTAs across badge, card, profile, QR payment, and transaction receipt flows. It adds referral-source analytics, invite-code routing, localized invitation text, conditional share links, and focused tests.

Changes

Referral sharing and invite entry points

Layer / File(s) Summary
Referral-aware sharing contracts
src/constants/analytics.consts.ts, src/components/Card/share-asset/*, src/components/Card/BadgeSkipCelebration.tsx, src/components/Card/CardUnlockDrawer.tsx, src/components/Card/CardRejectionScreen.tsx
Sharing now composes captions with optional profile URLs and reports whether a profile link was included. Card surfaces omit links when usernames are hidden or unavailable.
Badge invite-link sharing
src/components/Badges/*, src/components/Badges/__tests__/*
Badge sharing now uses invite-code links, records referral events, and falls back to the application base URL when no username exists.
Public profile invite entry
src/components/Profile/components/PublicProfile.tsx, src/components/Profile/components/ProfileHeader.tsx, src/components/Profile/components/__tests__/*
Guest profile CTAs save invite codes, capture referral events, and route to invite URLs or store handoff. Self-profile sharing uses live-origin URLs and localized clipboard feedback.
QR payment success invite CTA
src/app/(mobile-ui)/qr-pay/page.tsx, src/app/(mobile-ui)/qr-pay/__tests__/*, src/i18n/app/messages/*
Authenticated QR payment success states show an invite CTA when reward conditions allow it and open InviteFriendsModal with the QR payment source.
Transaction receipt referral nudge
src/components/TransactionDetails/*
Receipt referral eligibility uses transaction kind and direction. The receipt tracks one impression per transaction and renders a button or text link based on CTA stacking.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Guest
  participant PublicProfile
  participant PostHog
  participant SessionCookie
  participant InviteRoute
  Guest->>PublicProfile: select join CTA
  PublicProfile->>SessionCookie: save invite code
  PublicProfile->>PostHog: capture referral CTA
  PublicProfile->>InviteRoute: navigate with invite code
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the PR's main changes: attributed share links and referral nudges across payment types.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/attributed-share-links

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.

@0xkkonrad

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

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

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7159.32 → 7151.89 (-7.43)
Findings: +3 net (+71 new, -68 resolved)

🆕 New findings (71)

  • critical complexity — src/app/(mobile-ui)/qr-pay/page.tsx — CC 310, MI 52.77, SLOC 1088
  • critical complexity — src/components/TransactionDetails/TransactionDetailsReceipt.tsx — CC 173, MI 51.73, SLOC 443
  • critical method-complexity — src/components/TransactionDetails/TransactionDetailsReceipt.tsx:99 — CC 115 SLOC 233
  • critical method-complexity — src/app/(mobile-ui)/qr-pay/page.tsx:90 — QRPayPage CC 78 SLOC 409
  • high hotspot — src/app/(mobile-ui)/qr-pay/page.tsx — 101 commits, +1242/-1159 lines since 6 months ago
  • high hotspot — src/components/TransactionDetails/TransactionDetailsReceipt.tsx — 50 commits, +751/-1287 lines since 6 months ago
  • high hotspot — src/components/Badges/badge.utils.ts — 46 commits, +588/-463 lines since 6 months ago
  • high hotspot — src/constants/analytics.consts.ts — 43 commits, +366/-13 lines since 6 months ago
  • high complexity — src/components/Profile/components/PublicProfile.tsx — CC 40, MI 58.93, SLOC 138
  • high complexity — src/components/TransactionDetails/transaction-predicates.ts — CC 40, MI 70.46, SLOC 77
  • high hotspot — src/utils/general.utils.ts — 35 commits, +191/-218 lines since 6 months ago
  • high complexity — src/components/Global/ShareButton/index.tsx — CC 29, MI 49.81, SLOC 86
  • high complexity — src/components/Card/share-asset/ShareAssetActions.tsx — CC 26, MI 48.93, SLOC 114
  • high complexity — src/constants/analytics.consts.ts — CC 1, MI 31.86, SLOC 190
  • medium react-long-component — src/app/(mobile-ui)/qr-pay/page.tsx:90 — QRPayPage is 1587 lines — split it
  • medium high-mdd — src/app/(mobile-ui)/qr-pay/page.tsx:90 — QRPayPage: MDD 465.1 (uses across many lines from declarations)
  • medium high-mdd — src/components/TransactionDetails/TransactionDetailsReceipt.tsx:99 — TransactionDetailsReceipt: MDD 266.5 (uses across many lines from declarations)
  • medium high-mdd — src/components/Profile/components/PublicProfile.tsx:34 — PublicProfile: MDD 129.4 (uses across many lines from declarations)
  • medium high-dlt — src/app/(mobile-ui)/qr-pay/page.tsx:90 — QRPayPage: DLT 124 (calls 124 distinct functions — high context load)
  • medium high-dlt — src/components/TransactionDetails/TransactionDetailsReceipt.tsx:99 — TransactionDetailsReceipt: DLT 85 (calls 85 distinct functions — high context load)

…and 51 more.

✅ Resolved (68)

  • src/app/(mobile-ui)/qr-pay/page.tsx — CC 306, MI 52.56, SLOC 1078
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx — CC 163, MI 51.29, SLOC 428
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx:90 — CC 109 SLOC 219
  • src/app/(mobile-ui)/qr-pay/page.tsx:89 — QRPayPage CC 76 SLOC 403
  • src/app/(mobile-ui)/qr-pay/page.tsx — 98 commits, +1172/-1119 lines since 6 months ago
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx — 47 commits, +563/-1143 lines since 6 months ago
  • src/components/Badges/badge.utils.ts — 44 commits, +544/-445 lines since 6 months ago
  • src/constants/analytics.consts.ts — 42 commits, +351/-12 lines since 6 months ago
  • src/components/TransactionDetails/transaction-predicates.ts — CC 37, MI 71.44, SLOC 69
  • src/utils/general.utils.ts — 33 commits, +181/-208 lines since 6 months ago
  • src/components/Card/share-asset/ShareAssetActions.tsx — CC 23, MI 49.39, SLOC 111
  • src/constants/analytics.consts.ts — CC 1, MI 32.24, SLOC 184
  • src/app/(mobile-ui)/qr-pay/page.tsx:89 — QRPayPage is 1558 lines — split it
  • src/app/(mobile-ui)/qr-pay/page.tsx:89 — QRPayPage: MDD 463.0 (uses across many lines from declarations)
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx:90 — TransactionDetailsReceipt: MDD 259.9 (uses across many lines from declarations)
  • src/app/(mobile-ui)/qr-pay/page.tsx:89 — QRPayPage: DLT 123 (calls 123 distinct functions — high context load)
  • src/components/Profile/components/PublicProfile.tsx:30 — PublicProfile: MDD 120.6 (uses across many lines from declarations)
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx:90 — TransactionDetailsReceipt: DLT 86 (calls 86 distinct functions — high context load)
  • src/components/Card/CardRejectionScreen.tsx:54 — CardRejectionScreen: MDD 59.1 (uses across many lines from declarations)
  • src/components/Card/BadgeSkipCelebration.tsx:58 — BadgeSkipCelebration: MDD 53.6 (uses across many lines from declarations)

…and 48 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/components/Badges/useBadgeShareImpression.ts 0.0 5.3 +5.3
src/components/Profile/components/PublicProfile.tsx 8.9 10.6 +1.7
src/components/Profile/components/ProfileHeader.tsx 7.3 8.8 +1.5
src/components/Card/share-asset/share.utils.ts 2.8 3.6 +0.8
src/components/TransactionDetails/transaction-predicates.ts 7.2 7.9 +0.6

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 3025 ran, 0 failed, 0 skipped, 50.2s

📊 Coverage (unit)

metric %
statements 66.7%
branches 51.8%
functions 56.9%
lines 67.5%
⏱ 10 slowest test cases
time test
3.5s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
0.9s src/utils/__tests__/demo-api.test.ts › isDemoMode() is false when not running under Capacitor
0.3s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.3s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › every sticker stays within canvas at any count
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 › still lets a per-call timeoutMs win over the default
0.3s src/utils/__tests__/auth-token.test.ts › returns the token hydrated from Preferences after authReady
0.3s src/app/actions/__tests__/api-headers-extended.test.ts › should not include apiKey in validateInviteCode body
0.3s src/utils/__tests__/auth-token.test.ts › is none — never guarded — when only the guarded marker is present
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 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: 3

🤖 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/app/`(mobile-ui)/qr-pay/page.tsx:
- Around line 1524-1530: Update the InviteFriendsModal visibility tracking so
each false-to-true opening emits MODAL_SHOWN and REFERRAL_CTA_SHOWN, rather than
suppressing subsequent openings while the component remains mounted. Reset the
hasTrackedShow guard on close or otherwise detect every visibility transition,
preserving the existing initial-open behavior.

In `@src/components/Profile/components/ProfileHeader.tsx`:
- Around line 92-96: Update the share/copy handler in ProfileHeader so it is
asynchronous, awaits navigator.clipboard.writeText(profileUrl), and displays
toast.info with shareButton.linkCopied only after the write resolves. Handle
rejected clipboard writes before showing the success toast, preserving the
existing share behavior.

In `@src/components/TransactionDetails/TransactionDetailsReceipt.tsx`:
- Around line 347-372: Add capture of ANALYTICS_EVENTS.REFERRAL_CTA_CLICKED in
shareInviteLink’s successful share path, using the same source, link_type, and
variant payload as INVITE_LINK_SHARED. Place it before the success path returns
and keep cancellation/error handling unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 05532b7c-1dec-4b59-8235-b3f622084e7e

📥 Commits

Reviewing files that changed from the base of the PR and between ad5b61b and a5bf629.

📒 Files selected for processing (23)
  • src/app/(mobile-ui)/qr-pay/__tests__/qr-pay-states.test.tsx
  • src/app/(mobile-ui)/qr-pay/page.tsx
  • src/components/Badges/BadgeDetailModal.tsx
  • src/components/Badges/BadgeStatusDrawer.tsx
  • src/components/Badges/__tests__/BadgeDetailModal.test.tsx
  • src/components/Card/BadgeSkipCelebration.tsx
  • src/components/Card/CardRejectionScreen.tsx
  • src/components/Card/CardUnlockDrawer.tsx
  • src/components/Card/share-asset/ShareAssetActions.tsx
  • src/components/Card/share-asset/__tests__/share.utils.test.ts
  • src/components/Card/share-asset/share.utils.ts
  • src/components/Profile/components/ProfileHeader.tsx
  • src/components/Profile/components/PublicProfile.tsx
  • src/components/Profile/components/__tests__/ProfileHeader.test.tsx
  • src/components/Profile/components/__tests__/PublicProfile.test.tsx
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx
  • src/components/TransactionDetails/__tests__/transaction-predicates.test.ts
  • src/components/TransactionDetails/transaction-predicates.ts
  • src/constants/analytics.consts.ts
  • src/i18n/app/messages/en.json
  • src/i18n/app/messages/es-419.json
  • src/i18n/app/messages/es-AR.json
  • src/i18n/app/messages/pt-BR.json

Comment thread src/app/(mobile-ui)/qr-pay/page.tsx Outdated
Comment thread src/components/Profile/components/ProfileHeader.tsx Outdated
Comment thread src/components/TransactionDetails/TransactionDetailsReceipt.tsx Outdated
…e gate per fact

Applies the CodeRabbit + /code-review findings:

- bank_claim receipts nudge again: 'Claimed to Bank' is only ever viewed by
  the paying sender (external claimers have no account; Peanut-user claims
  render as 'send'), so it was wrong to treat it as inbound
- clipboard writes can't lie anymore: copyTextToClipboardWithFallback now
  reports success (execCommand's boolean was being dropped), and both the
  profile pill and the receipt toast only after a real write
- receipt share: navigator.share rejection (WebView permission policy) now
  falls back to copy instead of a dead button; REFERRAL_CTA_CLICKED added
  so the receipt funnel matches the badge/modal event pair
- InviteFriendsModal mounts only while open on the QR success screen — its
  per-mount shown-guard was swallowing re-open analytics, and a closed modal
  reconciled on every 50ms hold-to-claim tick
- guest-door hardening: impression keyed by username (route reuses the
  component across profiles), click guarded against the pre-auth flash
  (fast tap wrote another user's invite code into a logged-in session)
- badge shares: link_type reports what the share actually carried (bare
  origin when auth hasn't resolved), and both badge surfaces now emit the
  SHOWN impression leg
- duplication collapsed behind helpers: getBadgeShareLink/captureBadgeShare
  (badge.utils), profileShareUrl (share.utils), rewardClaimable/rewardClaimed
  (qr-pay), showSplitCta/receiptUrl (receipt)
…ookie, whole funnels

Applies the /code-review cross-file tracer's findings:

- bank_claim back out of the nudge: the direction has two possible viewers
  (paying sender when claimed externally, AND a Peanut user viewing their
  own claim-to-bank — fiat-offramp only rewrites to 'send' on the sender
  side), and the direction alone can't tell them apart. Blocked until the
  predicate can see the viewer role; bank_request_fulfillment confirmed
  SENDER-only by its strategy and locked in by test
- the guest door validates before persisting: a profile can exist without
  being a claimable invite, and an unvalidated cookie gets upgraded to 30
  days by the accept retry and then skips the Landing screen — the exact
  PR #2346 lockout the session-scope choice was protecting against
- link_type completed across the funnel: InviteFriendsModal (the
  qr_pay_success surface) now carries it, badge impressions latch once per
  open instead of re-firing when the username hydrates mid-open, and
  profile_header gains the SHOWN leg + outcome-based CLICKED so every
  REFERRAL_SOURCES member reports the same event shape
@0xkkonrad
0xkkonrad marked this pull request as ready for review August 12, 2026 13:59
…e profile owner

The store handoff ran window.open after an awaited validate — iOS drops
the user gesture across a fetch, blocks the popup, and the intercepted
branch skipped navigation, so the CTA died silently on mobile web. The
intercept now fires synchronously first; the cookie write still lands
because the handoff opens _blank and this tab lives on (store-hop
attribution proper is TASK-21044).

Crediting now requires the code to resolve to the profile owner themself:
the API typo-fallback can resolve a waitlisted handle to a different real
user (maria23 -> maria), and that click must not write a cookie nor
report link_type invite_code.
…ests to tables, comments to file norm

The receipt and profile pill hand-rolled the share/copy/toast/capture flow
that ShareButton (imported in the same file) already owns; both route
through it now, which also lets copyTextToClipboardWithFallback revert to
its original contract. Design-history comments and repeated tellings of
the same rule go — the allow-list story lives once, on
REFERRAL_NUDGE_KINDS; the anti-dox rule once, on profileShareUrl.
Mock-permutation tests collapse to test.each tables; suites that tested
untouched code or pinned a Tailwind class are deleted. rewardClaimed
reverted (readability refactor of untouched conditionals — out of scope).

Behavior deltas, all from the ShareButton unification: mobile copies +
toasts before the sheet; the receipt copy toast says 'Link copied'; a
double failure now surfaces 'Sharing failed' instead of dying silently.
Review follow-ups from Kush's pass:

1. ShareButton: a cancelled share sheet after a successful copy now calls
   onSuccess — the link is on the clipboard and the 'copied' toast already
   showed, so consumers capturing INVITE_LINK_SHARED must count it. With no
   copy landed, cancellation stays quiet as before.
2. ProfileHeader: the share-pill impression re-arms when the pill hides —
   the [...recipient] route reuses the component instance, so a mount-scoped
   latch undercounted self → other → self navigations.

Both matter because this PR's variant/link_type comparison is the metric
these events feed.

@kushagrasarathe kushagrasarathe 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.

Reviewed twice — full-diff pass, then a second pass focused on attribution correctness and PII (Aleks's hunch).

Approving with one hard condition: peanut-api-ts#1342 must reach prod before or with this PR. The FE owner-match guard only blocks the silent cookie write — a mismatched code still navigates to /invite?code=<handle>, where the landing page validates against the old BE ladder and handleClaim persists the raw code. With this PR live and #1342 not deployed, every public profile funnels guests into that path. Verify at release time, not merge time.

PII pass: clean. No usernames/PII in any new analytics payload (checked every capture site — source/link_type/variant only), guests see nothing beyond the username already in the URL, the anti-dox rule lives in one place and is derived per-render, and the public-receipt nudge is !isPublic-gated. The BE pair actually closes a leak (ghost1ghost).

Pushed 13abe813f (review follow-ups, full suite green): ShareButton now counts a cancelled sheet after a landed copy as a share outcome, and the profile-pill impression re-arms when the pill hides on the reused route instance. Both undercounts would have biased exactly the variant/link_type comparison this PR exists to measure.

Remaining, non-blocking: task link still to be attached; the copy pass on the 3 new keys is still open per the PR body.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants