Skip to content

feat(refer) : Implement referral system with UI components and API integration#430

Merged
graycyrus merged 4 commits intotinyhumansai:mainfrom
YellowSnnowmann:feat/referral-system
Apr 8, 2026
Merged

feat(refer) : Implement referral system with UI components and API integration#430
graycyrus merged 4 commits intotinyhumansai:mainfrom
YellowSnnowmann:feat/referral-system

Conversation

@YellowSnnowmann
Copy link
Copy Markdown
Contributor

@YellowSnnowmann YellowSnnowmann commented Apr 8, 2026

Summary

  • Rewards page

    • Referral block with:
      • Code/link
      • Copy/share actions
      • Totals
      • Activity table:
        • masked referred user
        • status
        • reward
        • updated timestamp
    • Optional apply-code flow
    • Refresh stats
  • Onboarding

    • Optional referral-apply step after Welcome
    • Skipped if user already has referral attribution (profile or /referral/stats)
  • Core (Rust)

    • openhuman.referral_get_stats
    • openhuman.referral_apply
    • Uses BackendOAuthClient with session JWT (same pattern as billing)
    • Avoids WebView fetch → fixes WKWebView “Load failed” / CORS issues
  • App client

    • referralApi uses callCoreCommand
    • Normalizes backend response variations:
      • referredUserMasked
      • totalRewardsEarnedUsd
      • Decimal128
      • rewardAmountUsd
      • transactions
      • Joined / Completed
      • referralId / joinedAt
    • Derives totals/counts when aggregates are missing
  • Mock API

    • GET /referral/stats
    • POST /referral/apply
    • Used for local/Vitest
  • Tests

    • Vitest coverage:
      • normalizeReferralStats
      • referralApi wiring

Problem

  • Referral functionality existed on backend but not exposed in desktop app

  • Users couldn’t:

    • View referral codes
    • Share links
    • Track referral status
  • Direct WebView fetch caused failures:

    • “Load failed” (WKWebView)
    • CORS-like issues
  • Backend response inconsistencies:

    • Field names differed
    • Decimal formats (Mongo)
    • Aggregates vs row-level rewards
    • UI could show 0 or despite valid data

Solution

  • UI + Onboarding

    • Route:
      • GET /referral/stats
      • POST /referral/apply
    • Through core JSON-RPC
    • Normalize responses before rendering
  • Rust referral module

    • Thin reqwest adapters
    • Uses controller registry
    • No business logic duplication (kept in backend)
  • TypeScript normalization layer

    • Centralized mapping:
      • Field aliases
      • Decimal handling
      • Aggregate fallback logic
    • Derives totals when backend values are missing or zero
  • Tradeoff

    • Requires sidecar (like other callCoreCommand flows)
    • Not designed for pure web-only usage

Submission Checklist

  • Unit tests

    • Vitest:
      • app/src/services/api/__tests__/referralApi.test.ts
    • Covers:
      • normalization
      • RPC mocking
  • E2E / Integration

    • Not included
    • Suggested:
      • JSON-RPC test in tests/json_rpc_e2e.rs (mock backend)
      • Optional focused E2E test for full flow
  • Doc comments

    • Rust:
      • //! module-level docs
    • TypeScript:
      • Comments on RPC + normalization paths
    • Optional:
      • Expand /// for stricter Rust API docs
  • Inline comments

    • Minimal by design
    • Tests describe normalization logic
    • Add notes if reviewers request invariants:
      • e.g. "derive totals only when API total is 0"

Impact

  • Desktop (Tauri)

    • Primary target
    • Referral calls:
      • sidecar → backend
      • NOT WebView → backend
  • Web / CLI

    • Uses same RPC methods
    • Depends on:
      • session
      • api_url
  • Security

    • JWT remains in session flow
    • No new secrets exposed to UI
  • Compatibility

    • Requires updated openhuman binary
    • Older sidecars will fail on unknown RPC methods
  • Performance

    • Negligible overhead
    • One extra RPC round-trip per:
      • stats fetch
      • apply action

Related

