Skip to content

feat(invites): emit invited_by in invite links, keep code as an alias - #2828

Open
0xkkonrad wants to merge 3 commits into
devfrom
feat/invited-by-param-alias
Open

feat(invites): emit invited_by in invite links, keep code as an alias#2828
0xkkonrad wants to merge 3 commits into
devfrom
feat/invited-by-param-alias

Conversation

@0xkkonrad

Copy link
Copy Markdown
Contributor

What

Invite links now emit /invite?invited_by=<username> instead of /invite?code=<username>. The code param stays supported forever as a read-side alias — every previously shared link, QR code, X post, and campaign URL keeps its exact behavior.

Why

The value in the param is the inviter's username, so the param should say so. code stays because years of shared links carry it, and because the code slot also transports non-username values (offramp, legacy INVITESYOU suffixes, campaign slugs).

How

  • New inviteCodeFromParams() in invite-code.utils.ts is the single reader: params.get('code') ?? params.get('invited_by'). Legacy code wins when both are present, so a pre-existing link keeps its behavior even with the new param appended — the alias is purely additive.
  • Readers switched to it: InvitesPage, /setup deep-link cookie persistence, native openDeepLink (useNativeAppLinks).
  • Writers now emit invited_by: generateInviteCodeLink(), inviteFlowUrl(), and the PublicProfile guest-door navigation.
  • No API change: /invites/validate and /invites/accept still receive the same bare-username code in the body. INVITE_CODE_TO_CAMPAIGN_MAP / badge-campaign resolution are untouched — they key off the resolved value, not the param name.
  • Analytics: link_type: 'invite_code' wire value unchanged (registered constant, PostHog continuity).

Tests

  • New unit suite for inviteCodeFromParams (both params, code-wins precedence, absent case).
  • Emitted-shape assertions updated (general.utils, invite-flow-url, PublicProfile, BadgeDetailModal).
  • Legacy ?code= deep-link tests kept as-is and now prove the alias; added invited_by variants for native route mapping and InvitesPage.
  • 8 suites / 205 tests green; tsc --noEmit clean.

New links (share links, guest-door nav, invite-flow URLs) emit
/invite?invited_by=<username> — the param names what the value is.
Every reader (InvitesPage, /setup deep-link persistence, native
openDeepLink) accepts both params via inviteCodeFromParams; legacy
?code= wins when both are present, so every previously shared link,
QR code, and campaign URL keeps its exact behavior. No API change:
the backend still receives the same bare-username invite code.
@vercel

vercel Bot commented Aug 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 Aug 26, 2026 5:47pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

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: f5d60a76-c4c0-47a0-b4c0-d75722b01abc

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

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

@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7451.71 → 7453.41 (+1.7)
Findings: +1 net (+35 new, -34 resolved)

🆕 New findings (35)

  • critical complexity — src/components/Invites/InvitesPage.tsx — CC 87, MI 55.11, SLOC 214
  • critical complexity — src/app/(setup)/setup/page.tsx — CC 66, MI 55.31, SLOC 242
  • high hotspot — src/constants/analytics.consts.ts — 53 commits, +422/-24 lines since 6 months ago
  • high hotspot — src/utils/general.utils.ts — 44 commits, +243/-332 lines since 6 months ago
  • high method-complexity — src/app/(setup)/setup/page.tsx:95 — CC 33 SLOC 110
  • high hotspot — src/components/Invites/InvitesPage.tsx — 32 commits, +558/-350 lines since 6 months ago
  • medium high-mdd — src/components/Invites/InvitesPage.tsx:40 — InvitePageContent: MDD 109.8 (uses across many lines from declarations)
  • medium high-mdd — src/app/(setup)/setup/page.tsx:27 — SetupPageContent: MDD 80.2 (uses across many lines from declarations)
  • medium high-dlt — src/app/(setup)/setup/page.tsx:27 — SetupPageContent: DLT 52 (calls 52 distinct functions — high context load)
  • medium high-dlt — src/components/Invites/InvitesPage.tsx:40 — InvitePageContent: DLT 50 (calls 50 distinct functions — high context load)
  • medium high-mdd — src/app/(setup)/setup/page.tsx:94 — : MDD 32.8 (uses across many lines from declarations)
  • medium high-dlt — src/app/(setup)/setup/page.tsx:94 — : DLT 30 (calls 30 distinct functions — high context load)
  • medium structural-dup — app/[...recipient]/page.tsx:175 — 28 duplicate lines / 101 tokens with app/invite/page.tsx:67
  • medium high-mdd — src/app/(setup)/setup/page.tsx:95 — determineInitialStep: MDD 27.0 (uses across many lines from declarations)
  • medium high-mdd — src/utils/native-routes.ts:112 — mapDeepLinkPath: MDD 26.2 (uses across many lines from declarations)
  • medium method-complexity — src/components/Invites/InvitesPage.tsx:40 — InvitePageContent CC 22 SLOC 96
  • medium method-complexity — src/utils/native-routes.ts:202 — rewriteMethodPath CC 16 SLOC 31
  • medium complexity — src/app/invite/page.tsx — CC 13, MI 58.58, SLOC 61
  • low high-dlt — src/app/(setup)/setup/page.tsx:95 — determineInitialStep: DLT 26 (calls 26 distinct functions — high context load)
  • low high-mdd — src/components/Invites/InvitesPage.tsx:145 — : MDD 18.9 (uses across many lines from declarations)

