Skip to content

feat: implement referral system with API endpoints and UI components#433

Open
sundayonah wants to merge 10 commits into
mainfrom
feat/user-referral-and-earnings-program-clean
Open

feat: implement referral system with API endpoints and UI components#433
sundayonah wants to merge 10 commits into
mainfrom
feat/user-referral-and-earnings-program-clean

Conversation

@sundayonah
Copy link
Copy Markdown
Collaborator

@sundayonah sundayonah commented Mar 26, 2026

Description

This pull request implements a comprehensive referral system for Noblocks, enabling existing users to generate unique referral codes/links, track referral status, and earn rewards upon successful referrals. It covers both new user acquisition flows (post-onboarding referral code entry) and existing user referral management (dashboard for tracking earnings and referrals). The implementation aligns with the provided Figma designs and backend requirements for code generation, validation, and reward crediting.

New User Referral Flow

  • Added a post-onboarding modal/screen prompting new users to enter a referral code or skip with "I don't have a referral code."
  • Integrated backend endpoint for validating submitted referral codes and associating them with the new user's account.
  • Upon successful KYC/profile completion and first stablecoin transaction, the new user receives an immediate $1 USDC welcome credit to their Noblocks wallet.

Existing User Referral & Tracking Flow

  • Introduced a dedicated "Earnings/Referral Dashboard" accessible from the Home/Wallet screen via the "Invite, Earn, Repeat" CTA.
  • Generated unique 6-character alphanumeric referral codes (e.g., NB738K) for every existing user on first access to the dashboard.
  • Added UI components for displaying the referral code, copy buttons for code/link, and instructional text on earning rewards (e.g., "$1 for both on first $100 transaction").
  • Implemented tabbed view (Pending/Earned) with summaries for total earned/pending rewards and a list of referrals showing wallet address, date, amount, and status.
  • Backend tracking service links referrers to referred users, updating status from Pending to Earned and crediting $1 USDC to the referrer's wallet upon qualifying action completion (KYC + first stablecoin transaction).

Technical Integrations

  • Wallet integration for secure reward crediting in USDC.
  • Ensured referral lifecycle tracking prevents rewards for incomplete actions (e.g., no reward for signup alone).

References

Design Reference: Figma - Noblocks Web App
closes #279

Testing

Screenshot 2025-11-18 184520 Screenshot 2025-11-18 184439 Screenshot 2025-11-18 181542 Screenshot 2025-11-18 181457 Screenshot 2025-11-18 181412 Screenshot 2025-11-18 181401

Checklist

  • I have added documentation and tests for new/changed functionality in this PR
  • All active GitHub checks for tests, formatting, and security are passing
  • The correct base branch is being used, if not main
    By submitting a PR, I agree to Noblocks's Contributor Code of Conduct and Contribution Guide.

Summary by CodeRabbit

  • New Features

    • Referral program: submit invite codes, copy/share invite links and codes
    • Referral dashboard (desktop + mobile): earned vs pending totals, per-referral details, tabs, skeleton UIs
    • In-app referral CTA, entry modal, and wallet-integrated referral flow to open the dashboard
    • Reward claiming flow with KYC and qualifying-transaction checks
  • Chores

    • Config/env additions and a DB migration to enforce referral uniqueness and support referral settings

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 26, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c0afc83e-33dd-48e4-95fa-e60d33253aef

📥 Commits

Reviewing files that changed from the base of the PR and between e848e12 and f1cd4e9.

📒 Files selected for processing (9)
  • .env.example
  • app/api/aggregator.ts
  • app/components/MainPageContent.tsx
  • app/components/MobileDropdown.tsx
  • app/components/index.ts
  • app/lib/privy.ts
  • app/types.ts
  • app/utils.ts
  • middleware.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • app/components/index.ts
  • app/types.ts
  • app/api/aggregator.ts
  • app/lib/privy.ts
  • app/components/MobileDropdown.tsx
  • app/utils.ts
  • app/components/MainPageContent.tsx

📝 Walkthrough

Walkthrough

Adds a referral program: server APIs for submission, referral-data, and claim processing (with KYC, transaction-volume checks, and USDC payouts), client API helpers, desktop and mobile referral UI (input modal, dashboard, CTA, skeletons), types/config, utilities, Privy auth helper, and middleware routing for referral endpoints.

Changes

Referral Program Feature

Layer / File(s) Summary
Types and Configuration
app/types.ts, .env.example, app/lib/config.ts
Adds referrals view to MobileSheetView; extends Config with referralMinQualifyingVolumeUsd and referralRewardAmountUsd; adds ReferralData, ApiResponse<T>, and SubmitReferralResult types; updates .env.example with referral env vars.
Backend Infra & Auth
app/lib/privy.ts, middleware.ts
Adds getPrivyUserIdFromRequest to resolve Privy user id from NextRequest (x-user-id or verified Bearer token). Middleware matcher now includes /api/referral routes.
Referral Code Submission Endpoint
app/api/referral/submit/route.ts
POST /api/referral/submit: authenticates via x-user-id, validates/normalizes code (^NB[A-Z0-9]{4}$), prevents duplicates and self-referrals, inserts pending referral record, and records analytics; returns 201 on success or appropriate error codes.
Referral Data Fetch & Auto-Generation
app/api/referral/referral-data/route.ts
GET /api/referral/referral-data: requires auth, fetches/auto-generates referral_code (up to 10 attempts), returns merged referrals with per-wallet pending/earned status, computes totals, logs response, and fires background claim checks for pending/earned referrals.
Reward Claim Processing
app/api/referral/claim/route.ts
GET/POST /api/referral/claim: authenticates, verifies KYC, computes qualifying transaction volume (USDC/USDT 1:1; optional cNGN conversion), manages idempotent referral_claims rows, submits on-chain USDC transfer on Base, and persists claim state (completed/failed).
Utility Functions
app/utils.ts
Adds getAvatarImageFromAddress(), generateReferralCode(), handleCopyCode(), and handleCopyLink() for avatar selection, code generation, and clipboard copy UX with toast feedback.
Client API Helpers
app/api/aggregator.ts
Adds submitReferralCode(code, accessToken?) and getReferralData(accessToken, walletAddress?) returning typed ApiResponse results with consistent error mapping.
Desktop Referral Input Modal
app/components/ReferralModal.tsx
ReferralInputModal component with input validation, Privy token retrieval, submission via submitReferralCode, and toast-driven success/error flows.
Desktop Referral Dashboard
app/components/ReferralDashboard.tsx
ReferralDashboard fetches referral data on open, shows earned/pending totals, invite code with copy actions, and a tabbed referral list; shows ReferralDashboardSkeleton while loading.
CTA & Skeleton
app/components/ReferralCTA.tsx, app/components/ReferralDashboardSkeleton.tsx
ReferralCTA renders animated CTA; ReferralDashboardSkeleton provides loading placeholders matching dashboard layout.
Desktop UI Integration
app/components/MainPageContent.tsx, app/components/WalletDetails.tsx, app/components/FundWalletForm.tsx
Wires referral modal/dashboard/CTA into main UI, adds showReferralModal state and localStorage-based eligibility check, simplifies rate-refetch logic, and adds "referrals" view support to FundWalletForm.
Mobile Referral Dashboard
app/components/wallet-mobile-modal/ReferralDashboardView.tsx
Mobile-optimized dashboard component: fetches referral data when opened, shows totals, copy actions, tabbed list, and skeleton during load.
Mobile Skeletons
app/components/ReferralDashboardViewSkeleton.tsx, app/components/ReferralDashboardSkeleton.tsx
Scrollable mobile skeletons and repeated row placeholders used during mobile dashboard loading.
Mobile UI Integration
app/components/MobileDropdown.tsx, app/components/wallet-mobile-modal/WalletView.tsx, app/components/NetworkSelectionModal.tsx
Adds "referrals" view to mobile dropdown, renders ReferralCTA in WalletView for non-injected wallets, and updates NetworkSelectionModal to accept onNetworkSelected and persist dismissal state.
Barrel Exports
app/components/index.ts, app/components/wallet-mobile-modal/index.ts
Re-exports new referral components (ReferralInputModal, ReferralCTA, ReferralDashboardView) from component barrels.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • paycrest/noblocks#217: Related middleware/matching changes that also touch analytics and route instrumentation.

Suggested labels

enhancement

Suggested reviewers

  • chibie
  • 5ran6
  • onahprosper

Poem