Summary by CodeRabbit

  • New Features

    • Referral rewards program: personal referral codes, apply flow during onboarding, stats dashboard, share/copy controls, and device fingerprint support.
    • Referral application UI added to rewards and onboarding flows; activity table and status badges shown.
  • Documentation

    • New referral program documentation describing rules, eligibility, and migration guidance.
  • Tests

    • Added unit tests covering stats normalization, apply/get flows, and payment idempotency scenarios.

- Added  to document the referral system, including reward structure, rules, data model, migration, core services, and API endpoints.
- Created  component to display referral stats and allow users to apply referral codes.
- Integrated referral functionality into the onboarding process with  for applying referral codes.
- Updated  page to include the new .
- Implemented API calls in  for fetching referral stats and applying referral codes.
- Added tests for the referral API to ensure proper functionality and data normalization.
- Introduced device fingerprinting for referral code application to prevent abuse.

This commit establishes a comprehensive referral system, enhancing user engagement and incentivizing referrals.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 8, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a40e0b50-54e0-4f54-8364-f8d1ca633ffa

📥 Commits

Reviewing files that changed from the base of the PR and between fdbe49d and 3b372cd.

📒 Files selected for processing (5)
  • app/src/components/referral/ReferralRewardsSection.tsx
  • app/src/pages/onboarding/Onboarding.tsx
  • app/src/pages/onboarding/steps/WelcomeStep.tsx
  • app/src/services/api/__tests__/referralApi.test.ts
  • app/src/services/api/referralApi.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/src/pages/onboarding/steps/WelcomeStep.tsx
  • app/src/services/api/tests/referralApi.test.ts

📝 Walkthrough

Walkthrough

Adds a referral rewards system: documentation, frontend UI and onboarding steps for applying codes, frontend API/types/tests, device-fingerprint utility, mock endpoints, backend RPC ops/schemas/registry, and a migration to backfill legacy referral data.

Changes

Cohort / File(s) Summary
Documentation
Referral-doc.md
New design doc describing reward rules, data models, migration, services, API endpoints, payment integration points, and tests.
Frontend Types & Utils
app/src/types/referral.ts, app/src/utils/deviceFingerprint.ts
Adds referral TypeScript types and a stable device fingerprint generator persisted to localStorage.
Frontend API & Tests
app/src/services/api/referralApi.ts, app/src/services/api/__tests__/referralApi.test.ts
New referralApi with normalization logic for varied backend payloads, RPC wrappers (getStats/applyCode), error shaping, and comprehensive tests covering schema coercion and idempotency.
Frontend Components
app/src/components/referral/ReferralRewardsSection.tsx
New ReferralRewardsSection React component rendering stats, share/copy, activity table, and apply-code flow with request-id guards and multi-state handling.
Pages / Onboarding
app/src/pages/Rewards.tsx, app/src/pages/onboarding/Onboarding.tsx
Rewards page includes referral section; onboarding flow updated to preflight referral status, optionally skip referral step, and compute dynamic progress/step sequencing.
Onboarding Step
app/src/pages/onboarding/steps/ReferralApplyStep.tsx, app/src/pages/onboarding/steps/WelcomeStep.tsx
Adds ReferralApplyStep component (apply/skip/back, success auto-advance) and extends WelcomeStep props to support disabling/loading the Next button.
Mock Server
scripts/mock-api-core.mjs
Adds GET /referral/stats and POST /referral/apply handlers and advertises x-device-fingerprint CORS header.
Backend RPC Adapter
src/openhuman/referral/ops.rs, src/openhuman/referral/schemas.rs, src/openhuman/referral/mod.rs
New referral RPC adapter exposing get_stats and apply_code, controller schemas, handlers, and param deserialization/response conversion.
Backend Registry
src/openhuman/mod.rs, src/core/all.rs
Registers referral module and adds namespace description to controller registry.
Migration
src/migrations/1744200000000-referral-system.ts
Migration to create indexes and backfill referral codes/referrals from legacy user.referral.invitedBy fields for users and tgusers.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant FE as Frontend (Rewards UI)
    participant API as referralApi
    participant Core as Core RPC Adapter
    participant Backend as Hosted Backend
    User->>FE: Open Rewards page
    FE->>API: getStats()
    API->>Core: openhuman.referral_get_stats()
    Core->>Backend: GET /referral/stats
    Backend-->>Core: JSON stats payload
    Core-->>API: RpcOutcome(JSON)
    API->>API: normalizeReferralStats(raw)
    API-->>FE: ReferralStats
    FE-->>User: Render referral UI & activity