…and 15 more.

✅ Resolved (34)

  • src/components/Invites/InvitesPage.tsx — CC 87, MI 55.1, SLOC 214
  • src/app/(setup)/setup/page.tsx — CC 66, MI 55.3, SLOC 242
  • src/constants/analytics.consts.ts — 52 commits, +420/-23 lines since 6 months ago
  • src/utils/general.utils.ts — 43 commits, +235/-326 lines since 6 months ago
  • src/app/(setup)/setup/page.tsx:94 — CC 33 SLOC 110
  • src/app/(mobile-ui)/withdraw/page.tsx — 30 commits, +309/-205 lines since 6 months ago
  • src/components/Invites/InvitesPage.tsx:39 — InvitePageContent: MDD 109.8 (uses across many lines from declarations)
  • src/app/(setup)/setup/page.tsx:26 — SetupPageContent: MDD 80.2 (uses across many lines from declarations)
  • src/app/(setup)/setup/page.tsx:26 — SetupPageContent: DLT 51 (calls 51 distinct functions — high context load)
  • src/components/Invites/InvitesPage.tsx:39 — InvitePageContent: DLT 49 (calls 49 distinct functions — high context load)
  • src/app/(setup)/setup/page.tsx:93 — : MDD 32.8 (uses across many lines from declarations)
  • app/[...recipient]/page.tsx:175 — 28 duplicate lines / 101 tokens with app/invite/page.tsx:58
  • src/app/(setup)/setup/page.tsx:94 — determineInitialStep: MDD 27.0 (uses across many lines from declarations)
  • src/utils/native-routes.ts:112 — mapDeepLinkPath: MDD 25.7 (uses across many lines from declarations)
  • src/components/Invites/InvitesPage.tsx:39 — InvitePageContent CC 22 SLOC 96
  • src/utils/native-routes.ts:201 — rewriteMethodPath CC 16 SLOC 31
  • src/app/invite/page.tsx — CC 11, MI 57.44, SLOC 55
  • src/app/(setup)/setup/page.tsx:93 — : DLT 29 (calls 29 distinct functions — high context load)
  • src/app/(setup)/setup/page.tsx:94 — determineInitialStep: DLT 25 (calls 25 distinct functions — high context load)
  • src/app/invite/page.tsx:24 — generateMetadata: MDD 19.0 (uses across many lines from declarations)

…and 14 more.

@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • unit: 3608 ran, 0 failed, 0 skipped, 46.2s

📊 Coverage (unit)

metric %
statements 69.6%
branches 54.8%
functions 61.0%
lines 70.5%
⏱ 10 slowest test cases
time test
2.7s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.0s src/hooks/query/__tests__/user.test.tsx › does NOT clear a token that rotated mid-request (stale 401 racing a fresh login)
0.5s 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/hooks/__tests__/useCrispTokenId.test.ts › retries then stays undefined when the endpoint keeps failing (no fallback token)
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › the lazy bank view survives a re-render without blanking
0.3s src/utils/__tests__/sentry.utils.test.ts › still lets a per-call timeoutMs win over the default
0.3s src/utils/__tests__/crisp.test.ts › settles, and hands back a usable plugin, against a real-shaped plugin proxy
0.2s src/utils/__tests__/crisp.test.ts › configures once across repeated support opens
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

…rect

Review of #2828 found two readers the alias missed:

- src/app/invite/page.tsx generateMetadata read only ?code=, so every new
  ?invited_by= link unfurled as "Invites | Peanut / Invalid invite link"
  with the generic image (verified on the Vercel preview). Now goes through
  inviteCodeFromParams like the client readers.
- next.config.js root-domain hand-off was keyed on campaign|campaignTag|code
  only, so peanut.me/?invited_by=alice landed on the marketing page with
  the param dropped.

Also: an empty ?code= falls through to ?invited_by= instead of masking it;
stale ?code= comments updated. Tests cover both readers.
@0xkkonrad

Copy link
Copy Markdown
Contributor Author

Adversarial review — 26 Aug

Verified on this PR's Vercel preview + prod curls; full grep for every other code reader/writer across peanut-ui (dev + main), peanut-api-ts, mono content/skills.