🐇 I nibble codes in moonlit glade,
NB keys in paw, a lively trade.
Share the link, the coins will flow,
A hop, a hop — referrals grow! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% 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
Title check ✅ Passed The title 'feat: implement referral system with API endpoints and UI components' accurately describes the main objective of the PR—implementing a complete referral system with backend endpoints and frontend components.
Description check ✅ Passed The PR description covers the main objectives (new user flow, existing user flow, technical integrations), references the linked issue #279, includes testing evidence (screenshots), and confirms all checklist items are completed.
Linked Issues check ✅ Passed The implementation addresses all core acceptance criteria: post-onboarding referral modal with code entry/skip option [#279], referral code validation endpoint, referral dashboard with code display and copy functionality, pending/earned status tracking, reward crediting on qualifying actions, and wallet integration.
Out of Scope Changes check ✅ Passed All code changes align with the referral system requirements. Minor infrastructure updates (middleware matcher, config additions, type definitions) are necessary to support the referral feature.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

app/lib/privy.ts

Parsing error: '}' expected.


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

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

🧹 Nitpick comments (13)
app/components/NetworkSelectionModal.tsx (1)

49-55: Remove commented-out dead code.

The commented-out handleClose function is no longer needed and adds noise to the file.

🧹 Remove dead code
-  // const handleClose = () => {
-  //   if (user?.wallet?.address) {
-  //     const storageKey = `hasSeenNetworkModal-${user.wallet.address}`;
-  //     localStorage.setItem(storageKey, "true");
-  //   }
-  //   setIsOpen(false);
-  // };
-
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/NetworkSelectionModal.tsx` around lines 49 - 55, Remove the
dead/commented-out handleClose function: delete the commented block containing
handleClose and its internals (references to user?.wallet?.address, storageKey =
`hasSeenNetworkModal-${user.wallet.address}`, localStorage.setItem, and
setIsOpen(false)) so the file no longer contains the unused commented code and
noise.
app/components/ReferralCTA.tsx (1)

5-12: Remove unused import and simplify handler.

useRouter is imported but never used. Also, the if (onViewReferrals) check is redundant since the prop is required (not optional with ?).

♻️ Proposed cleanup
 "use client";

 import { motion } from "framer-motion";
 import Image from "next/image";
-import { useRouter } from "next/navigation";

 export const ReferralCTA = ({ onViewReferrals }: { onViewReferrals: () => void }) => {
-    const router = useRouter();
-
-    const handleViewReferrals = () => {
-        if (onViewReferrals) return onViewReferrals();
-    };

     return (
         <motion.div

Then update the button's onClick:

             <button
                 type="button"
-                onClick={handleViewReferrals}
+                onClick={onViewReferrals}
                 className="min-h-11 w-full rounded-xl bg-accent-gray py-2.5 text-sm font-medium text-gray-900 transition-colors hover:bg-accent-gray/80 dark:bg-white/5 dark:text-white dark:hover:bg-white/10"
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/ReferralCTA.tsx` around lines 5 - 12, Remove the unused
useRouter import and simplify the click handler in ReferralCTA: delete the
import of useRouter, remove the const router = useRouter() line, and replace the
handleViewReferrals implementation so it directly calls the required prop (e.g.,
const handleViewReferrals = () => onViewReferrals();). Update any button onClick
to use handleViewReferrals if not already.
app/components/FundWalletForm.tsx (1)

23-32: Use imported MobileSheetView instead of duplicating the type.

MobileSheetView is imported but not used. The local MobileView type duplicates it. Consider using the shared type directly to avoid drift and duplication.

♻️ Proposed fix
 import { Token, type MobileSheetView } from "../types";
 import Image from "next/image";

-type MobileView =
-  | "wallet"
-  | "settings"
-  | "transfer"
-  | "fund"
-  | "history"
-  | "referrals";
+type MobileView = MobileSheetView;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/FundWalletForm.tsx` around lines 23 - 32, Replace the
duplicated local type alias MobileView with the imported MobileSheetView: remove
the local MobileView declaration and update any usages of MobileView in this
file (e.g., props, state, handlers) to use MobileSheetView instead so the
component relies on the shared MobileSheetView type; ensure the import {
MobileSheetView } remains used and run type checks to confirm no remaining
references to MobileView.
app/components/TransferForm.tsx (1)

30-36: Imported MobileSheetView is unused; local MobileView type is inconsistent.

MobileSheetView is imported but not used. The local MobileView type on line 36 lacks the "referrals" view that exists in the shared type. Consider using MobileSheetView directly to ensure consistency across components.

♻️ Proposed fix to use shared type
-import { Token, type MobileSheetView } from "../types";
+import { Token, type MobileSheetView } from "../types";
 import { networks } from "../mocks";
 import { getNetworkImageUrl } from "../utils";
 import { useActualTheme } from "../hooks/useActualTheme";
 import Image from "next/image";

-type MobileView = "wallet" | "settings" | "transfer" | "fund" | "history";
+type MobileView = MobileSheetView;

 export const TransferForm: React.FC<{

Or simply remove the import if you prefer keeping the local type and just add "referrals" to it for completeness.

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

In `@app/components/TransferForm.tsx` around lines 30 - 36, The file defines a
local MobileView type but also imports MobileSheetView which is unused and the
local type is missing the "referrals" variant; either remove the unused import
and add "referrals" to the MobileView union, or (preferably) delete the local
MobileView and replace its uses with the shared MobileSheetView type so the
component consistently uses MobileSheetView (update any references in
TransferForm.tsx to MobileView → MobileSheetView and remove the unused import or
local type accordingly).
app/components/MobileDropdown.tsx (1)

48-55: Consider using the imported MobileSheetView type directly.

The currentView state uses an inline union type that duplicates what's likely defined in the imported MobileSheetView type. This could lead to type drift if MobileSheetView is updated elsewhere.

♻️ Suggested refactor
-  const [currentView, setCurrentView] = useState<
-    | "wallet"
-    | "settings"
-    | "transfer"
-    | "fund"
-    | "history"
-    | "referrals"
-  >("wallet");
+  const [currentView, setCurrentView] = useState<MobileSheetView>("wallet");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/MobileDropdown.tsx` around lines 48 - 55, Replace the inline
union type on the currentView state with the imported MobileSheetView type:
change useState<...>("wallet") to useState<MobileSheetView>("wallet") and ensure
the imported MobileSheetView symbol is used (and remove the inline union) so
currentView and setCurrentView rely on the canonical MobileSheetView type.
app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1)

71-73: Consider adding type safety for referral data.

The referralData and filteredReferrals are typed as any, which bypasses TypeScript's type checking. Consider defining proper interfaces for the referral data structure.

♻️ Suggested type definition
interface Referral {
    id: string;
    wallet_address: string;
    wallet_address_short: string;
    status: "pending" | "earned";
    amount: number;
}

interface ReferralData {
    referral_code: string;
    total_earned: number;
    total_pending: number;
    referrals: Referral[];
}

Then update:

-const [referralData, setReferralData] = useState<any | null>(null);
+const [referralData, setReferralData] = useState<ReferralData | null>(null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/wallet-mobile-modal/ReferralDashboardView.tsx` around lines 71
- 73, Define proper TypeScript interfaces (e.g., Referral and ReferralData) and
replace the any types used for referralData and filteredReferrals in
ReferralDashboardView.tsx: type the referralData variable/state/prop as
ReferralData | undefined and change filteredReferrals to Referral[] by updating
the expression that computes it (the filteredReferrals declaration and any place
that consumes referralData.referrals). Also update any function signatures or
props that pass referral data (e.g., props or hooks that return referralData) to
use the new types so the component benefits from compile-time checking.
app/components/MainPageContent.tsx (1)

199-212: Consider adding null-check for wallet address availability.

The handleNetworkSelected callback checks user?.wallet?.address but authenticated can be true before the wallet is fully linked. This could cause the modal to be skipped silently if the wallet address isn't available yet.

♻️ Suggested defensive check
   const handleNetworkSelected = useCallback(() => {
-    if (!authenticated || !user?.wallet?.address) {
+    // Wait for wallet to be fully available before checking referral modal
+    if (!authenticated || !ready || !user?.wallet?.address) {
       return;
     }

     // Check if user has already seen the referral modal
     const referralStorageKey = `hasSeenReferralModal-${user.wallet.address}`;
     const hasSeenReferralModal = localStorage.getItem(referralStorageKey);

     if (!hasSeenReferralModal) {
       // Show referral modal after network selection
       setShowReferralModal(true);
     }
-  }, [authenticated, user?.wallet?.address]);
+  }, [authenticated, ready, user?.wallet?.address]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/MainPageContent.tsx` around lines 199 - 212,
handleNetworkSelected currently returns early when authenticated is true but
user.wallet.address is missing, causing the referral modal to be skipped; update
this callback to explicitly null-check user.wallet and user.wallet.address and
defer or enqueue showing the modal until the wallet address becomes available
instead of silently returning. Specifically, inside handleNetworkSelected check
for user and user.wallet first, compute referralStorageKey using
user.wallet.address only when present, and if authenticated but address is not
yet present either (a) store a temporary flag/state to attempt showing the
referral modal once user.wallet.address is populated (use an effect that watches
user?.wallet?.address) or (b setShowReferralModal after the address appears);
keep references to referralStorageKey, localStorage.getItem, and
setShowReferralModal when implementing the fix.
app/components/ReferralModal.tsx (1)

39-64: Simplify nested try-catch structure.

The nested try-catch at lines 48-64 is redundant since submitReferralCode already returns an ApiResponse object rather than throwing. The outer catch at lines 65-68 would only catch errors from getAccessToken() which is already handled by the inner try-catch's logic.

♻️ Simplified structure
         try {
             const code = referralCode.trim().toUpperCase();
             if (!/^NB[A-Z0-9]{4}$/.test(code)) {
                 toast.error("Invalid referral code format");
                 return;
             }

             const token = await getAccessToken();
-
-            try {
-                const res = await submitReferralCode(code, token ?? undefined);
-
-                if (res && res.success) {
-                    toast.success(res.data?.message || "Referral code applied! Complete KYC and your first transaction to earn rewards.");
-                    onSubmitSuccess();
-                    onClose();
-                } else {
-                    // API returned a well-formed error response
-                    const message = res && !res.success ? res.error : "Failed to submit referral code. Please try again.";
-                    toast.error(message);
-                }
-            } catch (err) {
-                // Unexpected errors (should be rare since submitReferralCode returns ApiResponse)
-                const message = err instanceof Error ? err.message : "Failed to submit referral code. Please try again.";
-                toast.error(message);
+            const res = await submitReferralCode(code, token ?? undefined);
+
+            if (res && res.success) {
+                toast.success(res.data?.message || "Referral code applied! Complete KYC and your first transaction to earn rewards.");
+                onSubmitSuccess();
+                onClose();
+            } else {
+                const message = res && !res.success ? res.error : "Failed to submit referral code. Please try again.";
+                toast.error(message);
             }
         } catch (error) {
             toast.error(
-                error instanceof Error ? error.message : "Invalid referral code. Please check and try again."
+                error instanceof Error ? error.message : "Failed to submit referral code. Please try again."
             );
         } finally {
             setIsSubmitting(false);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/ReferralModal.tsx` around lines 39 - 64, The nested try-catch
is unnecessary: remove the inner try/catch around submitReferralCode and instead
use a single try/catch that covers getAccessToken() and submitReferralCode(code,
token), validate the code with the existing regex
(referralCode.trim().toUpperCase()), then check the returned ApiResponse (res &&
res.success) to call toast.success + onSubmitSuccess() + onClose() or
toast.error with res.error/default message; let the outer catch handle any
unexpected thrown errors from getAccessToken or submitReferralCode and surface a
generic toast.error.
app/api/referral/referral-data/route.ts (1)

60-77: Sequential uniqueness checks may be slow under high contention.

The collision-check loop runs up to 10 sequential database queries to find a unique code. With a 36^4 (≈1.7M) code space, collisions should be rare, but under heavy concurrent signups this could become a bottleneck.

Consider using a single query with INSERT ... ON CONFLICT DO NOTHING and checking the result, or generating multiple candidates and checking them in a single IN query.

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

In `@app/api/referral/referral-data/route.ts` around lines 60 - 77, The current
loop in route.ts that calls generateReferralCode() and does sequential
uniqueness checks against
supabaseAdmin.from("users").select(...).eq("referral_code", ...) should be
replaced with an atomic approach to avoid many round-trips under contention:
either (A) attempt an INSERT into the users/referral table (or a dedicated
referrals table) with the generated code using Postgres' INSERT ... ON CONFLICT
DO NOTHING and check the insert result to know if the code was accepted, or (B)
generate N candidates via generateReferralCode() and issue a single
supabaseAdmin.from("users").select("referral_code").in("referral_code",
candidates) to filter out collisions and then pick a remaining candidate;
implement this logic inside the same function that currently houses the while
loop so callers of generateReferralCode/route.ts see the new atomic/batched
behavior and avoid the sequential queries.
app/components/ReferralDashboard.tsx (1)

24-24: Consider using the ReferralData type instead of any.

The referralData state is typed as any | null, but ReferralData is already imported in the codebase and matches the API response structure.

♻️ Proposed fix for type safety
+import type { ReferralData } from "../types";
+
 export const ReferralDashboard = ({
     isOpen,
     onClose,
 }: {
     isOpen: boolean;
     onClose: () => void;
 }) => {
     const { getAccessToken } = usePrivy();
-    const [referralData, setReferralData] = useState<any | null>(null);
+    const [referralData, setReferralData] = useState<ReferralData | null>(null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/components/ReferralDashboard.tsx` at line 24, The state referralData is
currently typed as any | null; update its type to use the existing ReferralData
type for better type safety by changing the useState declaration for
referralData (and setReferralData) to ReferralData | null and adjust any places
consuming referralData to satisfy that stronger type (ensuring functions
expecting ReferralData still accept the state or are null-guarded).
app/api/referral/submit/route.ts (1)

67-79: Consider logging the validation failure for invalid format codes.

Format validation rejects codes that don't match ^NB[A-Z0-9]{4}$, but unlike the "missing code" case (lines 50-56), there's no trackApiError call. This makes it harder to detect if users are confused about the format or if there's a UI issue passing malformed codes.

📊 Proposed fix to add tracking
         // Validate code format (6 characters, starts with NB)
         if (!/^NB[A-Z0-9]{4}$/.test(normalizedCode)) {
+            trackApiError(
+                request,
+                "/api/referral/submit",
+                "POST",
+                new Error("Invalid referral code format"),
+                400
+            );
             return NextResponse.json(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/referral/submit/route.ts` around lines 67 - 79, Add a tracking call
when a referral code fails the format regex: in the same route handler where
normalizedCode is computed and the regex /^NB[A-Z0-9]{4}$/ is tested, call
trackApiError (the same helper used for the "missing code" branch) before
returning the 400 NextResponse.json. Include the invalid normalizedCode and an
error code like "INVALID_REFERRAL_FORMAT" in the tracking payload so malformed
submissions are logged consistently with the missing-code case.
app/api/referral/claim/route.ts (1)

139-155: KYC service failure returns generic 500 error.

Per context snippet 1, fetchKYCStatus throws on network errors. The catch block (lines 190-202) returns a generic "VERIFICATION_ERROR" with status 500. Users won't know if the issue is with their KYC status or a temporary service outage.

Consider catching KYC-specific errors separately to provide better user feedback (e.g., "KYC service temporarily unavailable, please try again").

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

In `@app/api/referral/claim/route.ts` around lines 139 - 155, The KYC check should
distinguish service/network failures from user verification failures: wrap the
fetchKYCStatus call (used to set callerKyc and callerVerified) in its own
try/catch and, on network/remote-service errors thrown by fetchKYCStatus, return
a specific NextResponse.json with a 503 (or appropriate 5xx) status and a code
like "KYC_SERVICE_UNAVAILABLE" and message such as "KYC service temporarily
unavailable, please try again"; keep the existing 400 response for
callerVerified === false and let the outer catch still handle other unexpected
errors.
app/api/aggregator.ts (1)

815-828: The walletAddress parameter is unused by the backend endpoint.

Per the referral-data/route.ts implementation (context snippet 1, lines 13-33), the endpoint always uses the authenticated user's wallet address extracted from the x-user-id header via getSmartWalletAddressFromPrivyUserId(userId). It does not read any wallet_address query parameter from the request.

The walletAddress parameter and the conditional URL construction on lines 826-828 are dead code that may mislead future maintainers into thinking they can fetch referral data for arbitrary wallets.

♻️ Proposed fix to remove unused parameter
 export async function getReferralData(
   accessToken: string,
-  walletAddress?: string,
 ): Promise<ApiResponse<ReferralData>> {
   if (!accessToken) {
     return {
       success: false,
       error: "Authentication token is required",
     };
   }

-  const url = walletAddress
-    ? `/api/referral/referral-data?wallet_address=${encodeURIComponent(walletAddress)}`
-    : `/api/referral/referral-data`;
+  const url = `/api/referral/referral-data`;

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

In `@app/api/aggregator.ts` around lines 815 - 828, The getReferralData function
currently accepts a walletAddress and builds a conditional URL, but the backend
endpoint (referral-data/route.ts) ignores any query param and always uses the
authenticated user's wallet via the x-user-id header and
getSmartWalletAddressFromPrivyUserId; remove the unused walletAddress parameter
and the conditional URL logic in getReferralData so it always requests
`/api/referral/referral-data`, update the function signature (remove
walletAddress), adjust all call sites to stop passing a walletAddress, and
remove any encodeURIComponent usage tied to this param to avoid dead code and
confusion.
🤖 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/api/referral/claim/route.ts`:
- Line 34: The call in route.ts uses an undefined function
getWalletAddressFromPrivyUserId causing a runtime ReferenceError; update the
code to call the correctly imported function
getSmartWalletAddressFromPrivyUserId (or adjust the import to match the intended
function) where walletAddress is assigned so the symbol names match (e.g.,
replace getWalletAddressFromPrivyUserId(userId) with
getSmartWalletAddressFromPrivyUserId(userId) or import
getWalletAddressFromPrivyUserId if that was intended).
- Around line 349-369: walletClient.writeContract currently writes and
immediately returns txHash which is used to mark the claim completed; change the
flow to call publicClient.waitForTransactionReceipt(txHash) after
walletClient.writeContract and only update referral_claims to "completed" when
the receipt shows success (e.g., status === 1); if the receipt indicates failure
or times out, do not mark completed—update the claim to a failure status or log
for manual review using the same supabaseAdmin.update call (referencing
pendingClaim.id and txHash) and ensure errors from waitForTransactionReceipt are
caught and handled.

In `@app/api/referral/referral-data/route.ts`:
- Around line 154-175: The merged referrals array allReferrals currently loses
perspective info, so update the mapping for entries created from
referralsAsReferrer and referralsAsReferred to include a role field (e.g., role:
"referrer" for items from referralsAsReferrer and role: "referred" for items
from referralsAsReferred) so the UI can distinguish relationship direction;
modify the two .map callbacks that build objects (referencing
referralsAsReferrer and referralsAsReferred) to add this role property while
keeping existing fields like id, wallet_address, wallet_address_short, status,
amount, created_at, and completed_at.

In `@app/components/NetworkSelectionModal.tsx`:
- Around line 57-71: handleClose no longer calls markNetworkModalDismissed(),
which prevents the reactive signal used by useIsNetworkModalDismissed() in
MigrationContext from updating; restore the store update by invoking
markNetworkModalDismissed() inside handleClose (before or after setting
localStorage and setIsOpen(false)) so dismissedViaStore flips immediately, and
keep the existing onNetworkSelected timeout behavior unchanged.
- Line 39: The storage key in NetworkSelectionModal is using the raw
user.wallet.address which is inconsistent with the rest of the app; update the
storageKey construction to use user.wallet.address.toLowerCase() and ensure the
modal's handleClose logic (the function named handleClose in
NetworkSelectionModal) reads/writes/removes the same lowercase key so dismissal
and session cleanup match other components that expect lowercase keys.

In `@app/components/ReferralDashboard.tsx`:
- Around line 40-44: The API success branch currently sets referral state but
when response.success is false it only calls setReferralData(null) without user
feedback; update the conditional in ReferralDashboard.tsx (the block checking
mounted && response.success) to call the same toast error used in the catch
block (e.g., toast.error(...)) when mounted && !response.success, passing the
response error/message (fallback to a generic message) and still
setReferralData(null); this keeps UI behavior consistent with the existing catch
handler.

In `@app/components/ReferralModal.tsx`:
- Around line 101-103: Update the referral copy in the ReferralModal component
so the threshold matches the design (change "$100" to "$20"); locate the
paragraph inside ReferralModal.tsx that renders the sentence containing
sponsorChain (the <p className="text-sm text-text-secondary dark:text-white/50">
node) and replace the hardcoded "$100" text with "$20" (or better, use a shared
constant/prop if one exists for referralThreshold to avoid future mismatches).

In `@app/components/wallet-mobile-modal/ReferralDashboardView.tsx`:
- Around line 274-276: In ReferralDashboardView.tsx the JSX directly calls
referral.amount.toFixed(1) which will throw if referral.amount is
undefined/non-numeric; update the rendering to defensively check referral.amount
(e.g., typeof referral.amount === 'number' && Number.isFinite(referral.amount'))
and only call toFixed when valid, otherwise render a safe fallback such as "—"
or "0.0 USDC"; adjust the <p> that currently contains
{referral.amount.toFixed(1)} USDC to use this conditional formatting so
referral.amount is never invoked with toFixed on an invalid value.

In `@app/components/WalletDetails.tsx`:
- Around line 373-384: The CNGN USD calculation currently uses (rate || 1) which
hides missing rates and misrepresents USD; update the conditional rendering in
WalletDetails (the token, balance and rate variables) so that when
token.toUpperCase() === "CNGN" and rate is null/undefined you render a
placeholder or the existing rateError message instead of performing
(balance)/(rate||1); only compute (balance/rate).toFixed(2) when rate is a valid
number, otherwise show a clear fallback like "—" or rateError.

In `@app/utils.ts`:
- Around line 1295-1318: Both handleCopyCode and handleCopyLink must handle
clipboard errors: convert them to async functions and wrap the
navigator.clipboard.writeText(...) call in a try-catch. In handleCopyCode, await
writeText, keep the onCopied(true) / setTimeout(... false) on success, and in
the catch call onCopied(false) if provided and surface the error (e.g.,
toast.error("Failed to copy") or similar). In handleCopyLink, await writeText
and on success call toast.success as now; in the catch call toast.error with a
clear message. This ensures failures (permission/HTTPS unsupported) are handled
gracefully.

---

Nitpick comments:
In `@app/api/aggregator.ts`:
- Around line 815-828: The getReferralData function currently accepts a
walletAddress and builds a conditional URL, but the backend endpoint
(referral-data/route.ts) ignores any query param and always uses the
authenticated user's wallet via the x-user-id header and
getSmartWalletAddressFromPrivyUserId; remove the unused walletAddress parameter
and the conditional URL logic in getReferralData so it always requests
`/api/referral/referral-data`, update the function signature (remove
walletAddress), adjust all call sites to stop passing a walletAddress, and
remove any encodeURIComponent usage tied to this param to avoid dead code and
confusion.

In `@app/api/referral/claim/route.ts`:
- Around line 139-155: The KYC check should distinguish service/network failures
from user verification failures: wrap the fetchKYCStatus call (used to set
callerKyc and callerVerified) in its own try/catch and, on
network/remote-service errors thrown by fetchKYCStatus, return a specific
NextResponse.json with a 503 (or appropriate 5xx) status and a code like
"KYC_SERVICE_UNAVAILABLE" and message such as "KYC service temporarily
unavailable, please try again"; keep the existing 400 response for
callerVerified === false and let the outer catch still handle other unexpected
errors.

In `@app/api/referral/referral-data/route.ts`:
- Around line 60-77: The current loop in route.ts that calls
generateReferralCode() and does sequential uniqueness checks against
supabaseAdmin.from("users").select(...).eq("referral_code", ...) should be
replaced with an atomic approach to avoid many round-trips under contention:
either (A) attempt an INSERT into the users/referral table (or a dedicated
referrals table) with the generated code using Postgres' INSERT ... ON CONFLICT
DO NOTHING and check the insert result to know if the code was accepted, or (B)
generate N candidates via generateReferralCode() and issue a single
supabaseAdmin.from("users").select("referral_code").in("referral_code",
candidates) to filter out collisions and then pick a remaining candidate;
implement this logic inside the same function that currently houses the while
loop so callers of generateReferralCode/route.ts see the new atomic/batched
behavior and avoid the sequential queries.

In `@app/api/referral/submit/route.ts`:
- Around line 67-79: Add a tracking call when a referral code fails the format
regex: in the same route handler where normalizedCode is computed and the regex
/^NB[A-Z0-9]{4}$/ is tested, call trackApiError (the same helper used for the
"missing code" branch) before returning the 400 NextResponse.json. Include the
invalid normalizedCode and an error code like "INVALID_REFERRAL_FORMAT" in the
tracking payload so malformed submissions are logged consistently with the
missing-code case.

In `@app/components/FundWalletForm.tsx`:
- Around line 23-32: Replace the duplicated local type alias MobileView with the
imported MobileSheetView: remove the local MobileView declaration and update any
usages of MobileView in this file (e.g., props, state, handlers) to use
MobileSheetView instead so the component relies on the shared MobileSheetView
type; ensure the import { MobileSheetView } remains used and run type checks to
confirm no remaining references to MobileView.

In `@app/components/MainPageContent.tsx`:
- Around line 199-212: handleNetworkSelected currently returns early when
authenticated is true but user.wallet.address is missing, causing the referral
modal to be skipped; update this callback to explicitly null-check user.wallet
and user.wallet.address and defer or enqueue showing the modal until the wallet
address becomes available instead of silently returning. Specifically, inside
handleNetworkSelected check for user and user.wallet first, compute
referralStorageKey using user.wallet.address only when present, and if
authenticated but address is not yet present either (a) store a temporary
flag/state to attempt showing the referral modal once user.wallet.address is
populated (use an effect that watches user?.wallet?.address) or (b
setShowReferralModal after the address appears); keep references to
referralStorageKey, localStorage.getItem, and setShowReferralModal when
implementing the fix.

In `@app/components/MobileDropdown.tsx`:
- Around line 48-55: Replace the inline union type on the currentView state with
the imported MobileSheetView type: change useState<...>("wallet") to
useState<MobileSheetView>("wallet") and ensure the imported MobileSheetView
symbol is used (and remove the inline union) so currentView and setCurrentView
rely on the canonical MobileSheetView type.

In `@app/components/NetworkSelectionModal.tsx`:
- Around line 49-55: Remove the dead/commented-out handleClose function: delete
the commented block containing handleClose and its internals (references to
user?.wallet?.address, storageKey =
`hasSeenNetworkModal-${user.wallet.address}`, localStorage.setItem, and
setIsOpen(false)) so the file no longer contains the unused commented code and
noise.

In `@app/components/ReferralCTA.tsx`:
- Around line 5-12: Remove the unused useRouter import and simplify the click
handler in ReferralCTA: delete the import of useRouter, remove the const router
= useRouter() line, and replace the handleViewReferrals implementation so it
directly calls the required prop (e.g., const handleViewReferrals = () =>
onViewReferrals();). Update any button onClick to use handleViewReferrals if not
already.

In `@app/components/ReferralDashboard.tsx`:
- Line 24: The state referralData is currently typed as any | null; update its
type to use the existing ReferralData type for better type safety by changing
the useState declaration for referralData (and setReferralData) to ReferralData
| null and adjust any places consuming referralData to satisfy that stronger
type (ensuring functions expecting ReferralData still accept the state or are
null-guarded).

In `@app/components/ReferralModal.tsx`:
- Around line 39-64: The nested try-catch is unnecessary: remove the inner
try/catch around submitReferralCode and instead use a single try/catch that
covers getAccessToken() and submitReferralCode(code, token), validate the code
with the existing regex (referralCode.trim().toUpperCase()), then check the
returned ApiResponse (res && res.success) to call toast.success +
onSubmitSuccess() + onClose() or toast.error with res.error/default message; let
the outer catch handle any unexpected thrown errors from getAccessToken or
submitReferralCode and surface a generic toast.error.

In `@app/components/TransferForm.tsx`:
- Around line 30-36: The file defines a local MobileView type but also imports
MobileSheetView which is unused and the local type is missing the "referrals"
variant; either remove the unused import and add "referrals" to the MobileView
union, or (preferably) delete the local MobileView and replace its uses with the
shared MobileSheetView type so the component consistently uses MobileSheetView
(update any references in TransferForm.tsx to MobileView → MobileSheetView and
remove the unused import or local type accordingly).

In `@app/components/wallet-mobile-modal/ReferralDashboardView.tsx`:
- Around line 71-73: Define proper TypeScript interfaces (e.g., Referral and
ReferralData) and replace the any types used for referralData and
filteredReferrals in ReferralDashboardView.tsx: type the referralData
variable/state/prop as ReferralData | undefined and change filteredReferrals to
Referral[] by updating the expression that computes it (the filteredReferrals
declaration and any place that consumes referralData.referrals). Also update any
function signatures or props that pass referral data (e.g., props or hooks that
return referralData) to use the new types so the component benefits from
compile-time checking.
🪄 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: 2e881261-330c-4ae8-8e21-8730f29fe57e

📥 Commits

Reviewing files that changed from the base of the PR and between c7dfdae and f657c94.

⛔ Files ignored due to path filters (11)
  • public/images/avatar/Avatar.png is excluded by !**/*.png
  • public/images/avatar/Avatar1.png is excluded by !**/*.png
  • public/images/avatar/Avatar2.png is excluded by !**/*.png
  • public/images/avatar/Avatar3.png is excluded by !**/*.png
  • public/images/avatar/Avatar4.png is excluded by !**/*.png
  • public/images/avatar/Avatar5.png is excluded by !**/*.png
  • public/images/avatar/Avatar6.png is excluded by !**/*.png
  • public/images/avatar/Avatar7.png is excluded by !**/*.png
  • public/images/referral-cta-dollar.png is excluded by !**/*.png
  • public/images/referral-cta.png is excluded by !**/*.png
  • public/images/referral-graphic.png is excluded by !**/*.png
📒 Files selected for processing (22)
  • app/api/aggregator.ts
  • app/api/referral/claim/route.ts
  • app/api/referral/referral-data/route.ts
  • app/api/referral/submit/route.ts
  • app/components/FundWalletForm.tsx
  • app/components/MainPageContent.tsx
  • app/components/MobileDropdown.tsx
  • app/components/NetworkSelectionModal.tsx
  • app/components/ReferralCTA.tsx
  • app/components/ReferralDashboard.tsx
  • app/components/ReferralDashboardSkeleton.tsx
  • app/components/ReferralDashboardViewSkeleton.tsx
  • app/components/ReferralModal.tsx
  • app/components/TransferForm.tsx
  • app/components/WalletDetails.tsx
  • app/components/index.ts
  • app/components/wallet-mobile-modal/ReferralDashboardView.tsx
  • app/components/wallet-mobile-modal/WalletView.tsx
  • app/components/wallet-mobile-modal/index.ts
  • app/types.ts
  • app/utils.ts
  • middleware.ts

Comment thread app/api/referral/claim/route.ts Outdated
Comment thread app/api/referral/claim/route.ts Outdated
Comment thread app/api/referral/referral-data/route.ts
Comment thread app/components/NetworkSelectionModal.tsx
Comment thread app/components/NetworkSelectionModal.tsx
Comment thread app/components/ReferralDashboard.tsx
Comment thread app/components/ReferralModal.tsx
Comment thread app/components/wallet-mobile-modal/ReferralDashboardView.tsx
Comment thread app/components/WalletDetails.tsx Outdated
Comment thread app/utils.ts
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: 6

♻️ Duplicate comments (1)
app/utils.ts (1)

1295-1327: ⚠️ Potential issue | 🟡 Minor

Await clipboard writes before reporting success.

navigator.clipboard.writeText() is async. The current try/catch only handles synchronous failures, so rejected writes still mark the code as copied or fire the success toast. Make both helpers wait for the clipboard promise before updating UI state.

Proposed fix
 export const handleCopyCode = (
   referralCode: string | undefined,
   onCopied?: (value: boolean) => void
 ): void => {
   if (referralCode) {
-    try {
-      navigator.clipboard.writeText(referralCode);
-      if (onCopied) {
-        onCopied(true);
-        setTimeout(() => onCopied(false), 2000);
-      }
-    } catch (error) {
-      console.error("Failed to copy referral code:", error);
-    }
+    void navigator.clipboard.writeText(referralCode)
+      .then(() => {
+        onCopied?.(true);
+        setTimeout(() => onCopied?.(false), 2000);
+      })
+      .catch((error) => {
+        onCopied?.(false);
+        console.error("Failed to copy referral code:", error);
+        toast.error("Failed to copy referral code");
+      });
   }
 };
 
 export const handleCopyLink = (referralCode: string | undefined): void => {
   if (referralCode) {
     const link = `${window.location.origin}?ref=${referralCode}`;
-    try {
-      navigator.clipboard.writeText(link);
-      toast.success("Referral link copied!");
-    } catch (error) {
-      console.error("Failed to copy referral link:", error);
-      toast.error("Failed to copy link");
-    }
+    void navigator.clipboard.writeText(link)
+      .then(() => {
+        toast.success("Referral link copied!");
+      })
+      .catch((error) => {
+        console.error("Failed to copy referral link:", error);
+        toast.error("Failed to copy link");
+      });
   }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/utils.ts` around lines 1295 - 1327, The clipboard write calls in
handleCopyCode and handleCopyLink are not awaited so rejected promises still
trigger success UI; make both functions async (returning Promise<void>), await
navigator.clipboard.writeText(...) inside their existing try/catch blocks, and
only call onCopied(true)/setTimeout(...) and toast.success(...) after the
awaited call succeeds; keep the catch to log the error and show toast.error or
call onCopied(false) as appropriate.
🤖 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/api/referral/claim/route.ts`:
- Around line 89-90: Change the status filter to only accept already-earned
referrals by removing "pending" from the .in("status", ["pending","earned"])
calls (search for that exact expression) so claims cannot be made against
pending referrals; ensure the volume/KYC check uses the referred user's identity
(not the caller/referrer) when evaluating the qualifying transaction threshold;
replace any hard-coded $100 threshold with the $20/first-qualifying-transaction
rule (search for numeric literal 100 used in referral checks); and only flip the
parent referral to "earned" (the code that updates status to "earned") after the
payout completes, not before.
- Around line 204-209: The current check using existingClaim (from
supabaseAdmin.from("referral_claims").select(...).single()) is race-prone; add a
unique DB constraint on (referral_id, wallet_address) and change the code that
creates claims to perform a single atomic upsert/insert-with-conflict-handling
instead of separate read+insert. Specifically, add the unique constraint in your
migrations for referral_claims, replace the read-then-create pattern that
populates existingClaim with an insert/upsert call (using Supabase/Postgres ON
CONFLICT or Supabase .upsert/.insert(...).onConflict equivalent) that returns
the existing row on conflict, and in the route handler check the returned
row/error to decide whether to call writeContract; also defensively catch a
duplicate-key DB error (e.g. Postgres 23505) and treat it as “already claimed”
to prevent double payouts (apply same change to the other create path referenced
around lines 238-247).
- Around line 225-236: The current early-return uses existingClaim to
short-circuit all statuses; change the logic so only existingClaim.status ===
"completed" returns immediately (keep the NextResponse.json shape with amount:
existingClaim.reward_amount, status, txHash: existingClaim.tx_hash), and for
statuses "pending" or "failed" do NOT return—allow the handler to continue to
attempt retry/recovery (e.g., re-validate wallet/balance and update or recreate
the claim). Apply the same change to the other similar blocks that check
existingClaim and return (the other occurrences handling status checks and
NextResponse.json) so only "completed" is terminal and transient
"pending"/"failed" flows can proceed.

In `@app/api/referral/referral-data/route.ts`:
- Around line 156-175: The mapping that builds referral DTOs is using the
boolean-or operator (e.g., r.reward_amount || 1.0 and r.amount || 0) which
treats an explicit 0 as missing; change those to nullish coalescing (use ??) so
explicit zero reward values are preserved (update occurrences in the
referralsAsReferrer and referralsAsReferred mapping blocks and the later mapping
that uses r.amount). Ensure the default fallback values remain the same (1.0 or
0) but use r.reward_amount ?? 1.0 and r.amount ?? 0 (or equivalent) so zero is
not overridden.
- Around line 54-109: The referral-code generation is race-prone; add a DB-level
unique constraint/index on users.referral_code and replace the preflight
select+check flow in the block using generateReferralCode() with a retrying
upsert that attempts to write the generated code and, on unique-constraint
conflict from supabaseAdmin.from("users").upsert (handle the Postgres
unique-violation error returned by Supabase), regenerates and retries up to
maxAttempts before failing; keep the upsert target (wallet_address) and
updated_at logic, set isNewlyGenerated only after a successful upsert returning
referral_code, and remove the separate .select(...) existence check to rely on
DB enforcement.

In `@app/components/ReferralDashboard.tsx`:
- Around line 34-39: In fetchData (ReferralDashboard) handle the case when
getAccessToken() returns null instead of early-returning: set referralData to
null/empty and set an auth error state (or call the existing error handler)
before letting the finally block clear loading so the UI can show an auth error
rather than the empty-dashboard UI; apply the same change to the analogous
branch in wallet-mobile-modal/ReferralDashboardView.tsx. Specifically, update
the token-null branch in fetchData and the corresponding place that calls
getAccessToken() to call setReferralData(null) or setAuthError(...) and avoid
silently returning while leaving prior referral state in place.

---

Duplicate comments:
In `@app/utils.ts`:
- Around line 1295-1327: The clipboard write calls in handleCopyCode and
handleCopyLink are not awaited so rejected promises still trigger success UI;
make both functions async (returning Promise<void>), await
navigator.clipboard.writeText(...) inside their existing try/catch blocks, and
only call onCopied(true)/setTimeout(...) and toast.success(...) after the
awaited call succeeds; keep the catch to log the error and show toast.error or
call onCopied(false) as appropriate.
🪄 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: b29fb6cf-f3eb-4040-95c8-8f4e5e92f363

📥 Commits

Reviewing files that changed from the base of the PR and between f657c94 and 64b0e65.

📒 Files selected for processing (7)
  • app/api/referral/claim/route.ts
  • app/api/referral/referral-data/route.ts
  • app/components/ReferralDashboard.tsx
  • app/components/TransferForm.tsx
  • app/components/wallet-mobile-modal/ReferralDashboardView.tsx
  • app/types.ts
  • app/utils.ts

Comment thread app/api/referral/claim/route.ts Outdated
Comment thread app/api/referral/claim/route.ts Outdated
Comment thread app/api/referral/claim/route.ts Outdated
Comment thread app/api/referral/referral-data/route.ts
Comment thread app/api/referral/referral-data/route.ts
Comment thread app/components/ReferralDashboard.tsx
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: 3

♻️ Duplicate comments (4)
app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1)

214-217: ⚠️ Potential issue | 🟠 Major

Keep the reward-threshold copy consistent here too.

This duplicate string still says $100 even though the backend and the PR contract are $20. It should be updated together with app/components/ReferralDashboard.tsx so desktop and mobile agree.

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

In `@app/components/wallet-mobile-modal/ReferralDashboardView.tsx` around lines
214 - 217, Update the reward-threshold copy in the ReferralDashboardView
component so it matches the backend/PR contract ($20) and stays consistent with
the desktop component ReferralDashboard; locate the paragraph string inside
ReferralDashboardView (the <p> with className "mb-6 text-sm leading-relaxed
text-text-secondary dark:text-white/60") and change the "$100" mention to "$20"
to mirror the copy used in app/components/ReferralDashboard.tsx.
app/api/referral/claim/route.ts (2)

179-195: ⚠️ Potential issue | 🔴 Critical

This rewards aggregate volume, not the promised first qualifying transaction.

The current check sums every completed transaction on the qualifying wallet. That lets multiple smaller transfers satisfy the rule, even though the acceptance criteria here are a first qualifying $20 transaction. Eligibility needs to be driven by a single qualifying transaction, not lifetime aggregate volume.

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

In `@app/api/referral/claim/route.ts` around lines 179 - 195, The code currently
sums all completed transactions (volTxs) to compute totalUsd, but the rule
requires checking for a single first qualifying transaction >=
MIN_QUALIFYING_VOLUME_USD; replace the aggregation with logic that finds the
earliest completed transaction for qualifyingWallet whose amount_usd or
amount_received is >= MIN_QUALIFYING_VOLUME_USD (e.g., query/select created_at
and order by created_at asc or filter in SQL), then set meetsVolume to true only
if that single qualifying transaction exists (and use that transaction’s amount
for any further checks) instead of summing volTxs; keep the existing error
handling around volTxError and reference volTxs, volTxError, qualifyingWallet,
and MIN_QUALIFYING_VOLUME_USD when making the change.

264-379: ⚠️ Potential issue | 🔴 Critical

A single pending claim can still broadcast twice.

Every path that finds or creates a pending row proceeds straight to writeContract. A concurrent request can reuse the same pendingClaim, and any failure after broadcast drops back to failed, so a retry can resend USDC even if the first transfer is already in flight or mined. This needs an atomic pending -> processing handoff and to persist tx_hash as soon as the broadcast succeeds; otherwise the claim table is not a real idempotency barrier.

Also applies to: 471-550

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

In `@app/api/referral/claim/route.ts` around lines 264 - 379, The current flow
allows two concurrent requests to grab the same pendingClaimRow and both call
writeContract; fix by making the handoff atomic: when you find/insert a pending
row (variable pendingClaimRow) immediately perform an update on referral_claims
that sets status = "processing" (or similar) with a conditional WHERE id =
pendingClaimRow.id AND status = "pending" (so the update only succeeds for the
first request) and use the returned row to decide whether to proceed to
writeContract; after broadcasting, persist tx_hash and set status = "completed"
(or set status back to "failed" on error) via another update, and apply the same
atomic conditional update pattern for the other duplicate handling branch that
also proceeds to writeContract.
app/api/referral/referral-data/route.ts (1)

56-99: ⚠️ Potential issue | 🟠 Major

Code auto-generation can still overwrite itself on first load.

Two concurrent first opens for the same wallet can both enter this block and both upsert(..., { onConflict: "wallet_address" }). The later write replaces the earlier code, so one client can display or copy a referral code that no longer exists in users.referral_code. This still needs a database-enforced unique users.referral_code plus a write path that only fills a missing code instead of overwriting an existing one.

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

In `@app/api/referral/referral-data/route.ts` around lines 56 - 99, Add a
DB-constraint and make the write idempotent: ensure users.referral_code has a
UNIQUE constraint in the database, then stop using upsert to assign the
generated code; instead attempt to atomically set the code only when it is
currently missing (e.g., an UPDATE on users where wallet_address = walletAddress
AND referral_code IS NULL, returning the referral_code) and retry on
unique-violation errors (generateReferralCode + attempted UPDATE) until success
or attempts exhausted; reference the generateReferralCode helper, the
supabaseAdmin write path (replace the upsert(...) onConflict logic), and the
users.referral_code and wallet_address columns when making these 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/api/referral/claim/route.ts`:
- Around line 99-104: The referral lookup is incorrectly filtering by
.eq("status", "pending") which makes a referral unclaimable after the first
payout; change the query that fetches the referral (the supabaseAdmin
.from("referrals") .select("*") .eq("id", referralId) call) to load the referral
regardless of status, then consult the referral_claims table to determine if the
current wallet has already been paid (use the existing referral_claims logic to
decide idempotency) and only flip the parent referral to "earned" when
appropriate; apply the same fix to the other occurrences referenced (the blocks
around lines 229-262 and 519-526) so all claim paths load referrals without the
status filter and rely on referral_claims for payout checks.

In `@app/api/referral/submit/route.ts`:
- Around line 81-97: Add a DB uniqueness constraint on
referrals.referred_wallet_address and stop relying on the preflight read;
instead, attempt the insert with supabaseAdmin.from("referrals").insert(...) and
handle the unique-conflict error as a 409 "You have already used a referral
code". Update the code paths that currently check existingReferral (the
preflight select and the identical check around lines with
existingReferral/existingError) to remove the race-prone read and catch the
insert error (Postgres unique-violation) for the same user; also apply the same
change to the other submit branch noted (around the second block 135-150) so
both create paths are atomic and return the same 409 on unique constraint
violation.

In `@app/components/ReferralDashboard.tsx`:
- Around line 209-212: Referral copy in ReferralDashboard.tsx and the duplicated
string in wallet-mobile-modal/ReferralDashboardView.tsx incorrectly says "$100"
instead of the backend/default "$20"; update the UI copy in both components
(ReferralDashboard and ReferralDashboardView) to read "$20" and, if feasible,
centralize the threshold text into a single constant or i18n key to avoid future
divergence (e.g., create a REFERRAL_THRESHOLD constant used by both components).

---

Duplicate comments:
In `@app/api/referral/claim/route.ts`:
- Around line 179-195: The code currently sums all completed transactions
(volTxs) to compute totalUsd, but the rule requires checking for a single first
qualifying transaction >= MIN_QUALIFYING_VOLUME_USD; replace the aggregation
with logic that finds the earliest completed transaction for qualifyingWallet
whose amount_usd or amount_received is >= MIN_QUALIFYING_VOLUME_USD (e.g.,
query/select created_at and order by created_at asc or filter in SQL), then set
meetsVolume to true only if that single qualifying transaction exists (and use
that transaction’s amount for any further checks) instead of summing volTxs;
keep the existing error handling around volTxError and reference volTxs,
volTxError, qualifyingWallet, and MIN_QUALIFYING_VOLUME_USD when making the
change.
- Around line 264-379: The current flow allows two concurrent requests to grab
the same pendingClaimRow and both call writeContract; fix by making the handoff
atomic: when you find/insert a pending row (variable pendingClaimRow)
immediately perform an update on referral_claims that sets status = "processing"
(or similar) with a conditional WHERE id = pendingClaimRow.id AND status =
"pending" (so the update only succeeds for the first request) and use the
returned row to decide whether to proceed to writeContract; after broadcasting,
persist tx_hash and set status = "completed" (or set status back to "failed" on
error) via another update, and apply the same atomic conditional update pattern
for the other duplicate handling branch that also proceeds to writeContract.

In `@app/api/referral/referral-data/route.ts`:
- Around line 56-99: Add a DB-constraint and make the write idempotent: ensure
users.referral_code has a UNIQUE constraint in the database, then stop using
upsert to assign the generated code; instead attempt to atomically set the code
only when it is currently missing (e.g., an UPDATE on users where wallet_address
= walletAddress AND referral_code IS NULL, returning the referral_code) and
retry on unique-violation errors (generateReferralCode + attempted UPDATE) until
success or attempts exhausted; reference the generateReferralCode helper, the
supabaseAdmin write path (replace the upsert(...) onConflict logic), and the
users.referral_code and wallet_address columns when making these changes.

In `@app/components/wallet-mobile-modal/ReferralDashboardView.tsx`:
- Around line 214-217: Update the reward-threshold copy in the
ReferralDashboardView component so it matches the backend/PR contract ($20) and
stays consistent with the desktop component ReferralDashboard; locate the
paragraph string inside ReferralDashboardView (the <p> with className "mb-6
text-sm leading-relaxed text-text-secondary dark:text-white/60") and change the
"$100" mention to "$20" to mirror the copy used in
app/components/ReferralDashboard.tsx.
🪄 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: 773bac82-c667-46bc-8e1a-a5f73e89d506

📥 Commits

Reviewing files that changed from the base of the PR and between 64b0e65 and 5d1c32f.

📒 Files selected for processing (6)
  • app/api/referral/claim/route.ts
  • app/api/referral/referral-data/route.ts
  • app/api/referral/submit/route.ts
  • app/components/ReferralDashboard.tsx
  • app/components/wallet-mobile-modal/ReferralDashboardView.tsx
  • app/lib/privy.ts

Comment thread app/api/referral/claim/route.ts Outdated
Comment thread app/api/referral/submit/route.ts Outdated
Comment thread app/components/ReferralDashboard.tsx
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: 3

🧹 Nitpick comments (2)
app/api/referral/referral-data/route.ts (1)

251-262: Consider forwarding only the Authorization header for background claim requests.

The background fetch manually sets x-user-id header (line 258). While the userId originates from the validated request, directly setting this header bypasses the normal authentication flow. The claim route's getPrivyUserIdFromRequest trusts x-user-id headers without re-validation when present.

If middleware doesn't run on this internal fetch (or fails silently), the manually-set x-user-id would be trusted directly. Relying solely on the Authorization header would ensure the claim route re-validates the user.

♻️ Proposed safer approach
         if (claimableReferrals.length > 0) {
             const authHeader = request.headers.get("Authorization");
+            if (!authHeader) {
+                // Skip auto-claim if no auth header to forward
+                console.warn("Auto-claim skipped: no Authorization header to forward");
+            } else {
             const origin = request.headers.get("origin") || `https://${request.headers.get("host")}`;
             fetch(`${origin}/api/referral/claim`, {
                 method: "GET",
                 headers: {
-                    ...(authHeader ? { Authorization: authHeader } : {}),
-                    "x-user-id": userId!,
+                    Authorization: authHeader,
                 },
             }).catch((e) =>
                 console.error("Auto-claim background request failed:", e),
             );
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/referral/referral-data/route.ts` around lines 251 - 262, The
background auto-claim fetch in the claimableReferrals handling should not set
x-user-id directly; update the fetch call inside route.ts (the block using
claimableReferrals and fetch(...)) to remove the "x-user-id": userId! header and
forward only the Authorization header (authHeader) so the claim route's
getPrivyUserIdFromRequest and normal auth middleware re-validate the user;
ensure nothing else depends on x-user-id being present for this internal
request.
app/api/referral/claim/route.ts (1)

266-274: Consider adding an explicit timeout to waitForTransactionReceipt to fail faster on issues.

While viem's waitForTransactionReceipt has a default timeout of 3 minutes (180,000 ms), explicitly setting a shorter timeout (e.g., 60 seconds) is a good practice for API requests to fail faster if the transaction gets stuck or the RPC node becomes unresponsive. This prevents lengthy request hangs and improves user experience.

⏱️ Proposed fix with timeout
-    const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 1 });
+    const receipt = await publicClient.waitForTransactionReceipt({ 
+      hash: txHash, 
+      confirmations: 1,
+      timeout: 60_000, // 60 second timeout
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/referral/claim/route.ts` around lines 266 - 274, Add an explicit
timeout to the viem waitForTransactionReceipt call (e.g., timeout: 60_000) so
the function fails faster if the RPC stalls; update the call to
publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 1,
timeout: 60000 }) and add a try/catch around it to handle timeout/errors,
ensuring you mark the referral_claims row for pendingClaim.id as failed via
supabaseAdmin.from("referral_claims").update({ status: "failed", updated_at: new
Date().toISOString() }).eq("id", pendingClaim.id) and return a failure response
(e.g., code "TRANSFER_TIMEOUT" or similar) when the wait times out or throws.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.env.example:
- Around line 143-145: The dotenv entries
NEXT_PUBLIC_REFERRAL_MIN_QUALIFYING_VOLUME_USD and
NEXT_PUBLIC_REFERRAL_REWARD_AMOUNT_USD currently have inline comments that may
be parsed as part of the value; move the comments to their own lines above each
variable (or remove inline trailing comments) so the variables remain plain
numeric values (e.g., place a line like "# in USDC" immediately above each
variable) to avoid ValueWithoutQuotes/parsing issues.

In `@app/api/referral/claim/route.ts`:
- Around line 54-62: Wrap the call to fetchKYCStatus(qualifyingWallet) in a
try-catch inside the route handler so network/API exceptions don't bubble up; in
the catch, log the error and return a structured response (e.g., { success:
false, code: "KYC_SERVICE_UNAVAILABLE", message: "Unable to verify KYC at this
time. Please try again later." }) instead of allowing an internal server error,
while keeping the existing verified-status check for kyc?.data?.status !==
"verified".

In `@app/lib/config.ts`:
- Around line 47-50: The two config values referralMinQualifyingVolumeUsd and
referralRewardAmountUsd are currently parsed with Number(...) and can become NaN
for invalid env values; update their parsing to mirror the other numeric configs
by using Number(...) then validate with Number.isFinite(...) and fall back to 0
(or the existing safe default) when not finite so these fields never end up as
NaN and comparisons behave correctly; modify the initialization for
referralMinQualifyingVolumeUsd and referralRewardAmountUsd to perform this check
and assign the safe default.

---

Nitpick comments:
In `@app/api/referral/claim/route.ts`:
- Around line 266-274: Add an explicit timeout to the viem
waitForTransactionReceipt call (e.g., timeout: 60_000) so the function fails
faster if the RPC stalls; update the call to
publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 1,
timeout: 60000 }) and add a try/catch around it to handle timeout/errors,
ensuring you mark the referral_claims row for pendingClaim.id as failed via
supabaseAdmin.from("referral_claims").update({ status: "failed", updated_at: new
Date().toISOString() }).eq("id", pendingClaim.id) and return a failure response
(e.g., code "TRANSFER_TIMEOUT" or similar) when the wait times out or throws.

In `@app/api/referral/referral-data/route.ts`:
- Around line 251-262: The background auto-claim fetch in the claimableReferrals
handling should not set x-user-id directly; update the fetch call inside
route.ts (the block using claimableReferrals and fetch(...)) to remove the
"x-user-id": userId! header and forward only the Authorization header
(authHeader) so the claim route's getPrivyUserIdFromRequest and normal auth
middleware re-validate the user; ensure nothing else depends on x-user-id being
present for this internal request.
🪄 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: d7af56ce-912a-4b5b-b405-03960af74336

📥 Commits

Reviewing files that changed from the base of the PR and between 5d1c32f and b30f253.

📒 Files selected for processing (5)
  • .env.example
  • app/api/referral/claim/route.ts
  • app/api/referral/referral-data/route.ts
  • app/lib/config.ts
  • app/types.ts

Comment thread .env.example
Comment thread app/api/referral/claim/route.ts
Comment thread app/lib/config.ts Outdated
@5ran6 5ran6 force-pushed the main branch 3 times, most recently from cac70dc to 447a769 Compare April 6, 2026 12:37
* Added new API routes for submitting and retrieving referral data.
* Introduced referral-related types and utility functions for handling referral codes.
* Created UI components for referral dashboard and call-to-action, enhancing user engagement.
* Updated mobile dropdown and main page content to include referral options and modals.
* Enhanced middleware to support new referral API routes.
* Added optional role property to referral data structure for better clarity.
* Improved clipboard copy functionality with error handling for referral codes and links.
* Updated referral data retrieval to include role information for referrers and referred users.
* Enhanced error handling in the ReferralDashboard and ReferralDashboardView components for better user feedback.
* Refactored referral API routes to utilize a new method for retrieving user IDs from requests.
* Updated referral data retrieval to ensure accurate wallet address handling.
* Enhanced error handling for referral code generation and transaction volume checks.
* Improved user feedback in the ReferralDashboard and ReferralDashboardView components for better user experience.
* Standardized handling of referral amounts to ensure consistency across the application.
… API

* Introduced new environment variables for minimum qualifying volume and reward amount in the referral system.
* Updated types to include referral configuration parameters.
* Refactored referral claim logic to utilize new configuration values for volume checks and reward distribution.
* Enhanced referral data retrieval to reflect user-specific claim statuses and auto-claim functionality for eligible referrals.
* Updated .env.example to better document referral program variables.
* Improved KYC status verification in the referral claim API with enhanced error handling for better user feedback.
* Refactored referral configuration values to ensure proper parsing and validation in the application.
- Renamed `getAvatarImage` to `getAvatarImageFromAddress` for clarity in its purpose.
- Updated all references to the renamed function across components, ensuring consistent usage.
- Added comments to improve code documentation and understanding of avatar image retrieval logic.
@Dprof-in-tech Dprof-in-tech force-pushed the feat/user-referral-and-earnings-program-clean branch from edfe6a1 to 866c809 Compare May 28, 2026 08:10
…heck

Co-authored-by: Cursor <cursoragent@cursor.com>
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: 4

♻️ Duplicate comments (2)
.env.example (1)

169-170: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Move inline comments off referral env assignments.

Trailing inline comments can be parsed as part of the value by some dotenv tooling; keep these values plain and put comments on separate lines.

Proposed fix
-NEXT_PUBLIC_REFERRAL_MIN_QUALIFYING_VOLUME_USD=100 # in USDC
-NEXT_PUBLIC_REFERRAL_REWARD_AMOUNT_USD=1 # in USDC
+# Referral minimum qualifying volume (in USDC)
+NEXT_PUBLIC_REFERRAL_MIN_QUALIFYING_VOLUME_USD=100
+# Referral reward amount (in USDC)
+NEXT_PUBLIC_REFERRAL_REWARD_AMOUNT_USD=1
🤖 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 @.env.example around lines 169 - 170, Remove the trailing inline comments
from the two env assignments so the dotenv values are plain; instead place
explanatory comments on their own lines above (or below) the entries for
NEXT_PUBLIC_REFERRAL_MIN_QUALIFYING_VOLUME_USD and
NEXT_PUBLIC_REFERRAL_REWARD_AMOUNT_USD to avoid the comment being parsed as part
of the value.
app/components/ReferralModal.tsx (1)

101-103: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update referral threshold copy to match the implemented program.

Line 102 still says “$100 transaction”, but this flow is defined as first $20 transaction in the PR objective/design.

🤖 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 `@app/components/ReferralModal.tsx` around lines 101 - 103, The referral copy
in the ReferralModal component still reads "$100 transaction" but the program is
for the first $20 transaction; update the JSX paragraph that uses sponsorChain
(the <p> containing "Enter your referral code below...") to replace "$100" with
"$20" so the displayed text matches the implemented flow. Ensure only the
numeric threshold is changed and keep sponsorChain interpolation intact.
🧹 Nitpick comments (1)
app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1)

27-27: ⚡ Quick win

Replace any type with proper ReferralData type.

The referralData state uses any, reducing type safety. The API aggregator returns ApiResponse<ReferralData>, so you should type this state accordingly.

♻️ Proposed fix
+import type { ReferralData } from "`@/app/types`";
+
 export const ReferralDashboardView = ({
     isOpen,
     onClose,
 }: {
     isOpen: boolean;
     onClose: () => void;
 }) => {
     const { getAccessToken } = usePrivy();
-    const [referralData, setReferralData] = useState<any | null>(null);
+    const [referralData, setReferralData] = useState<ReferralData | null>(null);

And update the filtered referrals type:

-    const filteredReferrals: any[] = (referralData?.referrals || []).filter(
+    const filteredReferrals = (referralData?.referrals || []).filter(
         (r: any) => r.status === activeTab
     );
🤖 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 `@app/components/wallet-mobile-modal/ReferralDashboardView.tsx` at line 27, The
state referralData is typed as any which weakens type safety; change its type to
ReferralData | null (and/or ApiResponse<ReferralData> | null if storing the raw
API response) and update setReferralData usage accordingly; also replace any
usages/filters that declare filtered referrals with proper types (e.g.,
ReferralData[] or Array<ReferralData>) so functions referencing referralData,
setReferralData, and the filtered referrals variable use the concrete
ReferralData type throughout.
🤖 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 `@app/components/MainPageContent.tsx`:
- Around line 111-117: The success toast is shown twice because
ReferralInputModal already displays a toast on submit; remove the duplicate
toast call passed via the onSubmitSuccess prop in MainPageContent by replacing
the inline toast.success callback with either a no-op or a different handler
that performs non-UI follow-up actions (e.g., state updates). Locate the
ReferralInputModal usage and delete the toast.success invocation from the
onSubmitSuccess prop, keeping only non-toast logic in that callback if needed so
ReferralModal.tsx remains the sole place showing the success notification.
- Around line 293-304: The referral modal is only triggered inside
handleNetworkSelected so users who never open the network modal miss the prompt;
add an independent eligibility check (based on authenticated and walletAddress)
that runs on mount and whenever authenticated or walletAddress changes to mirror
the same logic: compute referralStorageKey =
`hasSeenReferralModal-${walletAddress.toLowerCase()}`, read localStorage, and
call setShowReferralModal(true) when no flag exists; keep the existing
handleNetworkSelected but extract the shared eligibility logic into a reusable
helper (e.g., isEligibleForReferral or showReferralIfEligible) and invoke it
from both handleNetworkSelected and a useEffect that depends on [authenticated,
walletAddress].

In `@app/components/ReferralModal.tsx`:
- Line 136: In ReferralModal.tsx update the JSX text node "I don't have a
referral code" to avoid the unescaped apostrophe lint error; locate the string
in the ReferralModal component and replace it with a properly escaped version
(for example use {"I don't have a referral code"}, use the HTML entity &apos; or
a typographic apostrophe ’) so the JSX no longer triggers
react/no-unescaped-entities.

In `@app/components/wallet-mobile-modal/ReferralDashboardView.tsx`:
- Around line 219-220: The referral instruction text in the
ReferralDashboardView component is inconsistent: update the copy that currently
reads "$100 transaction" to "$20 transaction" so it matches the PR objectives
and other UI text; locate the string inside the ReferralDashboardView (the JSX
text block that starts "Earn when you refer your friends...") and change the
amount from 100 to 20.

---

Duplicate comments:
In @.env.example:
- Around line 169-170: Remove the trailing inline comments from the two env
assignments so the dotenv values are plain; instead place explanatory comments
on their own lines above (or below) the entries for
NEXT_PUBLIC_REFERRAL_MIN_QUALIFYING_VOLUME_USD and
NEXT_PUBLIC_REFERRAL_REWARD_AMOUNT_USD to avoid the comment being parsed as part
of the value.

In `@app/components/ReferralModal.tsx`:
- Around line 101-103: The referral copy in the ReferralModal component still
reads "$100 transaction" but the program is for the first $20 transaction;
update the JSX paragraph that uses sponsorChain (the <p> containing "Enter your
referral code below...") to replace "$100" with "$20" so the displayed text
matches the implemented flow. Ensure only the numeric threshold is changed and
keep sponsorChain interpolation intact.

---

Nitpick comments:
In `@app/components/wallet-mobile-modal/ReferralDashboardView.tsx`:
- Line 27: The state referralData is typed as any which weakens type safety;
change its type to ReferralData | null (and/or ApiResponse<ReferralData> | null
if storing the raw API response) and update setReferralData usage accordingly;
also replace any usages/filters that declare filtered referrals with proper
types (e.g., ReferralData[] or Array<ReferralData>) so functions referencing
referralData, setReferralData, and the filtered referrals variable use the
concrete ReferralData type throughout.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0680e9d5-9142-4601-9f19-9d737fcf3fa3

📥 Commits

Reviewing files that changed from the base of the PR and between b30f253 and fbca0a6.

⛔ Files ignored due to path filters (11)
  • public/images/avatar/Avatar.png is excluded by !**/*.png
  • public/images/avatar/Avatar1.png is excluded by !**/*.png
  • public/images/avatar/Avatar2.png is excluded by !**/*.png
  • public/images/avatar/Avatar3.png is excluded by !**/*.png
  • public/images/avatar/Avatar4.png is excluded by !**/*.png
  • public/images/avatar/Avatar5.png is excluded by !**/*.png
  • public/images/avatar/Avatar6.png is excluded by !**/*.png
  • public/images/avatar/Avatar7.png is excluded by !**/*.png
  • public/images/referral-cta-dollar.png is excluded by !**/*.png
  • public/images/referral-cta.png is excluded by !**/*.png
  • public/images/referral-graphic.png is excluded by !**/*.png
📒 Files selected for processing (24)
  • .env.example
  • app/api/aggregator.ts
  • app/api/referral/claim/route.ts
  • app/api/referral/referral-data/route.ts
  • app/api/referral/submit/route.ts
  • app/components/FundWalletForm.tsx
  • app/components/MainPageContent.tsx
  • app/components/MobileDropdown.tsx
  • app/components/NetworkSelectionModal.tsx
  • app/components/ReferralCTA.tsx
  • app/components/ReferralDashboard.tsx
  • app/components/ReferralDashboardSkeleton.tsx
  • app/components/ReferralDashboardViewSkeleton.tsx
  • app/components/ReferralModal.tsx
  • app/components/WalletDetails.tsx
  • app/components/index.ts
  • app/components/wallet-mobile-modal/ReferralDashboardView.tsx
  • app/components/wallet-mobile-modal/WalletView.tsx
  • app/components/wallet-mobile-modal/index.ts
  • app/lib/config.ts
  • app/lib/privy.ts
  • app/types.ts
  • app/utils.ts
  • middleware.ts
✅ Files skipped from review due to trivial changes (2)
  • app/components/wallet-mobile-modal/index.ts
  • app/components/ReferralDashboardSkeleton.tsx
🚧 Files skipped from review as they are similar to previous changes (16)
  • app/lib/config.ts
  • middleware.ts
  • app/lib/privy.ts
  • app/components/ReferralDashboardViewSkeleton.tsx
  • app/api/aggregator.ts
  • app/components/ReferralCTA.tsx
  • app/components/FundWalletForm.tsx
  • app/types.ts
  • app/components/MobileDropdown.tsx
  • app/components/index.ts
  • app/components/NetworkSelectionModal.tsx
  • app/api/referral/submit/route.ts
  • app/components/ReferralDashboard.tsx
  • app/api/referral/referral-data/route.ts
  • app/api/referral/claim/route.ts
  • app/utils.ts

Comment thread app/components/MainPageContent.tsx
Comment thread app/components/MainPageContent.tsx Outdated
Comment thread app/components/ReferralModal.tsx Outdated
Comment thread app/components/wallet-mobile-modal/ReferralDashboardView.tsx Outdated
Dprof-in-tech and others added 3 commits May 28, 2026 09:35
Remove duplicate submit toast, show referral prompt on login without
requiring network modal, fix unescaped apostrophe, and standardize
qualifying volume copy and env default to $20.

Co-authored-by: Cursor <cursoragent@cursor.com>
Remove race-prone preflight read, return 409 on referred_wallet_address
unique violations, and add a case-insensitive unique index migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

Implement User Referral & Earnings Program

2 participants