Loading
sequenceDiagram
    actor User
    participant Step as ReferralApplyStep
    participant Utils as deviceFingerprint
    participant API as referralApi
    participant Core as Core RPC Adapter
    participant Backend as Hosted Backend
    User->>Step: Enter code & click Apply
    Step->>Utils: getOrCreateDeviceFingerprint()
    Utils-->>Step: deviceFingerprint
    Step->>API: applyCode(code)
    API->>Core: openhuman.referral_apply(code, deviceFingerprint)
    Core->>Backend: POST /referral/apply { code, deviceFingerprint }
    Backend-->>Core: acknowledgment JSON
    Core-->>API: RpcOutcome(JSON)
    API-->>Step: resolved
    Step->>Step: refresh() user state, call onApplied, auto-advance
    Step-->>User: Success UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

Poem

🐰
I dug a tunnel of code so neat,
Referral carrots for friends to meet,
Fingerprints stored, RPCs in line,
Rewards hop in, one at a time,
A tiny-hop celebration — sprinkle of shine!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(refer) : Implement referral system with UI components and API integration' accurately summarizes the main change—adding a complete referral system across UI, onboarding, client, and core.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@YellowSnnowmann YellowSnnowmann marked this pull request as ready for review April 8, 2026 13:02
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (3)
app/src/pages/onboarding/Onboarding.tsx (1)

23-30: Use a const arrow helper for hasReferralFromProfile.

This small local helper is the only remaining function declaration in the file; converting it keeps the onboarding code aligned with the repo’s TS style.