Fixed in de3cfc1 (this branch)

  1. P0 src/app/invite/page.tsx generateMetadata read only searchParams.code → every new ?invited_by= link unfurled as "Invites | Peanut / Invalid invite link" with the generic image (preview: /invite?invited_by=squirrel vs ?code=squirrel). Now uses inviteCodeFromParams; test added.
  2. P1 next.config.js root-domain hand-off keyed on campaign|campaignTag|codepeanut.me/?invited_by=alice was a 200 landing page with the param dropped. Added invited_by; test added.
  3. Nits: empty ?code= falls through to invited_by (|| instead of ??); stale ?code= comments.

Fixed elsewhere

  • P2 seo-route-policy.js (main-only) crawl-allowed only /invite?code= → new shape crawl-blocked, Google can't see the noindex header → hotfix(seo): crawl-allow /invite?invited_by= like /invite?code= #2835 (hotfix → main).
  • P1 peanut-api-ts POST /redirects/claim extracted the invite code from the client targetUrl via ?code= only; qr/[code]/page.tsx now sends ?invited_by= → every new QR claim would have fallen back to <USER>INVITESYOU → peanutprotocol/peanut-api-ts#1431 (→ dev). Merge order: api#1431 before this.
  • telly-crm creator link template → invited_by (mono 6ee64a08).

Not changed, noted

  • Old native bundles read only code (universal links claim /invite, static export). Capgo OTA on push to dev covers it; neither store listing is public yet, so exposure is testers-only until their next OTA pull.
  • mono content/* CTAs still use invite?code=SQUIRRELINVITESYOU — works via the alias, left alone.
  • PostHog insights filtering $current_url on code= (if any) go blind for new links — not checked.

@chip-peanut-bot chip-peanut-bot 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.

Chip review — changes requested

The new reader paths are internally consistent and CI is green, but emitting only invited_by breaks invite attribution when those links open in an older installed native app.

Findings

  • MAJOR · src/utils/general.utils.ts:822 · Keep new invite links readable by older native builds
    A user on this release can share /invite?invited_by=alice; because /invite is an iOS/Android App Link, a recipient with the current older binary opens it in that bundle. At the supplied base SHA, both useNativeAppLinks and /setup read only code, so the app reaches signup without persisting inviteCode and the inviter is never accepted. Emit both invited_by and code with identical values for a transition window, or gate the writer until supported native versions can read invited_by; add a version-skew regression test.

Checked clean

  • Verified the detached worktree HEAD and exact supplied base/merge base.
  • Read the trusted PR title and description without fetching issue or review comments.
  • Checked CI at the exact head: unit, e2e, typecheck, eslint, format, deploy, and aggregate gates succeeded; ping was skipped.
  • Traced invited_by and legacy code precedence through metadata, InvitesPage, setup, the native route mapper, and the native link listener.
  • Compared the new writers with base-SHA native readers and the iOS/Android App Link declarations; this exposed the version-skew attribution loss.
  • Confirmed the API still accepts bare usernames and historical INVITESYOU forms, and checked the intentional legacy merchant-code writer.
  • Reviewed redirect safety, off-host native-link rejection, empty-parameter behavior, and changed tests for reachable regressions.

Second opinion skipped: openrouter-timeout.

Exact head: 8e5f6e4f0ea4 · Context: repo, mono, ci

export const generateInviteCodeLink = (username: string) => {
const inviteCode = toInviteCode(username)
const inviteLink = shareableUrl(`/invite?code=${inviteCode}`)
const inviteLink = shareableUrl(`/invite?invited_by=${inviteCode}`)

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.

MAJOR: Keep new invite links readable by older native builds

A user on this release can share /invite?invited_by=alice; because /invite is an iOS/Android App Link, a recipient with the current older binary opens it in that bundle. At the supplied base SHA, both useNativeAppLinks and /setup read only code, so the app reaches signup without persisting inviteCode and the inviter is never accepted. Emit both invited_by and code with identical values for a transition window, or gate the writer until supported native versions can read invited_by; add a version-skew regression test.

@innolope-dev

Copy link
Copy Markdown
Collaborator

@0xkkonrad — this one is CONFLICTING against dev (single conflict, src/components/Profile/components/PublicProfile.tsx) so it needs a dev merge before it can go in. CI is otherwise green and there are no open review threads.

Worth doing soon rather than later: #2876 brings the design system to dev, and it rebuilds PublicProfile.tsx — so resolving against today's dev is a much smaller job than resolving against it afterwards.

No migration work needed on this branch either way: it's the only feature PR in the current queue with zero DS debt — no legacy palette classes, no stock text sizes. Once the conflict is resolved it can merge straight away, and the integration branch absorbs it.

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