As per coding guidelines, **/*.{js,jsx,ts,tsx}: Prefer arrow functions over function declarations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/pages/onboarding/Onboarding.tsx` around lines 23 - 30, Replace the
function declaration hasReferralFromProfile with a const arrow helper to match
the project's TS style: declare a const named hasReferralFromProfile assigned to
an arrow function with the same parameter type union and body (returning
!!(user?.referral?.invitedBy || user?.referral?.invitedByCode)); keep the exact
logic and exported/used references unchanged so all callers continue to work.
app/src/components/referral/ReferralRewardsSection.tsx (1)

8-32: Prefer arrow helpers for these local utilities.

These new local helpers are function declarations, while the app TS style in this repo prefers const arrow helpers for new code.

As per coding guidelines, **/*.{js,jsx,ts,tsx}: Prefer arrow functions over function declarations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/components/referral/ReferralRewardsSection.tsx` around lines 8 - 32,
Replace the three local function declarations with const arrow helpers: convert
function formatUsd, statusBadgeClass, and statusLabel into const formatUsd = (n:
number) => ..., const statusBadgeClass = (status: ReferralRelationshipStatus) =>
..., and const statusLabel = (status: ReferralRelationshipStatus) => ...; keep
the same parameter types, return types, switch logic, and exported semantics (if
any) so behavior is unchanged but style matches repo preference for arrow
helpers.
app/src/services/api/referralApi.ts (1)

10-260: Use const arrow helpers throughout this new TS module.

The new normalizer stack is built from function declarations, which drifts from the repo’s TS style and makes this module inconsistent with the rest of the app code.

As per coding guidelines, **/*.{js,jsx,ts,tsx}: Prefer arrow functions over function declarations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/services/api/referralApi.ts` around lines 10 - 260, The module uses
function declarations rather than the project's preferred const arrow helpers;
convert top-level functions (e.g., num, coerceMoney, coerceId, asRecord,
normalizeStatus, rowRewardUsd, normalizeRow, deriveTotalsFromReferrals,
normalizeReferralStats) into const arrow functions preserving their behavior and
exported signature (keep normalizeReferralStats exported), ensure types and
returns remain identical, and run/type-check to confirm no behavioral changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/components/referral/ReferralRewardsSection.tsx`:
- Around line 162-166: The display currently rounds basis points to whole
percent in ReferralRewardsSection by using (stats.rewardRateBps /
100).toFixed(0); change the formatting so the percent shows up to two decimal
places (e.g., use toFixed(2) or an Intl.NumberFormat with
maximumFractionDigits:2) when rendering stats.rewardRateBps to preserve values
like 12.5%; update the text rendering that references stats.rewardRateBps
accordingly so the percent and the "(basis points ...)" part remain correct.
- Around line 49-74: loadStats can write stale results after a logout/account
switch; add a request identity guard so only the most recent call updates state.
Introduce a ref like latestRequestIdRef incremented inside loadStats before
calling referralApi.getStats(), capture that id in the async call, and before
calling setStats/setLoadError/setLoading check that the captured id equals
latestRequestIdRef.current (or use an AbortController and pass signal to
referralApi.getStats if it supports aborting) so stale responses are ignored;
update references to loadStats, referralApi.getStats, and the useEffect that
invokes loadStats accordingly.
- Around line 267-271: In ReferralRewardsSection, avoid rendering raw internal
IDs by removing the fallback to row.referredUserId in the table cell rendering
(currently falling back after row.referredUserMasked and
row.referredDisplayName); instead, only render row.referredUserMasked or
row.referredDisplayName and then the default '—'. Update the JSX that constructs
the cell (the td where row.referredUserMasked || row.referredDisplayName ||
row.referredUserId || '—' is used) to drop row.referredUserId so it becomes
row.referredUserMasked || row.referredDisplayName || '—'.

In `@app/src/pages/onboarding/Onboarding.tsx`:
- Around line 48-90: The effect that fetches referral stats is currently
performing synchronous state updates (calls to setSkipReferralStep,
setReferralGateReady, setCurrentStep) inside useEffect which trips the
react-hooks/set-state-in-effect rule; refactor by moving the synchronous
branches into derived render logic or initial state/handlers and keep the effect
only for the async referralApi.getStats call and its results: i.e., compute and
render the referral gate readiness and skipReferralStep when token or
profileAlreadyReferred change (use derived booleans or initialize useState
accordingly) instead of calling setSkipReferralStep/setReferralGateReady
directly in those non-async branches inside the first useEffect, and change the
second useEffect (which advances currentStep when skipReferralStep &&
currentStep === 1) to run only when skipReferralStep changes or perform the step
advance in the render flow/handlers rather than synchronously in effect; retain
referralApi.getStats and its setSkipReferralStep/setReferralGateReady updates
only after the async response.

In `@app/src/pages/onboarding/steps/WelcomeStep.tsx`:
- Line 95: In the WelcomeStep component update the button label text from the
incorrect "Let Start" to the correct "Let's Start" by changing the label prop
value on the relevant element (label="Let Start") to label="Let's Start" inside
the WelcomeStep JSX so the displayed button reads correctly; ensure proper
escaping/quoting for the apostrophe in the string literal used in the component.

In `@app/src/services/api/__tests__/referralApi.test.ts`:
- Around line 110-117: Add a regression test that verifies
normalizeReferralStats prefers backend-provided totals even when they are zero
by creating a case where totals: { totalRewardUsd: 0, pendingCount: 0,
convertedCount: 0 } is returned alongside non-zero referral rows (e.g.,
referrals with rewardUsd > 0 and status 'converted'), call
normalizeReferralStats and assert stats.totals.totalRewardUsd === 0 and
stats.totals.convertedCount === 0 to ensure the explicit zero aggregates win
over any truthiness-based fallback; place the new it(...) beside the existing
test that references normalizeReferralStats and stats.totals to cover the
regression.

In `@app/src/services/api/referralApi.ts`:
- Around line 267-289: Wrap RPC calls in getStats and applyCode with try/catch
and normalize thrown errors into the expected contract before rethrowing: catch
the error from callCoreCommand in getStats and in applyCode, extract a useful
message from err.error or err.message (falling back to String(err)), and throw
an object like { success: false, error: <message> } so callers (e.g.,
ReferralApplyStep.tsx and ReferralRewardsSection.tsx) can read err.error
reliably; locate the callCoreCommand usages inside getStats and applyCode to
implement this change.

---

Nitpick comments:
In `@app/src/components/referral/ReferralRewardsSection.tsx`:
- Around line 8-32: Replace the three local function declarations with const
arrow helpers: convert function formatUsd, statusBadgeClass, and statusLabel
into const formatUsd = (n: number) => ..., const statusBadgeClass = (status:
ReferralRelationshipStatus) => ..., and const statusLabel = (status:
ReferralRelationshipStatus) => ...; keep the same parameter types, return types,
switch logic, and exported semantics (if any) so behavior is unchanged but style
matches repo preference for arrow helpers.

In `@app/src/pages/onboarding/Onboarding.tsx`:
- Around line 23-30: Replace the function declaration hasReferralFromProfile
with a const arrow helper to match the project's TS style: declare a const named
hasReferralFromProfile assigned to an arrow function with the same parameter
type union and body (returning !!(user?.referral?.invitedBy ||
user?.referral?.invitedByCode)); keep the exact logic and exported/used
references unchanged so all callers continue to work.

In `@app/src/services/api/referralApi.ts`:
- Around line 10-260: The module uses function declarations rather than the
project's preferred const arrow helpers; convert top-level functions (e.g., num,
coerceMoney, coerceId, asRecord, normalizeStatus, rowRewardUsd, normalizeRow,
deriveTotalsFromReferrals, normalizeReferralStats) into const arrow functions
preserving their behavior and exported signature (keep normalizeReferralStats
exported), ensure types and returns remain identical, and run/type-check to
confirm no behavioral changes.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2c7b4e5a-113d-4574-a14b-57a3e4e034a1

📥 Commits

Reviewing files that changed from the base of the PR and between a8b9304 and fdbe49d.

📒 Files selected for processing (16)
  • Referral-doc.md
  • app/src/components/referral/ReferralRewardsSection.tsx
  • app/src/pages/Rewards.tsx
  • app/src/pages/onboarding/Onboarding.tsx
  • app/src/pages/onboarding/steps/ReferralApplyStep.tsx
  • app/src/pages/onboarding/steps/WelcomeStep.tsx
  • app/src/services/api/__tests__/referralApi.test.ts
  • app/src/services/api/referralApi.ts
  • app/src/types/referral.ts
  • app/src/utils/deviceFingerprint.ts
  • scripts/mock-api-core.mjs
  • src/core/all.rs
  • src/openhuman/mod.rs
  • src/openhuman/referral/mod.rs
  • src/openhuman/referral/ops.rs
  • src/openhuman/referral/schemas.rs

Comment thread app/src/components/referral/ReferralRewardsSection.tsx
Comment thread app/src/components/referral/ReferralRewardsSection.tsx
Comment thread app/src/components/referral/ReferralRewardsSection.tsx Outdated
Comment thread app/src/pages/onboarding/Onboarding.tsx Outdated
Comment thread app/src/pages/onboarding/steps/WelcomeStep.tsx Outdated
Comment thread app/src/services/api/__tests__/referralApi.test.ts
Comment thread app/src/services/api/referralApi.ts
Comment thread app/src/services/api/referralApi.ts
- Added a new function to format reward rates from basis points to percentage for better display in the ReferralRewardsSection.
- Improved loading state management in the referral stats loading process to prevent race conditions.
- Updated the Onboarding component to handle referral step skipping more effectively, ensuring a smoother user experience.
- Fixed a typo in the WelcomeStep component's button label for clarity.
- Enhanced error handling in the referral API to provide clearer feedback on failures.

These changes improve the usability and reliability of the referral system and onboarding experience.
@graycyrus graycyrus merged commit 45a8216 into tinyhumansai:main Apr 8, 2026
8 of 9 checks passed
@YellowSnnowmann YellowSnnowmann linked an issue Apr 9, 2026 that may be closed by this pull request
34 tasks
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.

[Feature] Build referral system in Rewards page

2 participants