Skip to content

Conversation

@sundayonah
Copy link
Collaborator

@sundayonah sundayonah commented Nov 18, 2025

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 launched: generate codes, submit referral codes, claim rewards, and copy/share invite links.
    • Referral UI added: dashboard, modal input, CTA, and mobile/desktop integrations; network selection can trigger referral onboarding.
  • Chores

    • Server-side analytics tracking endpoint added and instrumented.
    • New environment variable for fee recipient address introduced.

✏️ Tip: You can customize this high-level summary in your review settings.

@sundayonah sundayonah requested a review from chibie as a code owner November 18, 2025 17:47
@coderabbitai
Copy link

coderabbitai bot commented Nov 18, 2025

Walkthrough

Adds a referral program: backend routes for submit/referral-data/claim, frontend components and modals for input and dashboard, utilities for code generation/copying, analytics tracking endpoint, middleware matcher updates, and new API aggregator helpers and types for referral flows.

Changes

Cohort / File(s) Summary
Backend Referral Routes
app/api/referral/submit/route.ts, app/api/referral/referral-data/route.ts, app/api/referral/claim/route.ts
New rate-limited POST/GET handlers implementing referral submission, referral-data retrieval (auto-generate code/upsert), and claim processing (KYC/volume checks, idempotency, USDC transfer via private key + viem, claim/referral state updates).
Analytics Endpoint & Middleware
app/api/v1/analytics/track/route.ts, middleware.ts
New server-side analytics POST endpoint (fire-and-forget tracking) and middleware matcher entries for /api/referral routes.
Aggregator API + Types
app/api/aggregator.ts, app/types.ts
New aggregator functions: submitReferralCode(), getReferralData(), generateReferralCode(); added ReferralData, ApiResponse<T>, and SubmitReferralResult types.
Frontend Referral UI (Desktop)
app/components/ReferralDashboard.tsx, app/components/ReferralModal.tsx, app/components/ReferralCTA.tsx, app/components/ReferralDashboardSkeleton.tsx, app/components/ReferralDashboardViewSkeleton.tsx
New Referral dashboard, input modal, CTA card, and skeleton components: fetch referral data, show totals/tabs, copy code/link UI, and handle submission flows with toasts and animations.
Frontend Referral UI (Mobile)
app/components/wallet-mobile-modal/ReferralDashboardView.tsx, app/components/wallet-mobile-modal/index.ts
Mobile drawer variant of referral dashboard, data fetching with Privy token, copy actions, tabs, and export.
Component Integration & UX wiring
app/components/MainPageContent.tsx, app/components/NetworkSelectionModal.tsx, app/components/WalletDetails.tsx, app/components/MobileDropdown.tsx, app/components/Navbar.tsx, app/components/wallet-mobile-modal/WalletView.tsx
Integrates referral flow into existing UI: network selection triggers modal, wallet sidebar shows CTA and opens dashboard, mobile dropdown and navbar propagate referral actions, added props for referral callbacks.
Exports / Indexes
app/components/index.ts, app/components/wallet-mobile-modal/index.ts
Re-exports for ReferralInputModal, ReferralCTA, and ReferralDashboardView.
Utilities
app/utils.ts
New helpers: generateReferralCode(), getAvatarImage(), handleCopyCode(), handleCopyLink().
Env / Config
.env.example
Added NEXT_PUBLIC_FEE_RECIPIENT_ADDRESS environment variable to example file.
Client API surface
app/api/aggregator.ts
Aggregator now imports/exports referral-related types and returns ApiResponse-wrapped results for new referral functions.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Frontend
    participant Privy
    participant API as /api/referral/submit
    participant DB as Supabase

    User->>Frontend: Enter referral code
    Frontend->>Frontend: Validate format
    Frontend->>Privy: (optional) get access token / headers
    Frontend->>API: POST submitReferralCode(code) with headers
    API->>API: Extract x-user-id, resolve wallet via Privy
    API->>DB: Ensure no existing referral for wallet
    DB-->>API: no duplicate
    API->>DB: Lookup referrer by code
    DB-->>API: referrer found
    API->>DB: Insert pending referral
    DB-->>API: referral_id
    API-->>Frontend: 201 + referral_id
    Frontend->>User: Show success toast
Loading
sequenceDiagram
    actor ReferredUser
    participant API as /api/referral/claim
    participant KYCService
    participant DB as Supabase
    participant Privy
    participant Chain as Blockchain (USDC)

    Note over API: Claim flow triggered after qualifying action
    ReferredUser->>API: POST claim (x-user-id)
    API->>API: Resolve wallet, fetch pending referral
    API->>KYCService: Verify KYC
    KYCService-->>API: verified
    API->>API: Check transaction volume >= $100
    API->>DB: Check/create referral_claims (idempotency)
    DB-->>API: no existing claim
    API->>Privy: Retrieve funding wallet private key
    Privy-->>API: private key
    API->>Chain: Derive address, check USDC balance
    Chain-->>API: balance ok
    API->>Chain: Execute USDC transfer -> user
    Chain-->>API: tx_hash
    API->>DB: Update referral_claims (completed + tx)
    API->>DB: Update referrals (earned)
    API-->>ReferredUser: 200 + tx_hash
Loading
sequenceDiagram
    actor Referrer
    participant Frontend
    participant Privy
    participant API as /api/referral/referral-data
    participant DB as Supabase

    Referrer->>Frontend: Open Referral Dashboard
    Frontend->>Privy: get access token
    Privy-->>Frontend: token
    Frontend->>API: GET referral-data (token, walletAddress)
    API->>API: Extract x-user-id, derive wallet
    API->>DB: Fetch or generate referral_code (upsert)
    API->>DB: Query referrals where referrer and where referred
    DB-->>API: referrals list
    API->>API: Compute totals and counts
    API-->>Frontend: ReferralData payload
    Frontend->>Referrer: Render dashboard UI (code, totals, list)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus review on:
    • app/api/referral/claim/route.ts — blockchain transfer, private key handling, viem setup, error classification, idempotency and Supabase updates.
    • app/api/referral/referral-data/route.ts — auto-generation/upsert of referral codes and aggregation logic.
    • Integration points in MainPageContent, NetworkSelectionModal, WalletDetails, MobileDropdown, Navbar — state/prop wiring and localStorage handling.
    • New types and ApiResponse usage in app/api/aggregator.ts to ensure callers handle returned unions correctly.

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • chibie
  • KEM-CONSOLATION

Poem

🐰
I hopped a code, NB and bright,
Shared with friends from day to night,
A little USDC for each new mate,
Tracks and copies—what a fate!
Hop on, referrals feel just right.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly references the main feature (referral system) with code generation and tracking, directly aligning with the changeset's primary focus.
Description check ✅ Passed The PR description provides comprehensive context covering new user flow, existing user flow, technical integrations, design references, testing evidence, and completed checklist items.
Linked Issues check ✅ Passed The implementation addresses all key objectives from #279: post-onboarding referral modal, $1 USDC reward crediting, referral dashboard with code/link copy, backend tracking service, and wallet integration for reward distribution.
Out of Scope Changes check ✅ Passed All changes are directly aligned with referral system requirements. Minor updates (middleware config, UI styling refinements) are necessary supporting changes for the referral feature integration.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/user-referral-and-earnings-program

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@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: 9

🧹 Nitpick comments (9)
.env.example (1)

55-61: Clarify variable scope and add documentation.

The new NEXT_PUBLIC_FEE_RECIPIENT_ADDRESS variable (line 61) has a client-side prefix (NEXT_PUBLIC_) but is placed under the "Server-side Analytics" section, creating classification inconsistency. Additionally, there's no inline documentation explaining its purpose, expected format (e.g., Ethereum address), or security implications.

Per the AI summary, this relates to the internal credit-wallet API and reward distribution, not analytics. Consider:

  1. Moving it to an appropriate section (e.g., under "External Services" or a new "Wallet & Transactions" section)
  2. Adding inline documentation explaining its role in the referral reward system
  3. Clarifying whether it should be NEXT_PUBLIC_ (exposed to client) or a server-only variable
app/components/ReferralModal.tsx (1)

31-64: Simplify handleSubmit error handling to a single clear path

handleSubmit currently has a nested try/catch, which makes the control flow harder to follow and splits error handling between two places without much benefit. You can keep behavior the same with a single validation block followed by one try/catch around token retrieval and submitReferralCode, which improves readability and future maintenance.

For example:

-    const handleSubmit = async () => {
-        if (!referralCode.trim()) {
-            toast.error("Please enter a referral code");
-            return;
-        }
-
-        setIsSubmitting(true);
-
-        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 payload = await submitReferralCode(code, token ?? undefined);
-                toast.success(payload?.data?.message || "Referral code applied! Complete KYC and your first transaction to earn rewards.");
-                onSubmitSuccess();
-                onClose();
-            } catch (err) {
-                const message = err instanceof Error ? err.message : "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."
-            );
-        } finally {
-            setIsSubmitting(false);
-        }
-    };
+    const handleSubmit = async () => {
+        const raw = referralCode.trim();
+        if (!raw) {
+            toast.error("Please enter a referral code");
+            return;
+        }
+
+        const code = raw.toUpperCase();
+        if (!/^NB[A-Z0-9]{4}$/.test(code)) {
+            toast.error("Invalid referral code format");
+            return;
+        }
+
+        setIsSubmitting(true);
+
+        try {
+            const token = await getAccessToken();
+            const payload = await submitReferralCode(code, token ?? undefined);
+            toast.success(
+                payload?.data?.message ||
+                    "Referral code applied! Complete KYC and your first transaction to earn rewards."
+            );
+            onSubmitSuccess();
+            onClose();
+        } catch (err) {
+            const message =
+                err instanceof Error
+                    ? err.message
+                    : "Failed to submit referral code. Please try again.";
+            toast.error(message);
+        } finally {
+            setIsSubmitting(false);
+        }
+    };

This keeps validation and submission paths clear while preserving the existing UX.

app/types.ts (1)

404-420: ReferralData shape looks good; consider documenting status/units

The ReferralData interface matches how the dashboard and API helpers expect to consume referral summaries (totals + detailed list) and is a solid base type.

If you want to harden it further, you could:

  • Introduce a ReferralStatus = "pending" | "earned" | "processing" | ... type and reuse it for referrals[].status.
  • Add a brief comment about units (e.g. “USDC in whole units, not micro‑units”) for total_earned, total_pending, and amount to avoid ambiguity with the micro‑denominated crediting path.
app/components/ReferralDashboard.tsx (1)

184-201: Strengthen referral item typing and avatar color fallback

Two small robustness improvements:

  • filteredReferrals and referralData are typed as any. Using ReferralData and a referral item type will catch shape mistakes at compile time:
-import { getReferralData } from "../api/aggregator";
+import { getReferralData } from "../api/aggregator";
+import type { ReferralData } from "../types";

-const [referralData, setReferralData] = useState<any | null>(null);
+const [referralData, setReferralData] = useState<ReferralData | null>(null);

-const filteredReferrals: any[] = (referralData?.referrals || []).filter(
-    (r: any) => r.status === activeTab
-);
+const filteredReferrals =
+  (referralData?.referrals ?? []).filter((r) => r.status === activeTab);
  • getAvatarColor assumes an 0x‑prefixed address and will produce NaN indices for unexpected values. Adding a simple guard keeps the className valid:
-    const index = parseInt(address.slice(2, 4), 16) % colors.length;
+    const hex = address.startsWith("0x") && address.length >= 4
+      ? address.slice(2, 4)
+      : "00";
+    const index = parseInt(hex, 16) % colors.length;

These are not blockers, but they make the UI more resilient and the types clearer.

Also applies to: 371-407

app/api/internal/credit-wallet/route.ts (1)

34-46: Validate credit amount and address assumptions for safety

Verification confirms the following points need addressing:

  • The amount validation (line 45) only checks typeof amount !== "number". Add explicit validation that amount is a non-negative integer, preventing accidental fractional or negative values:

    if (!Number.isInteger(amount) || amount <= 0) {
      return NextResponse.json(
        { success: false, error: "Amount must be a positive integer (micro‑units)" },
        { status: 400 }
      );
    }
  • TOKEN_DECIMALS is defined at line 109 but never used. Line 116 passes amount directly to BigNumber.from() without applying this conversion. Either remove the constant or use it to explicitly convert units so that contract calls and DB storage align.

  • No edge runtime override detected—the route remains NodeJS, which is correct given its use of ethers and Node-only APIs. ✓

Applies also to related amount handling logic at lines 61–74 and 105–121.

app/components/NetworkSelectionModal.tsx (1)

20-27: Clarify when onNetworkSelected should fire; currently it runs on every close

handleClose is used for all close paths (backdrop click, “Close” button, and the success callback from handleNetworkSwitch), but it always invokes onNetworkSelected if provided. That means the callback (and downstream referral flow) will also run when a user dismisses the modal without actually selecting a network.

If the intent is “run after a successful network selection,” consider either:

  • Tracking a hasSelectedNetwork flag and only calling onNetworkSelected when that’s true, or
  • Calling onNetworkSelected directly from handleNetworkSelect’s success path and keeping handleClose as a generic close handler, or
  • Renaming the prop to something like onNetworkModalClosed if the broader “closed at least once” behavior is actually what you want.

Also applies to: 57-71

app/components/MainPageContent.tsx (1)

1-521: Remove the large commented‑out legacy implementation

This file still contains a full copy of the previous MainPageContent and PageLayout implementation commented out at the top. That makes the file noisy and harder to maintain; Git history already preserves the old version.

I’d strongly suggest deleting this commented block so the file only contains the current implementation.

app/api/referral/referral-data/route.ts (1)

1-146: Referral data aggregation looks good; consider a small null-safety guard

The route is well-structured: auth via x-wallet-address, clear 401/404/500 handling, and a response shape that matches the dashboard’s needs (totals, counts, referral list) with proper analytics tracking.

One small defensive improvement: Supabase .select() can theoretically return data as null when something odd happens even if referralsError is falsy, which would make referrals.filter(...) and .map(...) throw. You could guard this by normalizing the value:

const { data: referralsRaw, error: referralsError } = await supabaseAdmin
  .from("referrals")
  .select(/* ... */)
  .eq("referrer_wallet_address", walletAddress)
  .order("created_at", { ascending: false });

if (referralsError) {
  throw referralsError;
}

const referrals = referralsRaw ?? [];

and then operate on referrals as you already do.

app/api/referral/claim/route.ts (1)

45-64: Supabase .single() error handling – consider .maybeSingle() or handling multi‑row cases explicitly

Right now:

  • You call .single() and treat error.code !== "PGRST116" as fatal, then treat lack of referral as 404.
  • This implicitly handles the “no rows” case, but if multiple pending referrals exist for the same referred_wallet_address (data corruption), the behavior depends on Supabase/PostgREST’s error code semantics and might end up surfacing as a 404 instead of a 500.

To make intent explicit and avoid reliance on a magic error code, you could:

  • Use .maybeSingle() (available in Supabase JS v2) and treat any non‑null error as fatal.
  • Optionally add a defensive limit(2) and a guard if more than one row is returned.

Example refactor:

-        const { data: referral, error: referralError } = await supabaseAdmin
-            .from("referrals")
-            .select("*")
-            .eq("referred_wallet_address", walletAddress)
-            .eq("status", "pending")
-            .single();
-
-        if (referralError && referralError.code !== "PGRST116") {
-            throw referralError;
-        }
-
-        if (!referral) {
+        const { data: referral, error: referralError } = await supabaseAdmin
+            .from("referrals")
+            .select("*")
+            .eq("referred_wallet_address", walletAddress)
+            .eq("status", "pending")
+            .maybeSingle();
+
+        if (referralError) {
+            throw referralError;
+        }
+
+        if (!referral) {
             return NextResponse.json(
                 {
                     success: false,
                     error: "No pending referral found",
                 },
                 { status: 404 }
             );
         }

This keeps the behavior for “no pending referral” but makes any ambiguous/multi‑row state fail loudly.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2fc36b0 and 2d3109a.

⛔ Files ignored due to path filters (3)
  • 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 (17)
  • .env.example (1 hunks)
  • app/api/aggregator.ts (2 hunks)
  • app/api/internal/credit-wallet/route.ts (1 hunks)
  • app/api/referral/claim/route.ts (1 hunks)
  • app/api/referral/generate-referral-code/route.ts (1 hunks)
  • app/api/referral/referral-data/route.ts (1 hunks)
  • app/api/referral/submit/route.ts (1 hunks)
  • app/components/MainPageContent.tsx (8 hunks)
  • app/components/NetworkSelectionModal.tsx (2 hunks)
  • app/components/ReferralCTA.tsx (1 hunks)
  • app/components/ReferralDashboard.tsx (1 hunks)
  • app/components/ReferralModal.tsx (1 hunks)
  • app/components/WalletDetails.tsx (4 hunks)
  • app/components/index.ts (1 hunks)
  • app/components/wallet-mobile-modal/WalletView.tsx (3 hunks)
  • app/types.ts (2 hunks)
  • middleware.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-10T16:44:32.125Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 244
File: app/components/CopyAddressWarningModal.tsx:48-52
Timestamp: 2025-10-10T16:44:32.125Z
Learning: In the CopyAddressWarningModal component (app/components/CopyAddressWarningModal.tsx), selectedNetwork from useNetwork() is always defined and does not require null safety checks when accessing its properties like selectedNetwork.chain.name.

Applied to files:

  • app/components/wallet-mobile-modal/WalletView.tsx
  • app/components/ReferralModal.tsx
  • app/components/MainPageContent.tsx
  • app/components/NetworkSelectionModal.tsx
📚 Learning: 2025-11-06T07:08:54.593Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:271-280
Timestamp: 2025-11-06T07:08:54.593Z
Learning: In the RecipientDetailsForm component (app/components/recipient/RecipientDetailsForm.tsx), Mobile Money institutions always return "Ok" from the fetchAccountName verification endpoint. Therefore, checking for accountName.toLowerCase() === "ok" is sufficient to handle both verification failures and Mobile Money institutions without needing explicit institution type checks in the frontend.

Applied to files:

  • app/components/MainPageContent.tsx
🧬 Code graph analysis (13)
app/components/wallet-mobile-modal/WalletView.tsx (2)
app/components/ReferralCTA.tsx (1)
  • ReferralCTA (7-51)
app/components/index.ts (1)
  • ReferralCTA (44-44)
app/components/ReferralModal.tsx (1)
app/api/aggregator.ts (1)
  • submitReferralCode (515-534)
app/api/referral/claim/route.ts (6)
app/api/internal/credit-wallet/route.ts (1)
  • POST (19-170)
app/api/referral/submit/route.ts (1)
  • POST (12-191)
app/lib/rate-limit.ts (1)
  • withRateLimit (32-59)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/api/aggregator.ts (1)
  • fetchKYCStatus (307-353)
app/lib/server-analytics.ts (1)
  • trackBusinessEvent (362-375)
app/api/referral/submit/route.ts (5)
app/api/internal/credit-wallet/route.ts (1)
  • POST (19-170)
app/api/referral/claim/route.ts (1)
  • POST (13-284)
app/lib/rate-limit.ts (1)
  • withRateLimit (32-59)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/lib/server-analytics.ts (2)
  • trackBusinessEvent (362-375)
  • trackAuthEvent (340-357)
app/components/ReferralCTA.tsx (1)
app/components/index.ts (1)
  • ReferralCTA (44-44)
app/components/ReferralDashboard.tsx (1)
app/components/AnimatedComponents.tsx (1)
  • sidebarAnimation (117-127)
app/api/aggregator.ts (1)
app/types.ts (1)
  • ReferralData (404-420)
app/components/MainPageContent.tsx (3)
app/components/NetworkSelectionModal.tsx (1)
  • NetworkSelectionModal (25-214)
app/components/ReferralModal.tsx (1)
  • ReferralInputModal (19-134)
app/hooks/useBlockFestReferral.ts (1)
  • useBlockFestReferral (5-28)
app/api/referral/generate-referral-code/route.ts (5)
app/api/aggregator.ts (1)
  • generateReferralCode (562-577)
app/api/referral/referral-data/route.ts (1)
  • GET (10-146)
app/lib/rate-limit.ts (1)
  • withRateLimit (32-59)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/lib/server-analytics.ts (1)
  • trackBusinessEvent (362-375)
app/api/referral/referral-data/route.ts (3)
app/api/referral/generate-referral-code/route.ts (1)
  • GET (21-154)
app/lib/rate-limit.ts (1)
  • withRateLimit (32-59)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/api/internal/credit-wallet/route.ts (2)
app/api/referral/claim/route.ts (1)
  • POST (13-284)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/components/WalletDetails.tsx (2)
app/components/ReferralCTA.tsx (1)
  • ReferralCTA (7-51)
app/components/ReferralDashboard.tsx (1)
  • ReferralDashboard (123-419)
app/components/NetworkSelectionModal.tsx (1)
app/components/index.ts (1)
  • NetworkSelectionModal (28-28)
🪛 Biome (2.1.2)
app/api/referral/generate-referral-code/route.ts

[error] 98-98: Unexpected constant condition.

(lint/correctness/noConstantCondition)

🔇 Additional comments (9)
app/components/index.ts (1)

43-44: Referral components correctly exposed via barrel export

Re-exports for ReferralInputModal and ReferralCTA follow the existing pattern and make the new referral UI available through the shared components index without expanding surface area unnecessarily.

app/api/aggregator.ts (1)

21-22: ReferralData import fits existing typing pattern

Importing ReferralData here is consistent with the rest of the module and lets referral helpers expose a well‑defined response shape instead of completely untyped data.

app/types.ts (1)

261-269: Network imageUrl union reflow is harmless

The formatting change on Network["imageUrl"] doesn’t alter semantics and keeps the union explicit and readable.

middleware.ts (1)

259-271: Code paths are correctly aligned; middleware protection verified

Verification confirms all referral endpoints match their client URLs and are properly protected:

  • Client calls /api/referral/submit, /api/referral/data, /api/referral/generate-referral-code → all correctly handled by matching route.ts files
  • Middleware matcher entries "/api/referral" and "/api/referral/:path*" catch all four referral endpoints (including /api/referral/claim)
  • Directory structure and endpoint paths are in sync; no misalignment issues
app/components/WalletDetails.tsx (1)

38-40: Referral CTA and sidebar wiring look correct; just verify visibility expectations

The new ReferralCTAisReferralOpenReferralDashboard flow is clean:

  • Clicking the CTA closes the wallet sidebar and opens the referral sidebar in one place (onViewReferrals).
  • ReferralDashboard is controlled by a single isReferralOpen flag with a simple onClose handler.

This is a good, self-contained pattern. The only thing to double‑check is whether showing the CTA for both injected and smart wallets is intentional, since it currently renders regardless of wallet type while transfer/fund actions are gated behind !isInjectedWallet.

Also applies to: 50-50, 280-282, 412-414

app/components/MainPageContent.tsx (1)

539-540: Referral modal flow is well-contained; just confirm the close semantics with NetworkSelectionModal

The new wiring for the referral input flow looks good overall:

  • MainPageContent owns showReferralModal plus handleNetworkSelected and handleReferralModalClose, and persists a per-wallet hasSeenReferralModal-* flag in localStorage.
  • PageLayout receives showReferralModal, onReferralModalClose, and onNetworkSelected, and renders:
    • NetworkSelectionModal with onNetworkSelected, and
    • ReferralInputModal driven solely by showReferralModal.
  • handleReferralModalClose properly hides the modal and sets the hasSeenReferralModal-* flag so the user isn’t prompted again.

Given the current NetworkSelectionModal implementation, onNetworkSelected is invoked on any modal close, not strictly after a successful network selection. That means this referral modal will be shown once per wallet after the first network modal dismissal, even if the user just cancels out. If the requirement is “prompt only after a completed network selection,” you may want to align the naming/behavior in NetworkSelectionModal as noted in that file’s comment.

Otherwise, the state management and persistence here look solid.

Also applies to: 564-584, 615-627, 646-649, 657-658, 695-718, 720-744, 1029-1032

app/api/referral/claim/route.ts (3)

18-40: Wallet header auth + basic request tracking look consistent

Using x-wallet-address from headers, lowercasing it, and short‑circuiting with a 401 plus trackApiError matches the pattern in referral/submit and keeps auth surface small. The trackApiRequest call immediately after is also well placed for successful requests.


104-132: Double‑check “first qualifying transaction” semantics and any required stablecoin filters

The transaction check currently:

  • Selects the earliest completed transaction for the wallet (order("created_at", { ascending: true }).limit(1)).
  • Uses amount_usd ?? amount_received and enforces >= 20.

This enforces “the user’s first completed transaction must be at least $20” and does not filter by asset/currency. If the product requirement is instead:

  • “Any first stablecoin transaction >= $20” (as suggested in the PR description), or
  • “Any completed transaction >= $20, regardless of whether the very first is smaller,”

you may want to:

  • Add a filter on currency/asset type (e.g., eq("asset_type", "stablecoin") or similar), and/or
  • Change the query to fetch the earliest transaction that meets both status === "completed" and the currency/amount criteria instead of strictly the first completed tx.

If the current logic matches your schema and product spec, no change needed—just worth validating the assumption.


134-149: Nice use of conditional update for safe pending → processing status transition

The update with both .eq("id", referral.id) and .eq("status", "pending") plus the length === 0 guard is a clean way to make the referral claim idempotent and avoid double‑processing in concurrent calls. Returning 409 when no rows are updated clearly signals “already processed or in progress” to the caller.

Comment on lines 512 to 577
/**
* Submit a referral code for a new user
*/
export async function submitReferralCode(
code: string,
accessToken?: string,
): Promise<any> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};

if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`;
}

const response = await axios.post(`/api/referral/submit`, { referral_code: code }, { headers });

if (!response.data?.success) {
throw new Error(response.data?.error || response.data?.message || "Failed to submit referral code");
}

return response.data;
}

/**
* Get user's referral data (code, earnings, referral list)
*/
export async function getReferralData(
accessToken?: string,
walletAddress?: string,
): Promise<ReferralData> {
const headers: Record<string, string> = {};
if (accessToken) headers.Authorization = `Bearer ${accessToken}`;

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

const response = await axios.get(url, { headers });

if (!response.data?.success) {
throw new Error(response.data?.error || "Failed to fetch referral data");
}

return response.data.data as ReferralData;
}

/**
* Generate or get user's referral code
*/
export async function generateReferralCode(
accessToken?: string,
): Promise<string> {
const headers: Record<string, string> = {};
if (accessToken) headers.Authorization = `Bearer ${accessToken}`;

const response = await axios.get(`/api/referral/generate-referral-code`, { headers });

if (!response.data?.success) {
throw new Error(response.data?.error || "Failed to generate referral code");
}

// normalize the payload shapes we might get back
const payload = response.data;
return payload.data?.referral_code || payload.data?.referralCode || payload.code || "";
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Tighten typing and error handling for referral helpers

The overall flow looks good, but there are a few improvements worth making:

  • submitReferralCode / getReferralData currently return any. It would be safer to type the responses, e.g. something like:
    • Promise<{ success: true; data: ReferralData }> (for getReferralData)
    • or a shared { success: boolean; data?: …; error?: string } shape, to catch backend/shape changes at compile time.
  • generateReferralCode returns "" if it can’t find a code in the payload even when success is true. That’s surprising for callers and can hide bugs. Consider throwing instead when no code is present so failures are explicit.
  • All three helpers treat accessToken as optional, but the middleware requires a JWT for /api/referral*. Unless you have known internal call sites that rely on this being optional, making accessToken required in the signatures would prevent accidental unauthenticated calls.

You can quickly verify that every call site passes an access token with:


🏁 Script executed:

#!/bin/bash
# Find all usages of the new referral helpers and inspect auth handling
rg -n 'submitReferralCode\(|getReferralData\(|generateReferralCode\(' app -S

Length of output: 611


I need to gather more information to verify the claims in the review comment. Let me inspect the call sites and search for any other usages of the exported generateReferralCode function.

Now let me inspect the actual call sites to verify the token handling claims:


🏁 Script executed:

#!/bin/bash
# Inspect context around ReferralModal.tsx:49 and ReferralDashboard.tsx:149
echo "=== ReferralModal.tsx context around line 49 ==="
sed -n '40,60p' app/components/ReferralModal.tsx

echo -e "\n=== ReferralDashboard.tsx context around line 149 ==="
sed -n '140,160p' app/components/ReferralDashboard.tsx

echo -e "\n=== Search for middleware JWT requirement ==="
fd -e ts -e js app/middleware -o app/api/referral | head -20

Length of output: 1934


I need to search for middleware and any other call sites. Let me fix the search command:


🏁 Script executed:

#!/bin/bash
# Search for middleware files
echo "=== Searching for middleware configuration ==="
find app -name "*middleware*" -type f 2>/dev/null
find . -name "middleware.*" -type f 2>/dev/null | grep -v node_modules

echo -e "\n=== Check for route.ts files in api/referral ==="
find app/api/referral -name "*.ts" 2>/dev/null

echo -e "\n=== Search for any other calls to exported generateReferralCode (excluding route.ts) ==="
rg -n 'generateReferralCode\(' app --type ts | grep -v 'route.ts:' | grep -v 'aggregator.ts:'

Length of output: 388


Now let me check the middleware and route handlers to verify the JWT requirements:


🏁 Script executed:

#!/bin/bash
# Check middleware.ts for auth/JWT requirements
echo "=== middleware.ts content ==="
cat ./middleware.ts

echo -e "\n=== Check submit/route.ts for auth handling ==="
cat app/api/referral/submit/route.ts

echo -e "\n=== Check referral-data/route.ts for auth handling ==="
cat app/api/referral/referral-data/route.ts

Length of output: 18408


Let me verify if aggregator.ts is client-side and if there are any other contexts where these functions are used:


🏁 Script executed:

#!/bin/bash
# Check if aggregator.ts is client or server
echo "=== Check aggregator.ts first lines ==="
head -20 app/api/aggregator.ts

echo -e "\n=== Search for imports of aggregator functions in server contexts ==="
rg -n 'from.*aggregator|import.*aggregator' app --type ts -C 2

echo -e "\n=== Check if generate-referral-code export is ever imported ==="
rg -n 'generateReferralCode' app --type ts -C 1 | grep -E '(import|from|generate-referral-code.*route)'

Length of output: 9793


Address type safety and unused exported function

The review identifies valid concerns, though some context differs:

  • Type safety (Promise<any>): Valid issue affecting all three functions. Consider using discriminated union types like Promise<{ success: true; data: ReferralData } | { success: false; error: string }> instead.

  • Empty string fallback in generateReferralCode: While the concern is valid, this exported function is never called anywhere in the codebase and appears to be dead code. Consider either removing it or throwing an error instead of returning "".

  • Optional accessToken parameter: Valid concern for submitReferralCode (the only actively used function). In ReferralModal.tsx:49, the token from getAccessToken() is passed with ?? undefined, which allows unauthenticated calls to fail at runtime rather than catch-time. Making accessToken required would force callers to handle the undefined case explicitly before calling the function. Note: getReferralData production code is commented out in ReferralDashboard.tsx:149, making its parameter concern academic.

🤖 Prompt for AI Agents
In app/api/aggregator.ts around lines 512-577, the three referral functions use
Promise<any>, submitReferralCode accepts an optional accessToken, and
generateReferralCode returns an empty string fallback and appears unused;
replace Promise<any> with precise types (e.g. a discriminated union like {
success: true; data: ReferralData } | { success: false; error: string } or
specific return types per function), make accessToken required for
submitReferralCode (update signature and callers) so callers must handle missing
tokens before calling, and either remove generateReferralCode (if truly unused)
or change it to throw an explicit error when no referral code is present and
tighten its return type to string (no empty-string fallback) while keeping
payload normalization and strong typing.

Comment on lines 154 to 207
const internalAuth = process.env.INTERNAL_API_KEY;
const internalBase = process.env.INTERNAL_API_BASE_URL || new URL(request.url).origin;

async function creditWallet(wallet: string, amountMicro: number, referralId: any) {
if (!internalAuth) {
// Wallet service not configured; skip actual crediting.
console.warn("Internal wallet service not configured, skipping credit for", wallet);
return { ok: false, skipped: true };
}

const resp = await fetch(`${internalBase}/api/internal/credit-wallet`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-internal-auth": internalAuth,
},
body: JSON.stringify({
wallet_address: wallet,
amount: amountMicro,
currency: "USDC",
reason: "referral_reward",
referral_id: referralId,
idempotency_key: `referral:${referral.id}:${wallet}`,
}),
});

if (!resp.ok) {
const text = await resp.text().catch(() => "");
throw new Error(`Wallet credit failed: ${resp.status} ${text}`);
}

return { ok: true };
}

try {
// credit referrer
const amountMicro = Math.round((referral.reward_amount || 1.0) * 1_000_000);
await creditWallet(referral.referrer_wallet_address, amountMicro, referral.id);
// credit referred user
await creditWallet(walletAddress, amountMicro, referral.id);
} catch (walletError) {
console.error("Wallet crediting failed, attempting rollback:", walletError);
// Roll back referral status to pending
try {
await supabaseAdmin.from("referrals").update({ status: "pending", completed_at: null }).eq("id", referral.id);
} catch (rbErr) {
console.error("Failed to roll back referral status:", rbErr);
}

const responseTime = Date.now() - startTime;
trackApiError(request, "/api/referral/claim", "POST", walletError as Error, 500, { response_time_ms: responseTime });

return NextResponse.json({ success: false, error: "Failed to credit referral rewards" }, { status: 500 });
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Missing INTERNAL_API_KEY is treated as success, which can silently drop rewards in production

At the moment:

  • creditWallet returns { ok: false, skipped: true } when INTERNAL_API_KEY is missing and just logs a warning.
  • The caller ignores the return value and treats “no throw” as success.
  • The flow then marks the referral as earned and emits “Referral Reward Earned” / “Referral Bonus Received” events even though no credit was attempted.

This is convenient for local/dev, but in a misconfigured production environment it would silently “complete” referrals without paying out, which is a high‑impact business bug.

A safer pattern:

  • Treat missing wallet configuration as a hard failure in production (500 + rollback), while still allowing the “skip” behavior in non‑prod.
  • Optionally gate the business events on whether credits were actually sent.

For example:

-        const internalAuth = process.env.INTERNAL_API_KEY;
-        const internalBase = process.env.INTERNAL_API_BASE_URL || new URL(request.url).origin;
+        const internalAuth = process.env.INTERNAL_API_KEY;
+        const internalBase = process.env.INTERNAL_API_BASE_URL || new URL(request.url).origin;
+        const isProd = process.env.NODE_ENV === "production";

         async function creditWallet(wallet: string, amountMicro: number, referralId: any) {
-            if (!internalAuth) {
-                // Wallet service not configured; skip actual crediting.
-                console.warn("Internal wallet service not configured, skipping credit for", wallet);
-                return { ok: false, skipped: true };
-            }
+            if (!internalAuth) {
+                if (isProd) {
+                    throw new Error("Internal wallet service not configured");
+                }
+                // Non‑prod: allow running the flow without actually crediting.
+                console.warn("Internal wallet service not configured, skipping credit for", wallet);
+                return { ok: false, skipped: true };
+            }
             const resp = await fetch(`${internalBase}/api/internal/credit-wallet`, {
                 method: "POST",
                 headers: {
                     "Content-Type": "application/json",
                     "x-internal-auth": internalAuth,
                 },
                 body: JSON.stringify({
                     wallet_address: wallet,
                     amount: amountMicro,
                     currency: "USDC",
                     reason: "referral_reward",
                     referral_id: referralId,
                     idempotency_key: `referral:${referral.id}:${wallet}`,
                 }),
             });

You may also want to:

  • Consider wiring the creditWallet return value into whether you emit “Reward Earned/Bonus Received” events, so analytics don’t claim payouts happened when they were skipped.

Given this touches the payout semantics, I’d treat it as something to fix before shipping.


I'll verify the review comment by inspecting the actual code in the repository.


🏁 Script executed:

#!/bin/bash
# Locate and examine the referral claim route file
fd -t f "route.ts" -p "*referral/claim*" | head -5

Length of output: 103


🏁 Script executed:

#!/bin/bash
# Get full content of the referral claim route file to understand context
find . -path "*app/api/referral/claim/route.ts" -o -path "*referral/claim/route.ts" | head -1 | xargs -r cat

Length of output: 10928


🏁 Script executed:

#!/bin/bash
# Search for "Referral Reward Earned" and "Referral Bonus Received" event emissions
rg -n "Referral Reward Earned|Referral Bonus Received" --type ts --type js -C 3

Length of output: 985


🏁 Script executed:

#!/bin/bash
# Search for NODE_ENV checks in the referral-related files
rg -n "NODE_ENV|process\.env\.NODE_ENV" --type ts --type js -g "*referral*" -C 2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check how creditWallet return value is handled
ast-grep --pattern 'await creditWallet($_,$_,$_)'

Length of output: 236


🏁 Script executed:

#!/bin/bash
# Search for INTERNAL_API_KEY usage in the codebase
rg -n "INTERNAL_API_KEY" --type ts --type js -C 3

Length of output: 4317


🏁 Script executed:

#!/bin/bash
# Look for environment configuration patterns
rg -n "isProd|isProduction|NODE_ENV.*production" --type ts --type js | head -20

Length of output: 455


🏁 Script executed:

#!/bin/bash
# Check for rollback and event emission patterns in the route file
rg -n "supabaseAdmin.*update|trackApiError|emit|event" app/api/referral/claim/route.ts -C 2

Length of output: 1894


🏁 Script executed:

#!/bin/bash
# Find if app/api/referral/claim/route.ts exists and show line count
test -f app/api/referral/claim/route.ts && wc -l app/api/referral/claim/route.ts || find . -name "route.ts" -path "*/referral/claim/*"

Length of output: 35


🏁 Script executed:

#!/bin/bash
# Get lines 150-220 context from the actual file
test -f app/api/referral/claim/route.ts && sed -n '150,220p' app/api/referral/claim/route.ts

Length of output: 3223


🏁 Script executed:

#!/bin/bash
# Search for all function calls to creditWallet in the entire file
rg -n "creditWallet" app/api/referral/claim/route.ts -B 2 -A 2

Length of output: 786


🏁 Script executed:

#!/bin/bash
# Check entire route file structure to understand context
test -f app/api/referral/claim/route.ts && head -100 app/api/referral/claim/route.ts

Length of output: 3404


🏁 Script executed:

#!/bin/bash
# Look for where referral status gets set to "earned" to understand event flow
rg -n "earned|status.*earned" app/api/referral/claim/route.ts -C 3

Length of output: 717


🏁 Script executed:

#!/bin/bash
# Check if there's a broader config or env setup file
find . -maxdepth 3 -name "env*.ts" -o -name "config*.ts" -o -name ".env*" | head -10

Length of output: 34


🏁 Script executed:

#!/bin/bash
# Look at the entire creditWallet function and surrounding code
test -f app/api/referral/claim/route.ts && sed -n '154,210p' app/api/referral/claim/route.ts

Length of output: 2554


🏁 Script executed:

#!/bin/bash
# Check after the creditWallet calls for event emission
test -f app/api/referral/claim/route.ts && sed -n '190,250p' app/api/referral/claim/route.ts

Length of output: 2653


🏁 Script executed:

#!/bin/bash
# Entire relevant section for full understanding
test -f app/api/referral/claim/route.ts && wc -l app/api/referral/claim/route.ts && echo "---" && cat app/api/referral/claim/route.ts

Length of output: 10968


Add environment-aware safety gate to prevent silent payment skips in production

Verification confirms the issue: when INTERNAL_API_KEY is missing, creditWallet() returns { ok: false, skipped: true } without throwing. The return value is ignored (lines 191–193), the catch block (lines 194–206) is never reached, and business events ("Referral Reward Earned", "Referral Bonus Received") are emitted unconditionally (lines 243–254). This allows silent reward skipping in production if the environment variable is misconfigured.

No NODE_ENV production check exists in this flow. The suggested fix—throwing an error in production when the wallet service is not configured, while allowing skip in non-prod—is the correct approach:

+        const isProd = process.env.NODE_ENV === "production";
         async function creditWallet(wallet: string, amountMicro: number, referralId: any) {
             if (!internalAuth) {
+                if (isProd) {
+                    throw new Error("Internal wallet service not configured");
+                }
                 // Non-prod: allow running the flow without actually crediting.
                 console.warn("Internal wallet service not configured, skipping credit for", wallet);
                 return { ok: false, skipped: true };
             }

Additionally, gate business events on whether credits were actually sent to prevent analytics from claiming payouts occurred when they were skipped.

🤖 Prompt for AI Agents
In app/api/referral/claim/route.ts around lines 154-207, creditWallet currently
silences missing INTERNAL_API_KEY by returning {ok:false, skipped:true} which is
ignored later; change creditWallet to throw an error when INTERNAL_API_KEY is
missing and NODE_ENV === "production" (allow skip only in non-prod), and ensure
the callers await and inspect the returned result for both referrer and referred
credits before proceeding; if either call throws or returns skipped/ok:false,
run the rollback/error flow and do not emit the business events or track success
— only emit "Referral Reward Earned"/"Referral Bonus Received" when both
creditWallet calls returned ok:true.

Comment on lines 13 to 21
// Mock data for presentation
const MOCK_DATA = {
referral_code: "NB738K",
total_earned: 34.9,
total_pending: 2.0,
total_referrals: 11,
earned_count: 8,
pending_count: 3,
referrals: [
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Dashboard still wired to MOCK_DATA instead of live referral API

Right now fetchData always sets MOCK_DATA and the production path using getAccessToken + getReferralData is commented out. That means the dashboard will never reflect real user referral codes or earnings in production.

Before shipping, this should be flipped so live data is the primary path and mocks are gated (e.g. behind a dev flag) or removed. For example:

-            try {
-                setIsLoading(true);
-                if (mounted) setReferralData(MOCK_DATA);
-
-                /* Production code (commented for demo):
-                const token = await getAccessToken();
-                if (!token) return;
-                const data = await getReferralData(token);
-                if (mounted) setReferralData(data);
-                */
+            try {
+                setIsLoading(true);
+                const token = await getAccessToken();
+                if (!token) return;
+                const data = await getReferralData(token);
+                if (mounted) setReferralData(data);
+                // Optionally: if (process.env.NEXT_PUBLIC_REFERRAL_USE_MOCK === "true") setReferralData(MOCK_DATA);

You can also type referralData as ReferralData | null to get compiler help:

import type { ReferralData } from "../types";

const [referralData, setReferralData] = useState<ReferralData | null>(null);

Also applies to: 136-166

🤖 Prompt for AI Agents
In app/components/ReferralDashboard.tsx around lines 13-21 (and also apply same
changes to lines 136-166), fetchData currently always assigns MOCK_DATA and the
live production path (getAccessToken + getReferralData) is commented out; change
it so the component fetches live referral data by default: uncomment and use the
getAccessToken/getReferralData flow, await and set the real response into state,
handle errors and fall back to mocks only when a dev flag (e.g.,
process.env.NODE_ENV === "development" or a dedicated USE_MOCKS flag) is
enabled; update the referralData state to be typed as ReferralData | null and
initialize to null so TypeScript forces null checks, and remove direct uses of
MOCK_DATA in production paths (or gate them behind the dev flag) and ensure you
call setReferralData with the live payload and log/handle failures.

Copy link

@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: 0

♻️ Duplicate comments (1)
app/components/ReferralDashboard.tsx (1)

13-166: Dashboard is still backed by MOCK_DATA instead of live referral API

fetchData always sets referralData(MOCK_DATA) and the production path with getAccessToken + getReferralData remains commented, so the dashboard will never show a user’s real referral code or earnings in production. isLoading and the loading UI are effectively unused as well.

Before shipping this feature, wire fetchData to call the live API by default (using getAccessToken + getReferralData), and gate or remove MOCK_DATA so it’s only used under an explicit dev flag. While you’re in there, typing referralData as ReferralData | null would make the null/shape checks much safer.

🧹 Nitpick comments (3)
app/api/referral/generate-referral-code/route.ts (1)

11-158: Referral code generation + uniqueness loop look solid

The generator + while (!code && attempts < maxAttempts) loop correctly enforces a bounded uniqueness check and handles the Supabase PGRST116 (“no row”) case explicitly. The upsert with onConflict: "wallet_address" cleanly reuses or assigns codes per wallet, and analytics coverage is good.

If you expect strict global uniqueness across wallets, make sure there’s a UNIQUE constraint on users.referral_code at the DB level so concurrent requests across different wallets can’t ever collide even under race conditions.

app/components/ReferralCTA.tsx (1)

7-48: CTA prop contract is good; handler can be simplified

The required onViewReferrals prop fixes the earlier no-op issue and the USDC wording now matches the backend reward currency. Given the prop is non-optional, the if (onViewReferrals) guard in handleViewReferrals is redundant—you could pass onViewReferrals directly to the button’s onClick (or drop the wrapper) to trim a tiny bit of indirection.

app/components/ReferralModal.tsx (1)

31-71: Improve error messaging and auth handling in handleSubmit

The control flow and loading state handling look solid, but the outer catch currently surfaces any failure from getAccessToken() (or other unexpected errors) as “Invalid referral code…”, which is misleading for auth/network issues. It also silently proceeds if getAccessToken() returns null, relying on the backend to fail with 401.

Consider:

  • Handling a missing/failed access token explicitly before calling submitReferralCode and surfacing a more accurate message (e.g. “Session expired, please reconnect your wallet”), and/or
  • Using a generic fallback message in the outer catch that doesn’t imply the code itself is invalid.

This keeps user feedback accurate and makes debugging auth issues easier while preserving your inner try/catch for well‑formed API responses.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2d3109a and 0e3de13.

📒 Files selected for processing (13)
  • app/api/aggregator.ts (2 hunks)
  • app/api/internal/credit-wallet/route.ts (1 hunks)
  • app/api/referral/claim/route.ts (1 hunks)
  • app/api/referral/generate-referral-code/route.ts (1 hunks)
  • app/api/referral/submit/route.ts (1 hunks)
  • app/components/MobileDropdown.tsx (4 hunks)
  • app/components/Navbar.tsx (3 hunks)
  • app/components/ReferralCTA.tsx (1 hunks)
  • app/components/ReferralDashboard.tsx (1 hunks)
  • app/components/ReferralModal.tsx (1 hunks)
  • app/components/WalletDetails.tsx (4 hunks)
  • app/components/wallet-mobile-modal/WalletView.tsx (5 hunks)
  • app/types.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/components/wallet-mobile-modal/WalletView.tsx
  • app/types.ts
  • app/api/internal/credit-wallet/route.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-10-10T16:44:32.125Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 244
File: app/components/CopyAddressWarningModal.tsx:48-52
Timestamp: 2025-10-10T16:44:32.125Z
Learning: In the CopyAddressWarningModal component (app/components/CopyAddressWarningModal.tsx), selectedNetwork from useNetwork() is always defined and does not require null safety checks when accessing its properties like selectedNetwork.chain.name.

Applied to files:

  • app/components/ReferralModal.tsx
  • app/components/ReferralDashboard.tsx
  • app/components/MobileDropdown.tsx
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is true (verification failed/returned "Ok"), the recipient safety alert should display when: isRecipientNameEditable && recipientName && !errors.recipientName && !recipientNameError. The !isFetchingRecipientName check is redundant because recipientName is cleared at fetch start and only populated after fetching completes or when the user manually enters it.

Applied to files:

  • app/components/ReferralCTA.tsx
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is false (verification succeeded), the recipient safety alert should display when: !isRecipientNameEditable && recipientName && !recipientNameError. The !errors.recipientName check is unnecessary because in non-editable mode the recipient name is displayed as read-only text (not an input field), so form validation errors cannot occur.

Applied to files:

  • app/components/ReferralCTA.tsx
🧬 Code graph analysis (10)
app/api/referral/claim/route.ts (6)
app/api/internal/credit-wallet/route.ts (1)
  • POST (5-162)
app/api/referral/submit/route.ts (1)
  • POST (12-195)
app/lib/rate-limit.ts (1)
  • withRateLimit (32-59)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/api/aggregator.ts (1)
  • fetchKYCStatus (309-355)
app/lib/server-analytics.ts (1)
  • trackBusinessEvent (362-375)
app/api/referral/submit/route.ts (5)
app/api/internal/credit-wallet/route.ts (1)
  • POST (5-162)
app/api/referral/claim/route.ts (1)
  • POST (13-285)
app/lib/rate-limit.ts (1)
  • withRateLimit (32-59)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/lib/server-analytics.ts (2)
  • trackBusinessEvent (362-375)
  • trackAuthEvent (340-357)
app/api/aggregator.ts (1)
app/types.ts (3)
  • ApiResponse (422-424)
  • SubmitReferralResult (426-426)
  • ReferralData (404-420)
app/components/ReferralModal.tsx (2)
app/components/index.ts (2)
  • ReferralInputModal (43-43)
  • AnimatedModal (17-17)
app/api/aggregator.ts (1)
  • submitReferralCode (517-544)
app/components/ReferralCTA.tsx (1)
app/components/index.ts (1)
  • ReferralCTA (44-44)
app/components/ReferralDashboard.tsx (1)
app/components/AnimatedComponents.tsx (1)
  • sidebarAnimation (117-127)
app/api/referral/generate-referral-code/route.ts (5)
app/api/aggregator.ts (1)
  • generateReferralCode (581-603)
app/api/referral/referral-data/route.ts (1)
  • GET (10-146)
app/lib/rate-limit.ts (1)
  • withRateLimit (32-59)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/lib/server-analytics.ts (1)
  • trackBusinessEvent (362-375)
app/components/WalletDetails.tsx (2)
app/components/ReferralCTA.tsx (1)
  • ReferralCTA (7-51)
app/components/ReferralDashboard.tsx (1)
  • ReferralDashboard (123-419)
app/components/MobileDropdown.tsx (2)
app/components/index.ts (1)
  • CopyAddressWarningModal (11-11)
app/components/CopyAddressWarningModal.tsx (1)
  • CopyAddressWarningModal (31-402)
app/components/Navbar.tsx (2)
app/utils.ts (1)
  • IS_MAIN_PRODUCTION_DOMAIN (664-666)
app/components/ReferralDashboard.tsx (1)
  • ReferralDashboard (123-419)
🔇 Additional comments (6)
app/api/referral/claim/route.ts (1)

12-264: Referral claim flow and environment-aware wallet crediting look correct

The POST handler’s sequence—pending referral lookup, dual KYC verification, first-completed-transaction check with a 20 USD minimum, guarded pending → processing status update, and rollback on wallet credit failure—is sound.

The updated creditWallet helper that throws in production when INTERNAL_API_KEY is missing (while only logging/returning { ok: false, skipped: true } in non‑prod) closes the earlier gap where referrals could be marked earned without payouts in a misconfigured prod environment. The rollback on any crediting error keeps referral status consistent with actual payouts.

app/components/Navbar.tsx (1)

30-292: Referral dashboard wiring in Navbar is coherent and scoped correctly

isReferralOpen state, the onViewReferrals callback into MobileDropdown, and the colocated <ReferralDashboard> render form a clean flow for opening/closing the referral drawer. Rendering it only when (ready && authenticated) || isInjectedWallet ensures the UI is shown only for users with an active wallet context.

app/components/WalletDetails.tsx (1)

38-40: Referral CTA and dashboard wiring looks solid

The new ReferralCTA and ReferralDashboard integration is clean: state is localized (isReferralOpen), the CTA neatly closes the wallet sidebar before opening the referral panel (with a delay matching the sidebar animation), and the dashboard is rendered once at the root with a simple isOpen/onClose API. No functional issues from this change.

Also applies to: 50-50, 280-287, 417-418

app/api/referral/submit/route.ts (1)

12-195: Referral submit route is well-structured and observability-friendly

The handler’s flow looks correct and robust:

  • Clear separation of 400 (missing/invalid code, self-referral), 404 (unknown code), 409 (already used), and 500 (unexpected DB issues).
  • Proper use of the Supabase “no row” pattern (code !== "PGRST116") so real DB failures bubble into the catch block and are tracked via trackApiError.
  • Consistent analytics: request, response (with timing), plus business and auth events on success.

No blocking issues from this implementation.

app/components/MobileDropdown.tsx (1)

26-34: Referral callback propagation from mobile dropdown is clean

The new optional onViewReferrals prop is threaded into WalletView without impacting existing flows, and the logout tracking helper remains non-blocking with the AbortError guard. The CopyAddressWarningModal placement still aligns with how isWarningModalOpen is managed. No issues here.

Also applies to: 135-153, 237-241, 295-299

app/api/aggregator.ts (1)

21-24: Disregard this review comment; callers do not require the suggested type changes

The verification found no evidence supporting the proposed changes:

  • submitReferralCode: The sole caller (ReferralModal.tsx:49) accesses only res.data?.message, never referral_id. The current type SubmitReferralResult = { message?: string } aligns perfectly with actual usage.
  • generateReferralCode: The aggregator wrapper has zero call sites in the codebase. No consumer relies on it, so the union type is not a practical concern.
  • getReferralData: Properly typed and cast; only used in commented demo code, so type safety has no functional impact.

The implementation is sound as-is. Type changes proposed in the review would add unused fields that callers do not consume.

Likely an incorrect or invalid review comment.

Copy link

@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: 1

🧹 Nitpick comments (5)
app/components/Navbar.tsx (1)

30-30: Referral dashboard state is currently unused; align with MobileDropdown wiring

isReferralOpen and <ReferralDashboard> are only controlled via onViewReferrals={() => setIsReferralOpen(true)} passed into MobileDropdown, but MobileDropdown never invokes this prop. As written, this sidebar dashboard will never open.

Either:

  • Wire MobileDropdown to call its onViewReferrals prop when the user taps “View referrals” (and potentially drop the internal referrals view there), or
  • If the mobile flow is meant to use ReferralDashboardView instead, remove the onViewReferrals prop and isReferralOpen/ReferralDashboard from Navbar to avoid dead code and confusion.

Also applies to: 36-37, 284-290

app/components/wallet-mobile-modal/WalletView.tsx (1)

15-16: Referral CTA wiring works; consider avoiding silent no‑op fallback

The new onViewReferrals prop and <ReferralCTA> render correctly gate the CTA to non‑injected wallets, and MobileDropdown now passes a concrete handler, so the button is functional.

To avoid a situation where a future parent forgets to pass onViewReferrals and the CTA silently becomes a no‑op, consider either:

  • Making onViewReferrals required on WalletViewProps, or
  • Rendering <ReferralCTA> only when onViewReferrals is defined, instead of defaulting to an empty function.

This keeps the UX from degrading quietly if the callback is omitted.

Also applies to: 39-40, 62-63, 299-303

app/components/MobileDropdown.tsx (2)

21-21: Resolve duplicated referral wiring and unused onViewReferrals prop

Inside MobileDropdown:

  • The onViewReferrals prop is defined on the component but never used.
  • Instead, WalletView gets onViewReferrals={() => setCurrentView("referrals")}, and the "referrals" branch renders ReferralDashboardView.

At the same time, Navbar passes onViewReferrals={() => setIsReferralOpen(true)} into MobileDropdown, expecting it to be called—this never happens, so the nav‑level ReferralDashboard is dead.

Recommend one of:

  • Drop the onViewReferrals prop from MobileDropdown and stop passing it from Navbar, relying solely on this internal "referrals" view; or
  • Call onViewReferrals?.() when handling “View referrals” (and reconsider whether you still want the internal "referrals" view) so the navbar’s dashboard can actually open.

This will avoid dangling props and clarify the single source of truth for opening referrals.

Also applies to: 26-37, 237-241, 285-290


285-290: Avoid nested modal Dialogs for the referrals view

ReferralDashboardView itself renders a full‑screen Dialog with its own overlay and focus trap, but here it is mounted inside the DialogPanel of MobileDropdown, creating two overlapping HeadlessUI Dialogs.

While it works visually, this can complicate focus management and accessibility. Consider instead:

  • Rendering ReferralDashboardView as a sibling to MobileDropdown (controlled by shared state), or
  • Extracting a content‑only ReferralDashboardContent used here inside the existing DialogPanel, leaving ReferralDashboardView for standalone use.

This keeps only one modal active at a time.

app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1)

176-187: Guard avatar color generation for unexpected address formats (optional)

getAvatarColor assumes address.slice(2, 4) is valid hex; if an unexpected string slips through, parseInt can return NaN, yielding an undefined class. Not fatal, but you could defensively handle it:

const getAvatarColor = (address: string) => {
  const colors = [...];
  const slice = address.slice(2, 4);
  const parsed = parseInt(slice, 16);
  const index = Number.isNaN(parsed) ? 0 : parsed % colors.length;
  return colors[index];
};

Purely a robustness tweak; current behavior degrades to “no gradient class” in bad data cases.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e3de13 and 63dc412.

📒 Files selected for processing (5)
  • app/components/MobileDropdown.tsx (5 hunks)
  • app/components/Navbar.tsx (3 hunks)
  • app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1 hunks)
  • app/components/wallet-mobile-modal/WalletView.tsx (5 hunks)
  • app/components/wallet-mobile-modal/index.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-10-10T16:44:32.125Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 244
File: app/components/CopyAddressWarningModal.tsx:48-52
Timestamp: 2025-10-10T16:44:32.125Z
Learning: In the CopyAddressWarningModal component (app/components/CopyAddressWarningModal.tsx), selectedNetwork from useNetwork() is always defined and does not require null safety checks when accessing its properties like selectedNetwork.chain.name.

Applied to files:

  • app/components/wallet-mobile-modal/WalletView.tsx
  • app/components/MobileDropdown.tsx
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is true (verification failed/returned "Ok"), the recipient safety alert should display when: isRecipientNameEditable && recipientName && !errors.recipientName && !recipientNameError. The !isFetchingRecipientName check is redundant because recipientName is cleared at fetch start and only populated after fetching completes or when the user manually enters it.

Applied to files:

  • app/components/wallet-mobile-modal/WalletView.tsx
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is false (verification succeeded), the recipient safety alert should display when: !isRecipientNameEditable && recipientName && !recipientNameError. The !errors.recipientName check is unnecessary because in non-editable mode the recipient name is displayed as read-only text (not an input field), so form validation errors cannot occur.

Applied to files:

  • app/components/wallet-mobile-modal/WalletView.tsx
🧬 Code graph analysis (4)
app/components/wallet-mobile-modal/WalletView.tsx (2)
app/components/ReferralCTA.tsx (1)
  • ReferralCTA (7-51)
app/components/index.ts (1)
  • ReferralCTA (44-44)
app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1)
app/components/AnimatedComponents.tsx (1)
  • slideUpAnimation (95-105)
app/components/Navbar.tsx (2)
app/utils.ts (1)
  • IS_MAIN_PRODUCTION_DOMAIN (664-666)
app/components/ReferralDashboard.tsx (1)
  • ReferralDashboard (123-419)
app/components/MobileDropdown.tsx (2)
app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1)
  • ReferralDashboardView (120-393)
app/components/wallet-mobile-modal/index.ts (1)
  • ReferralDashboardView (4-4)
🔇 Additional comments (4)
app/components/wallet-mobile-modal/index.ts (1)

4-4: ReferralDashboardView re-export looks good

Publicly exposing ReferralDashboardView via this barrel file matches its usage in MobileDropdown and keeps the modal API consistent.

app/components/Navbar.tsx (1)

187-189: Swap link className change is behavior‑neutral

The reflowed template literal for the “Swap” link doesn’t alter behavior; layout and theming remain equivalent.

app/components/wallet-mobile-modal/WalletView.tsx (1)

263-267: Network filter change maintains prior behavior

The updated .filter condition is semantically identical to the previous logic—still showing all networks for injected wallets and excluding “Celo” and “Hedera Mainnet” for smart wallets only.

app/components/MobileDropdown.tsx (1)

135-153: Logout tracking helper improvements look safe

Adding .catch and .finally around trackLogoutWithFetch makes the fallback analytics call more robust without impacting the logout flow; aborts are handled quietly and the timeout is always cleared.

… ethers USDC crediting with balance check and updates in ui.
Copy link

@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: 5

♻️ Duplicate comments (3)
app/components/ReferralDashboard.tsx (2)

132-132: Replace any type with proper ReferralData type.

Using any defeats TypeScript's type safety and prevents catching potential bugs at compile time.

Import the ReferralData type and use it:

+import type { ReferralData } from "../types";
 
-const [referralData, setReferralData] = useState<any | null>(null);
+const [referralData, setReferralData] = useState<ReferralData | null>(null);

Also update line 188:

-const filteredReferrals: any[] = (referralData?.referrals || []).filter(
-    (r: any) => r.status === activeTab
+const filteredReferrals = (referralData?.referrals || []).filter(
+    (r) => r.status === activeTab
 );

399-399: Add null safety for referral amount.

The code assumes referral.amount is always a number, but if the API returns null or undefined, .toFixed(1) will throw a runtime error.

Apply safe chaining:

-{referral.amount.toFixed(1)} USDC
+{referral.amount?.toFixed(1) ?? "0.0"} USDC

Or add type guards in the filter/map logic to exclude referrals with missing amounts.

app/api/aggregator.ts (1)

517-606: Tighten auth: make accessToken mandatory for referral helpers (or clearly justify optional use)

Both submitReferralCode (Lines 517–544) and generateReferralCode (Lines 604–626) treat accessToken as optional, but the backing /api/referral/* routes rely on authenticated middleware to populate x-wallet-address. This means callers can omit the token and only discover the mistake via a 401 at runtime.

Given these are user-facing flows and other helpers in this file typically require an access token, it would be safer to:

  • Make accessToken a required parameter for submitReferralCode and generateReferralCode, matching getReferralData and the middleware contract.
  • Let call sites handle “no token” cases explicitly before invoking these helpers.

If you intentionally support unauthenticated internal call sites, a short comment documenting that exception would help avoid accidental misuse.

🧹 Nitpick comments (7)
app/components/ReferralDashboard.tsx (3)

151-151: Remove or uncomment the commented error toast.

The commented toast.error should either be uncommented to provide user feedback on API failures, or removed entirely if silent failure is intentional.

 } else if (mounted) {
-    // toast.error(response.error || "Failed to load referral data");
+    toast.error(response.error || "Failed to load referral data");
     setReferralData(null);
 }

172-186: Add error handling for clipboard operations.

The navigator.clipboard.writeText() calls can fail in non-HTTPS contexts, older browsers, or when clipboard permissions are denied. Currently, these failures are silent.

Wrap clipboard operations in try-catch blocks:

 const handleCopyCode = () => {
     if (referralData?.referral_code) {
-        navigator.clipboard.writeText(referralData.referral_code);
-        setShowCopiedMessage(true);
-        setTimeout(() => setShowCopiedMessage(false), 2000);
+        navigator.clipboard.writeText(referralData.referral_code)
+            .then(() => {
+                setShowCopiedMessage(true);
+                setTimeout(() => setShowCopiedMessage(false), 2000);
+            })
+            .catch(() => toast.error("Failed to copy code"));
     }
 };

 const handleCopyLink = () => {
     if (referralData?.referral_code) {
         const link = `${window.location.origin}?ref=${referralData.referral_code}`;
-        navigator.clipboard.writeText(link);
-        toast.success("Referral link copied!");
+        navigator.clipboard.writeText(link)
+            .then(() => toast.success("Referral link copied!"))
+            .catch(() => toast.error("Failed to copy link"));
     }
 };

358-361: Consider using CSS classes instead of inline styles.

Inline styles for scrollbar hiding could be extracted to a CSS class or Tailwind utility for better maintainability and reusability.

Create a CSS class:

.scrollbar-hide {
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.scrollbar-hide::-webkit-scrollbar {
  display: none;
}

Or if Tailwind's scrollbar-hide plugin is available (which appears to be the case based on the className), you can remove the inline styles entirely:

 <div
-    className="max-h-[640px] space-y-3 overflow-y-auto scrollbar-hide"
-    style={{
-        scrollbarWidth: 'none',
-        msOverflowStyle: 'none',
-    }}
+    className="max-h-[640px] space-y-3 overflow-y-auto scrollbar-hide"
 >
app/api/referral/claim/route.ts (1)

119-138: Handle DB update failures for rollback and final “earned” status

On credit failure, the rollback to pending (Line 128) and the final earned update (Line 133) both ignore potential errors from the DB client. If either update fails, you can end up with referrals stuck in processing or with status out of sync with actual payouts, and you won’t have logs/metrics for it.

Consider capturing and handling the returned error from these updates, at least for logging/analytics, and optionally treating a failed “earned” update as a server error instead of success. For example:

-        if (!refCredit.ok || !refdCredit.ok) {
-            // Rollback
-            await supabaseAdmin.from("referrals").update({ status: "pending" }).eq("id", referral.id);
-            return NextResponse.json({ success: false, error: "Credit failed—try later" }, { status: 500 });
-        }
-
-        // Finalize: earned
-        await supabaseAdmin.from("referrals").update({ status: "earned", completed_at: new Date().toISOString() }).eq("id", referral.id);
+        if (!refCredit.ok || !refdCredit.ok) {
+            // Roll back to pending if crediting failed
+            const { error: rollbackError } = await supabaseAdmin
+                .from("referrals")
+                .update({ status: "pending" })
+                .eq("id", referral.id);
+
+            if (rollbackError) {
+                console.error("Failed to roll back referral status:", rollbackError);
+            }
+
+            return NextResponse.json(
+                { success: false, error: "Credit failed—try later" },
+                { status: 500 },
+            );
+        }
+
+        // Finalize: earned
+        const { error: finalizeError } = await supabaseAdmin
+            .from("referrals")
+            .update({
+                status: "earned",
+                completed_at: new Date().toISOString(),
+            })
+            .eq("id", referral.id);
+
+        if (finalizeError) {
+            console.error("Failed to mark referral as earned:", finalizeError);
+            return NextResponse.json(
+                { success: false, error: "Failed to finalize referral reward" },
+                { status: 500 },
+            );
+        }
app/api/referral/generate-referral-code/route.ts (1)

11-19: Deduplicate referral-code generation/uniqueness logic across referral endpoints

The generateReferralCode helper and uniqueness/upsert sequence here (Lines 11–19 and 77–121) are almost identical to the logic in app/api/referral/referral-data/route.ts. Duplicating this flow in two places makes it easy for them to drift if you ever change:

  • Code format (length, prefix, character set)
  • Uniqueness rules (max attempts, conflict handling)
  • Upsert behavior (additional fields, timestamps)

Consider extracting a shared server-side helper (e.g., createOrFetchReferralCode(walletAddress) in a small module under app/api/referral/) that encapsulates:

  • Generating a candidate code
  • Checking for collisions
  • Upserting the user with the new code
  • Returning the final referral_code

Both the “generate-code” route and the “referral-data” route could call into it, keeping behavior consistent and easing future changes.

Also applies to: 77-121

app/api/referral/referral-data/route.ts (1)

31-41: Align analytics endpoint name with actual /api/referral/referral-data route

In this handler, the analytics calls use "/api/referral/data" (Lines 33–35 and 180–183), but the actual route path is /api/referral/referral-data (based on the file location). This mismatch can make metrics and logs harder to correlate with real HTTP traffic.

Recommend updating the strings passed to trackApiRequest, trackApiResponse, and trackApiError to "/api/referral/referral-data" so your observability pipeline reflects the real endpoint name consistently (and matches client helpers like getReferralData).

Also applies to: 178-186

app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1)

147-150: Uncomment error toast for consistent user feedback.

When the API returns success: false, no toast notification is shown to the user (line 148 is commented out), but network/exception errors do show a toast (line 154). This creates inconsistent UX—users won't know why their referral data failed to load in the API error case.

Apply this diff:

 } else if (mounted) {
-    // toast.error(response.error || "Failed to load referral data");
+    toast.error(response.error || "Failed to load referral data");
     setReferralData(null);
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 63dc412 and f18e8ae.

📒 Files selected for processing (9)
  • app/api/aggregator.ts (2 hunks)
  • app/api/referral/claim/route.ts (1 hunks)
  • app/api/referral/generate-referral-code/route.ts (1 hunks)
  • app/api/referral/referral-data/route.ts (1 hunks)
  • app/components/ReferralDashboard.tsx (1 hunks)
  • app/components/ReferralDashboardSkeleton.tsx (1 hunks)
  • app/components/ReferralDashboardViewSkeleton.tsx (1 hunks)
  • app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1 hunks)
  • app/components/wallet-mobile-modal/WalletView.tsx (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/components/wallet-mobile-modal/WalletView.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-10T16:44:32.125Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 244
File: app/components/CopyAddressWarningModal.tsx:48-52
Timestamp: 2025-10-10T16:44:32.125Z
Learning: In the CopyAddressWarningModal component (app/components/CopyAddressWarningModal.tsx), selectedNetwork from useNetwork() is always defined and does not require null safety checks when accessing its properties like selectedNetwork.chain.name.

Applied to files:

  • app/components/ReferralDashboard.tsx
🧬 Code graph analysis (6)
app/api/referral/generate-referral-code/route.ts (5)
app/api/aggregator.ts (1)
  • generateReferralCode (604-626)
app/api/referral/referral-data/route.ts (1)
  • GET (21-209)
app/lib/rate-limit.ts (1)
  • withRateLimit (32-59)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/lib/server-analytics.ts (1)
  • trackBusinessEvent (362-375)
app/api/referral/referral-data/route.ts (4)
app/api/referral/generate-referral-code/route.ts (1)
  • GET (21-160)
app/lib/rate-limit.ts (1)
  • withRateLimit (32-59)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/lib/server-analytics.ts (1)
  • trackBusinessEvent (362-375)
app/api/referral/claim/route.ts (5)
app/api/referral/submit/route.ts (1)
  • POST (12-195)
app/lib/rate-limit.ts (1)
  • withRateLimit (32-59)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/api/aggregator.ts (1)
  • fetchKYCStatus (309-355)
app/lib/server-analytics.ts (1)
  • trackBusinessEvent (362-375)
app/api/aggregator.ts (1)
app/types.ts (3)
  • ApiResponse (422-424)
  • SubmitReferralResult (426-426)
  • ReferralData (404-420)
app/components/ReferralDashboard.tsx (3)
app/api/aggregator.ts (1)
  • getReferralData (550-599)
app/components/AnimatedComponents.tsx (1)
  • sidebarAnimation (117-127)
app/components/ReferralDashboardSkeleton.tsx (1)
  • ReferralDashboardSkeleton (1-64)
app/components/wallet-mobile-modal/ReferralDashboardView.tsx (3)
app/api/aggregator.ts (1)
  • getReferralData (550-599)
app/components/AnimatedComponents.tsx (1)
  • slideUpAnimation (95-105)
app/components/ReferralDashboardViewSkeleton.tsx (1)
  • ReferralDashboardViewSkeleton (1-64)
🔇 Additional comments (4)
app/components/ReferralDashboard.tsx (2)

206-230: LGTM: Loading state implementation.

The loading state properly renders a skeleton UI while data is being fetched, with appropriate animations and modal structure.


270-270: LGTM: Optional chaining on numeric methods is now correct.

The .toFixed() calls are now properly guarded with optional chaining and nullish coalescing, preventing crashes when referralData is null.

✅ Addressed in commit 0e3de13

Also applies to: 278-278

app/components/ReferralDashboardSkeleton.tsx (1)

1-63: Referral dashboard skeleton looks clean and appropriate

The structure and placeholders align well with a typical referral dashboard (stats, code card, list), and there’s no state or side‑effect complexity here.

app/components/ReferralDashboardViewSkeleton.tsx (1)

1-63: Mobile referral view skeleton is consistent and minimal-risk

Layout mirrors the main dashboard skeleton with mobile-friendly scrolling; nothing here affects behavior beyond loading visuals.

Comment on lines +145 to +146
const token = await getAccessToken();
if (!token) return;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix loading state stuck when token is missing.

If getAccessToken() returns a falsy value, the function returns early without setting isLoading to false, leaving the dashboard stuck in the loading skeleton forever.

Apply this diff to ensure loading state is always reset:

 const token = await getAccessToken();
-if (!token) return;
+if (!token) {
+    toast.error("Authentication required");
+    return;
+}

The finally block on line 160 will still execute and set isLoading(false), so this fix ensures users see an error message instead of infinite loading.

🤖 Prompt for AI Agents
In app/components/ReferralDashboard.tsx around lines 145-146, the code returns
early when getAccessToken() yields a falsy value which can leave the UI in an
indeterminate state; instead, when token is missing, explicitly reset loading
and surface an error before returning (e.g., call setIsLoading(false) and set an
error/message state) so the component exits loading and shows an error rather
than appearing stuck.

onClose: () => void;
}) => {
const { getAccessToken } = usePrivy();
const [referralData, setReferralData] = useState<any | null>(null);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Replace any type with proper ReferralData type.

Using any defeats TypeScript's type checking and prevents IntelliSense from helping with the referral data structure. Import and use the ReferralData type that's returned by the getReferralData API.

Apply this diff:

+import type { ReferralData } from "../../types"; // Adjust path as needed
 
-const [referralData, setReferralData] = useState<any | null>(null);
+const [referralData, setReferralData] = useState<ReferralData | null>(null);

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/components/wallet-mobile-modal/ReferralDashboardView.tsx around line 129,
replace the useState any type with the proper ReferralData type returned by
getReferralData: import ReferralData from the module that exports the API types
(the same file that exports getReferralData), change the state declaration to
useState<ReferralData | null>(null) and update any related handlers/props to use
ReferralData so TypeScript can enforce the referral data shape.

Comment on lines 388 to 390
<p className="text-sm font-medium text-text-secondary dark:text-white/60">
{referral.amount.toFixed(1)} USDC
</p>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add null check for referral amount to prevent runtime error.

Line 389 calls .toFixed(1) directly on referral.amount without checking for null/undefined. If the API returns a referral with a missing amount field, this will throw a runtime error and crash the component. The summary totals (lines 270, 278) use optional chaining; apply the same defensive pattern here.

Apply this diff:

 <p className="text-sm font-medium text-text-secondary dark:text-white/60">
-    {referral.amount.toFixed(1)} USDC
+    {(referral.amount ?? 0).toFixed(1)} USDC
 </p>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<p className="text-sm font-medium text-text-secondary dark:text-white/60">
{referral.amount.toFixed(1)} USDC
</p>
<p className="text-sm font-medium text-text-secondary dark:text-white/60">
{(referral.amount ?? 0).toFixed(1)} USDC
</p>
🤖 Prompt for AI Agents
In app/components/wallet-mobile-modal/ReferralDashboardView.tsx around lines 388
to 390, the code calls referral.amount.toFixed(1) without guarding against
null/undefined; change it to safely handle missing amounts by using optional
chaining or a default number (e.g., (referral.amount ?? 0).toFixed(1) or
referral.amount?.toFixed(1) ?? "0.0") so the component won’t throw when amount
is absent and the UI shows a sensible fallback.

Copy link

@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 (5)
app/api/referral/claim/route.ts (5)

62-68: Request body parsing is currently a no‑op

The JSON/body check only verifies the presence of a transactionId string and then discards it. If the route does not actually need a body, consider removing this block; if a transactionId is required for auditing or idempotency, enforce and persist it instead.


69-81: Tighten environment/config checks for payout infrastructure

You already 503 when CASHBACK_WALLET_PRIVATE_KEY is missing, and later validate its shape. However, RPC_URL and USDC_CONTRACT_ADDRESS silently fall back to defaults; if those are misconfigured (e.g., placeholder RPC), you’ll create a pending claim that is then marked failed on transfer, requiring manual cleanup. Consider failing fast (503/500) before inserting the claim when any required payout config is missing/invalid, so misconfigured environments don’t produce permanent failed claims.

Also applies to: 245-251


243-275: Clarify/strengthen handling of partial transfer failures and on‑chain client usage

The two writeContract calls are executed sequentially inside a single try, and any thrown error leads to a failed claim with no indication of which leg (referrer vs referred) may have succeeded on‑chain. For operational clarity:

  • Consider capturing and storing each tx hash as soon as it’s available, and differentiating between “partial success” and “no transfers” in the claim record and log messages.
  • Optionally, wrap each transfer in its own try/catch or annotate the error/details payload with which leg failed.

Also, since this depends on the viem wallet client and RPC configuration, it’s worth double‑checking against the current viem docs that the createWalletClient + writeContract usage and RPC URL are aligned with best practices for Base mainnet in your deployed environment.

Also applies to: 300-340


27-61: Ensure x-user-id header is set by trusted middleware only

This route fully trusts x-user-id for wallet resolution and authorization. Assuming middleware injects this header from an authenticated session, it’s fine; but if clients can set it directly, a user could attempt to claim rewards for someone else’s Privy user ID. Please confirm that:

  • x-user-id is only added server‑side (e.g., in middleware) after auth, and
  • direct client-set values are either stripped or ignored.

1-15: Add server-analytics tracking to match other referral endpoints

trackApiRequest, trackApiResponse, trackApiError, and trackBusinessEvent are imported but never used here, while /api/referral/submit is fully instrumented. For observability and funnel analysis, consider:

  • Tracking the incoming request at the top of the handler.
  • Emitting trackApiResponse on successful claim.
  • Using trackApiError in each error path (KYC, volume, DB, transfer failures).
  • Emitting business events such as “Referral Reward Claimed”, “Referral Reward Failed”, etc.

This will make it much easier to monitor the referral program and debug issues in production.

Also applies to: 27-355

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f18e8ae and 03cea8f.

📒 Files selected for processing (1)
  • app/api/referral/claim/route.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
app/api/referral/claim/route.ts (5)
app/api/referral/submit/route.ts (1)
  • POST (12-195)
app/lib/rate-limit.ts (1)
  • withRateLimit (32-59)
app/lib/privy.ts (1)
  • getSmartWalletAddressFromPrivyUserId (48-65)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/api/aggregator.ts (1)
  • fetchKYCStatus (309-355)
🔇 Additional comments (1)
app/api/referral/claim/route.ts (1)

17-21: Verify reward amount and volume thresholds align with product spec

The code pays $1.0 to both referrer and referred user (two transfers, lines 263-275) and enforces a $100 minimum aggregate transaction volume (line 174). Confirm these values match your product spec and PR objectives—particularly regarding eligibility trigger ($20 vs. $100 volume) and payout model (once per claim vs. per transaction). Update constants and user-facing messages if they diverge.

Comment on lines 218 to 228
// Create pending claim record
const { data: pendingClaim, error: claimError } = await supabaseAdmin
.from("referral_claims")
.insert({
referral_id: referral.id,
walletAddress,
reward_amount: REWARD_AMOUNT_USD,
status: "pending",
})
.select()
.single();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify referral_claims schema (wallet address field) and completeness of tx hash storage

  • The inserted field is walletAddress, whereas the rest of your schema uses snake_case (e.g., referred_wallet_address, referrer_wallet_address). If the column in referral_claims is actually wallet_address, this insert will error at runtime. Please confirm the column name and align it with the DB schema.
  • On completion, only referrerTxHash is stored in tx_hash, while both referrerTxHash and referredTxHash are returned to the client. If you need full auditability/reconciliation later, consider storing both hashes (e.g., separate columns or a JSON column) rather than dropping the referred user’s tx hash.

Also applies to: 276-283


I need to verify the concerns by examining the database schema and the actual code. Let me gather this information.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find database schema/migration files
fd -e sql -e ts -e js | grep -i -E "(migration|schema|supabase)" | head -20

Length of output: 228


🏁 Script executed:

#!/bin/bash
# Examine the referral route file at the specified lines
if [ -f "app/api/referral/claim/route.ts" ]; then
  echo "=== Lines 218-228 ===";
  sed -n '218,228p' "app/api/referral/claim/route.ts";
  echo -e "\n=== Lines 276-283 ===";
  sed -n '276,283p' "app/api/referral/claim/route.ts";
fi

Length of output: 803


🏁 Script executed:

#!/bin/bash
# Search for referral_claims table definition or schema references
rg -n "referral_claims" --type ts --type sql -A 5 -B 2 | head -50

Length of output: 2492


🏁 Script executed:

#!/bin/bash
# Search for wallet_address or walletAddress in the codebase
rg -n -i "wallet_address|walletaddress" --type ts --type sql | head -30

Length of output: 3271


🏁 Script executed:

#!/bin/bash
# Look for type definitions or interfaces related to referral claims
rg -n "referral_claim|ReferralClaim" --type ts -A 3 | head -40

Length of output: 1185


I found a critical gap: the referral_claims migration file wasn't located. Let me search more broadly for the table schema definition.


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for referral_claims in all files
rg -n "referral_claims" -A 10

Length of output: 3530


🏁 Script executed:

#!/bin/bash
# Look for schema.sql or any schema definition files
fd -name "*schema*" -o -name "*referral*"

Length of output: 230


🏁 Script executed:

#!/bin/bash
# Check supabase directory for all SQL files and their contents
find supabase -name "*.sql" -exec echo "=== {} ===" \; -exec cat {} \;

Length of output: 10806


🏁 Script executed:

#!/bin/bash
# Search in TypeScript for CREATE TABLE or table setup patterns for referral_claims
rg -n "CREATE TABLE.*referral_claims|referral_claims.*columns|referral_claims.*fields" --type ts --type sql

Length of output: 0


Critical schema mismatch: walletAddress must be wallet_address to match DB pattern, and referral_claims table definition is missing

Verification confirms both concerns in the original review:

  1. Field naming error (lines 218-228): The code inserts walletAddress (camelCase), but every other table in the codebase uses wallet_address (snake_case)—blockfest_cashback_claims, blockfest_participants, saved_recipients, and transactions all follow this pattern. Additionally, no migration file exists for the referral_claims table in supabase/migrations/, which means either the table schema is undefined or the schema definition is missing from version control. This will cause a runtime error.

  2. Incomplete tx hash storage (lines 276-283): Only referrerTxHash is stored in tx_hash (line 281), while both referrerTxHash and referredTxHash are available. The inline comment suggests uncertainty about the schema design. If full auditability is required, store both hashes explicitly (separate columns or JSON structure).

🤖 Prompt for AI Agents
In app/api/referral/claim/route.ts around lines 218-228, the insert uses
camelCase walletAddress and the referral_claims table migration is missing;
change the inserted field to wallet_address to match DB naming conventions and
add/commit a migration file in supabase/migrations that defines the
referral_claims table and its columns (including wallet_address, reward_amount,
status, referral_id, created_at, etc.). Also, for lines ~276-283 where tx_hash
is written, modify the schema and code to persist both referrer and referred
transaction hashes—either by adding explicit columns (referrer_tx_hash and
referred_tx_hash) or by changing tx_hash to store a JSON object containing both
hashes—and update the migration and insert/update calls accordingly so both
hashes are saved for full auditability.

…w components for improved code structure and UI consistency
Copy link

@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: 1

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

22-26: Replace any type with proper ReferralData type.

This is the same issue as in the desktop ReferralDashboard component. Using any defeats TypeScript's benefits and was flagged in a previous review.

+import type { ReferralData } from "../../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);

273-276: Add null check for referral amount to prevent runtime error.

This issue was flagged in a previous review but remains unaddressed. The referral.amount.toFixed(1) call can throw if amount is null/undefined.

                                                         {/* Amount */}
                                                         <p className="text-sm font-medium text-text-secondary dark:text-white/60">
-                                                            {referral.amount.toFixed(1)} USDC
+                                                            {(referral.amount ?? 0).toFixed(1)} USDC
                                                         </p>
🧹 Nitpick comments (5)
app/utils.ts (2)

1222-1233: Consider adding error handling for clipboard API.

The navigator.clipboard.writeText call can fail in certain contexts (e.g., non-secure origins, permission denied, or unsupported browsers). While this is a low-probability failure, silent failures could confuse users who click "Copy" but nothing happens.

 export const handleCopyCode = (
   referralCode: string | undefined,
   onCopied?: (value: boolean) => void
 ): void => {
   if (referralCode) {
-    navigator.clipboard.writeText(referralCode);
-    if (onCopied) {
-      onCopied(true);
-      setTimeout(() => onCopied(false), 2000);
-    }
+    navigator.clipboard.writeText(referralCode)
+      .then(() => {
+        if (onCopied) {
+          onCopied(true);
+          setTimeout(() => onCopied(false), 2000);
+        }
+      })
+      .catch((err) => {
+        console.error("Failed to copy referral code:", err);
+      });
   }
 };

1239-1245: Same clipboard error handling consideration applies here.

Similar to handleCopyCode, the clipboard write is not awaited or error-handled. The toast success message fires immediately regardless of whether the copy actually succeeded.

 export const handleCopyLink = (referralCode: string | undefined): void => {
   if (referralCode) {
     const link = `${window.location.origin}?ref=${referralCode}`;
-    navigator.clipboard.writeText(link);
-    toast.success("Referral link copied!");
+    navigator.clipboard.writeText(link)
+      .then(() => {
+        toast.success("Referral link copied!");
+      })
+      .catch((err) => {
+        console.error("Failed to copy referral link:", err);
+        toast.error("Failed to copy link");
+      });
   }
 };
app/components/ReferralDashboard.tsx (2)

23-27: Replace any type with proper ReferralData type.

Using any defeats TypeScript's type checking benefits. Import and use the ReferralData type from the types file for better type safety and IntelliSense support.

+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);

71-73: Replace any[] with proper type annotation.

Continuing from the type safety concern, the filtered referrals array should also use proper typing to ensure type safety throughout the component.

-    const filteredReferrals: any[] = (referralData?.referrals || []).filter(
-        (r: any) => r.status === activeTab
+    const filteredReferrals = (referralData?.referrals || []).filter(
+        (r) => r.status === activeTab
     );

With proper ReferralData typing on state, TypeScript will infer the correct type for filteredReferrals automatically.

app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1)

71-73: Replace any[] with proper type annotation.

Same as desktop component—with proper ReferralData typing, this can be simplified and TypeScript will infer the correct type.

-    const filteredReferrals: any[] = (referralData?.referrals || []).filter(
-        (r: any) => r.status === activeTab
+    const filteredReferrals = (referralData?.referrals || []).filter(
+        (r) => r.status === activeTab
     );
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 03cea8f and cbbedb9.

⛔ Files ignored due to path filters (8)
  • 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
📒 Files selected for processing (3)
  • app/components/ReferralDashboard.tsx (1 hunks)
  • app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1 hunks)
  • app/utils.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-10-10T16:44:32.125Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 244
File: app/components/CopyAddressWarningModal.tsx:48-52
Timestamp: 2025-10-10T16:44:32.125Z
Learning: In the CopyAddressWarningModal component (app/components/CopyAddressWarningModal.tsx), selectedNetwork from useNetwork() is always defined and does not require null safety checks when accessing its properties like selectedNetwork.chain.name.

Applied to files:

  • app/components/wallet-mobile-modal/ReferralDashboardView.tsx
  • app/components/ReferralDashboard.tsx
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is true (verification failed/returned "Ok"), the recipient safety alert should display when: isRecipientNameEditable && recipientName && !errors.recipientName && !recipientNameError. The !isFetchingRecipientName check is redundant because recipientName is cleared at fetch start and only populated after fetching completes or when the user manually enters it.

Applied to files:

  • app/components/wallet-mobile-modal/ReferralDashboardView.tsx
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is false (verification succeeded), the recipient safety alert should display when: !isRecipientNameEditable && recipientName && !recipientNameError. The !errors.recipientName check is unnecessary because in non-editable mode the recipient name is displayed as read-only text (not an input field), so form validation errors cannot occur.

Applied to files:

  • app/components/wallet-mobile-modal/ReferralDashboardView.tsx
📚 Learning: 2025-11-06T07:08:54.593Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:271-280
Timestamp: 2025-11-06T07:08:54.593Z
Learning: In the RecipientDetailsForm component (app/components/recipient/RecipientDetailsForm.tsx), Mobile Money institutions always return "Ok" from the fetchAccountName verification endpoint. Therefore, checking for accountName.toLowerCase() === "ok" is sufficient to handle both verification failures and Mobile Money institutions without needing explicit institution type checks in the frontend.

Applied to files:

  • app/components/wallet-mobile-modal/ReferralDashboardView.tsx
🧬 Code graph analysis (1)
app/components/ReferralDashboard.tsx (4)
app/api/aggregator.ts (1)
  • getReferralData (550-599)
app/utils.ts (3)
  • handleCopyCode (1222-1233)
  • handleCopyLink (1239-1245)
  • getAvatarImage (1207-1215)
app/components/AnimatedComponents.tsx (1)
  • sidebarAnimation (117-127)
app/components/ReferralDashboardSkeleton.tsx (1)
  • ReferralDashboardSkeleton (1-64)
🔇 Additional comments (7)
app/utils.ts (1)

1207-1215: LGTM!

The avatar mapping logic correctly uses hex parsing of the address to deterministically select from 8 avatar images. The modulo operation ensures valid indices, and the special case for index 0 is handled cleanly.

app/components/ReferralDashboard.tsx (3)

29-61: LGTM! Data fetching logic is well-structured.

The useEffect properly handles:

  • Early exit when panel is closed
  • Mounted flag to prevent state updates after unmount
  • Loading state management in finally block
  • Error handling with toast notification
  • Cleanup function for mounted flag

75-99: LGTM! Loading skeleton implementation is well-integrated.

The loading state properly renders the skeleton within the same modal structure, providing a seamless loading experience while maintaining the dialog's accessibility features.


163-184: Good accessibility implementation on copy button.

The aria-pressed attribute correctly reflects the copied state, and the button is properly disabled when there's no referral code. The visual feedback with icon swap is user-friendly.

app/components/wallet-mobile-modal/ReferralDashboardView.tsx (3)

28-60: LGTM! Data fetching implementation correctly uses live API.

The previous review feedback has been addressed—the component now properly fetches live data via getReferralData instead of using mock data. The mounted flag pattern prevents memory leaks and state updates after unmount.


124-135: Good accessibility and UX in header section.

The close button includes proper aria-label for screen readers, and the button has appropriate hover states for both light and dark themes.


140-157: LGTM! Safe optional chaining on totals.

The referralData?.total_earned?.toFixed(1) ?? "0.0" pattern correctly handles null/undefined cases, addressing the previous review feedback about .toFixed crash on initial render.

Copy link

@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: 2

🧹 Nitpick comments (3)
app/api/v1/analytics/track/route.ts (1)

85-104: Minor: regex can be simplified since input is already lowercased.

Since walletAddress is lowercased on lines 60/62, the uppercase character class [a-fA-F] will never match. Consider simplifying for clarity:

-        if (!/^0x[a-fA-F0-9]{40}$/.test(walletAddress)) {
+        if (!/^0x[a-f0-9]{40}$/.test(walletAddress)) {
app/api/referral/claim/route.ts (2)

186-227: Tighten failure semantics and DB error handling around claim status updates

Two related points in the claim / transfer section:

  1. Failed-transfer path doesn’t check the DB update result

    In the catch (transferError) block you do:

    await supabaseAdmin
      .from("referral_claims")
      .update({ status: "failed", updated_at: new Date().toISOString() })
      .eq("id", pendingClaim.id);

    but never inspect or log the error. If that update fails, you end up with on-chain failure or partial success and no reliable DB record that the claim is in a failed state.

    Suggest mirroring the success-path pattern:

  •    await supabaseAdmin
    
  •        .from("referral_claims")
    
  •        .update({
    
  •            status: "failed",
    
  •            updated_at: new Date().toISOString(),
    
  •        })
    
  •        .eq("id", pendingClaim.id);
    
  •    const { error: failUpdateError } = await supabaseAdmin
    
  •        .from("referral_claims")
    
  •        .update({
    
  •            status: "failed",
    
  •            updated_at: new Date().toISOString(),
    
  •        })
    
  •        .eq("id", pendingClaim.id);
    
  •    if (failUpdateError) {
    
  •        console.error("Failed to mark referral claim as failed:", failUpdateError);
    
  •    }
    
    
    
  1. Partial payout vs. “failed” status is opaque

    Because transfers occur sequentially (referrer then referred), it’s possible for the first transfer to succeed and the second (or the subsequent DB update) to fail. In that case:

    • The referrer may have actually received funds on-chain.
    • The claim is marked "failed" (if the status update succeeds), and the parent referral remains "pending".
    • No tx hashes are stored in the failure path, making manual reconciliation harder.

    If you expect ops to manually reconcile and potentially re-run failed payouts, you may want to:

    • Store any tx hashes you do get, even in failure cases, and/or
    • Introduce a more explicit status such as "partial" vs "failed", and
    • Decide whether non-completed statuses should be retriable or always require manual intervention.

These aren’t blockers for v1, but tightening this will make on-chain vs. DB state easier to reason about and debug when something goes wrong.

Also applies to: 343-365, 371-400


434-445: Log the unexpected error in the outer catch for debuggability

The outer catch (err) returns a generic 500 without logging the underlying error, which makes it hard to debug issues that escape the inner try/catch blocks.

Consider logging before returning:

-    } catch (err) {
-        return NextResponse.json(
+    } catch (err) {
+        console.error("Unhandled error in referral claim route:", err);
+        return NextResponse.json(
             {
                 success: false,
                 error: "Internal server error",
                 code: "INTERNAL_ERROR",
                 message:
                     "An unexpected error occurred while processing your referral claim. Please try again or contact support.",
                 response_time_ms: Date.now() - start,
             },
             { status: 500 },
         );
     }

That keeps client behavior unchanged while improving observability for rare edge cases.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 36aa221 and d65dff6.

📒 Files selected for processing (3)
  • app/api/referral/claim/route.ts (1 hunks)
  • app/api/v1/analytics/track/route.ts (1 hunks)
  • app/components/ReferralDashboard.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • app/components/ReferralDashboard.tsx
🧰 Additional context used
🧬 Code graph analysis (2)
app/api/v1/analytics/track/route.ts (2)
app/lib/rate-limit.ts (1)
  • withRateLimit (32-59)
app/lib/server-analytics.ts (3)
  • trackFundingEvent (318-335)
  • trackAuthEvent (340-357)
  • trackBusinessEvent (362-375)
app/api/referral/claim/route.ts (6)
app/lib/rate-limit.ts (1)
  • withRateLimit (32-59)
app/lib/privy.ts (1)
  • getSmartWalletAddressFromPrivyUserId (48-65)
app/lib/server-config.ts (1)
  • cashbackConfig (40-51)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/api/aggregator.ts (1)
  • fetchKYCStatus (309-355)
app/utils.ts (2)
  • getRpcUrl (159-178)
  • FALLBACK_TOKENS (235-355)
🔇 Additional comments (5)
app/api/v1/analytics/track/route.ts (4)

1-11: LGTM!

Imports are clean and all appear to be used in the implementation.


21-55: LGTM on validation logic.

The defensive JSON parsing with .catch(() => null) is a solid pattern. The async error tracking is duplicated across validation blocks—consider extracting a small helper if this pattern grows, but it's acceptable as-is for fire-and-forget analytics.


141-170: Event routing precedence is order-dependent.

The keyword-based routing works, but note that events containing multiple keywords will match the first condition. For example, "transaction_auth_event" would route to trackTransactionEvent rather than trackAuthEvent. If this is intentional, the current implementation is fine; otherwise, consider more specific matching or documenting the precedence.


188-218: LGTM!

Error handling appropriately avoids leaking internal error details to clients while still tracking the error server-side for observability.

app/api/referral/claim/route.ts (1)

17-135: Auth, referral lookup, and KYC gating are well-structured

Using x-user-id + Privy for wallet resolution, guarding on a pending referral tied to referred_wallet_address, and enforcing two-sided KYC with clear, structured error payloads (code, message, response_time_ms) makes this part of the flow robust and easy to consume from the client side. The Supabase “no rows” handling via PGRST116 is also handled correctly.

Copy link

@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: 1

🧹 Nitpick comments (8)
app/api/v1/analytics/track/route.ts (2)

43-52: Consider adding comments to empty catch blocks for consistency.

Lines 52, 80, and 101 have empty catch blocks without explanatory comments, while lines 181 and 211 include clarifying comments. For consistency and maintainability, add brief comments to all empty catch blocks.

-                } catch (e) { }
+                } catch (e) {
+                    // Ignore tracking errors - fire-and-forget
+                }

141-170: Consider the risk of false positives in event routing logic.

The event routing uses .includes() to match event names, which is flexible but could lead to unintended categorization. For example, an event named "custom_funding_report" would be routed to trackFundingEvent even if it's not actually a funding event.

If event naming conventions are well-controlled, this approach is acceptable. Otherwise, consider using exact matches or prefix/suffix patterns to reduce false positives.

app/utils.ts (2)

1222-1233: Add error handling for clipboard API.

The clipboard API can fail (e.g., in non-secure contexts or when permission is denied). Consider wrapping in try-catch to provide user feedback on failure.

 export const handleCopyCode = (
   referralCode: string | undefined,
   onCopied?: (value: boolean) => void
 ): void => {
   if (referralCode) {
-    navigator.clipboard.writeText(referralCode);
-    if (onCopied) {
-      onCopied(true);
-      setTimeout(() => onCopied(false), 2000);
-    }
+    navigator.clipboard.writeText(referralCode)
+      .then(() => {
+        if (onCopied) {
+          onCopied(true);
+          setTimeout(() => onCopied(false), 2000);
+        }
+      })
+      .catch((err) => {
+        console.error("Failed to copy referral code:", err);
+        toast.error("Failed to copy to clipboard");
+      });
   }
 };

1239-1245: Add error handling for clipboard API in handleCopyLink.

Same concern as handleCopyCode - the clipboard write can fail silently.

 export const handleCopyLink = (referralCode: string | undefined): void => {
   if (referralCode) {
     const link = `${window.location.origin}?ref=${referralCode}`;
-    navigator.clipboard.writeText(link);
-    toast.success("Referral link copied!");
+    navigator.clipboard.writeText(link)
+      .then(() => toast.success("Referral link copied!"))
+      .catch((err) => {
+        console.error("Failed to copy referral link:", err);
+        toast.error("Failed to copy to clipboard");
+      });
   }
 };
app/api/referral/submit/route.ts (1)

110-116: Minor: Consider consistent case normalization.

walletAddress is already lowercase from getSmartWalletAddressFromPrivyUserId, but for defensive coding, consider normalizing both sides:

-        if (referrer.wallet_address.toLowerCase() === walletAddress) {
+        if (referrer.wallet_address.toLowerCase() === walletAddress.toLowerCase()) {
app/api/aggregator.ts (1)

517-544: Consider making accessToken required for submitReferralCode.

The middleware requires authentication for /api/referral/* routes. Making accessToken required would catch missing tokens at compile-time rather than runtime:

 export async function submitReferralCode(
   code: string,
-  accessToken?: string,
+  accessToken: string,
 ): Promise<ApiResponse<SubmitReferralResult>> {
   const headers: Record<string, string> = {
     "Content-Type": "application/json",
   };

-  if (accessToken) {
-    headers.Authorization = `Bearer ${accessToken}`;
-  }
+  headers.Authorization = `Bearer ${accessToken}`;

This ensures callers handle the undefined token case explicitly before calling.

app/api/referral/referral-data/route.ts (2)

60-77: Good collision detection, but consider logging exhaustion.

The 10-attempt limit is reasonable, but if it fails, the error message doesn't indicate this was a collision issue. Consider adding more context:

             if (!code) {
-                throw new Error("Failed to generate unique referral code");
+                throw new Error("Failed to generate unique referral code after 10 attempts - collision rate too high");
             }

36-38: Minor: Endpoint path inconsistency in analytics.

The tracked endpoint is /api/referral/data but the actual file path is /api/referral/referral-data. Consider aligning for easier debugging:

         // Track API request
-        trackApiRequest(request, "/api/referral/data", "GET", {
+        trackApiRequest(request, "/api/referral/referral-data", "GET", {
             wallet_address: walletAddress,
         });

Also applies to lines 211 and 226.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d65dff6 and c7b030a.

📒 Files selected for processing (6)
  • app/api/aggregator.ts (2 hunks)
  • app/api/referral/claim/route.ts (1 hunks)
  • app/api/referral/referral-data/route.ts (1 hunks)
  • app/api/referral/submit/route.ts (1 hunks)
  • app/api/v1/analytics/track/route.ts (1 hunks)
  • app/utils.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
app/api/v1/analytics/track/route.ts (2)
app/lib/rate-limit.ts (1)
  • withRateLimit (32-59)
app/lib/server-analytics.ts (3)
  • trackFundingEvent (318-335)
  • trackAuthEvent (340-357)
  • trackBusinessEvent (362-375)
app/api/referral/referral-data/route.ts (5)
app/lib/rate-limit.ts (1)
  • withRateLimit (32-59)
app/lib/privy.ts (1)
  • getSmartWalletAddressFromPrivyUserId (48-65)
app/lib/supabase.ts (1)
  • supabaseAdmin (11-20)
app/utils.ts (1)
  • generateReferralCode (1251-1258)
app/lib/server-analytics.ts (1)
  • trackBusinessEvent (362-375)
app/api/aggregator.ts (1)
app/types.ts (3)
  • ApiResponse (422-424)
  • SubmitReferralResult (426-426)
  • ReferralData (404-420)
🔇 Additional comments (15)
app/api/v1/analytics/track/route.ts (4)

57-63: Review the security implications of accepting walletAddress from the request body.

The endpoint accepts walletAddress from the request body as a fallback when the x-wallet-address header is not present. This allows any client to track events for any wallet address without authentication. While this may be intentional for flexible client-side tracking, it could enable malicious actors to pollute analytics data or impersonate users.

Verify whether:

  1. The endpoint should require authentication (header-only approach).
  2. Unauthenticated tracking is acceptable for your analytics use case.
  3. Additional rate limiting or abuse prevention is needed.

85-104: LGTM! Wallet address format validation is correct.

The regex correctly validates Ethereum address format (0x followed by 40 hexadecimal characters), and error tracking is properly handled asynchronously.


106-113: LGTM! Immediate response pattern is correct.

The endpoint correctly returns a success response immediately before performing fire-and-forget tracking. The responseTime calculation captures the actual API response time without including tracking overhead.


186-216: LGTM! Error handling follows best practices.

The error handling correctly returns a generic error message to the client while tracking detailed error information asynchronously. This prevents leaking internal implementation details while maintaining observability.

app/utils.ts (2)

1207-1215: LGTM!

Clean implementation using hex parsing for deterministic avatar selection.


1251-1258: LGTM!

The code format (NB + 4 alphanumeric) provides ~1.68M unique combinations. Collision detection is properly handled server-side in the referral-data route.

app/api/referral/submit/route.ts (2)

94-108: LGTM - past feedback addressed.

The error handling now correctly distinguishes between "no rows found" (PGRST116) and actual database errors, matching the pattern used elsewhere in the codebase.


13-175: Well-structured API route.

Good implementation with:

  • Proper authentication check
  • Input validation with format regex
  • Idempotency check for existing referrals
  • Comprehensive analytics tracking
  • Appropriate error handling and status codes
app/api/aggregator.ts (1)

550-599: LGTM - well-typed implementation.

Good improvements:

  • Returns ApiResponse<ReferralData> instead of any
  • Proper early return when accessToken is missing
  • Consistent error handling pattern with axios
app/api/referral/referral-data/route.ts (1)

154-175: LGTM - clear data composition.

The logic correctly shows the "other party" in each referral relationship:

  • As referrer: shows who you referred
  • As referred: shows who referred you

This provides a complete view of the user's referral network.

app/api/referral/claim/route.ts (5)

88-90: Clarify: Is claiming "earned" referrals intentional?

The query allows fetching referrals with status "pending" OR "earned". If a referral is already "earned", should it still be claimable? This might be intentional for separate referrer/referred claims, but worth confirming.


349-355: LGTM - per-user claim design.

The transfer sends $1 to the caller only, with both referrer and referred able to claim separately. The idempotency check at line 208 ensures each user can only claim once per referral.


395-403: Consider: Should failed claims be retryable?

When transfer fails, the claim is marked as "failed". However, the idempotency check at line 225 only returns early for existing claims, meaning a user could have multiple failed claim records. Consider whether:

  1. Failed claims should block retries (current behavior allows retries)
  2. The query should check for existing claims of any status

Current behavior seems reasonable for transient failures, but worth confirming the intended UX.


357-384: Appropriate handling of post-transfer DB update failures.

The code correctly logs errors for manual review when the transfer succeeds but DB updates fail. This is a pragmatic approach given Supabase's transaction limitations.

Consider adding alerting/monitoring for these "MANUAL REVIEW NEEDED" log messages in production.


17-449: Well-structured claim route with comprehensive error handling.

The implementation includes:

  • Proper authentication and authorization checks
  • KYC and transaction volume verification
  • Idempotency via claim record lookup
  • Granular error codes for debugging
  • Direct on-chain transfer with balance verification

The past review comments about error handling (PGRST116 pattern, referral status update) have been addressed.

Comment on lines +13 to +16
// Referral program configuration
const REWARD_AMOUNT_USD = 1;
const MIN_TX_VOLUME_USD = 100;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for references to PR objectives, referral requirements, or documentation
fd -t f \( -name "*.md" -o -name "*.txt" -o -name "*.json" \) | head -20

Repository: paycrest/noblocks

Length of output: 291


🏁 Script executed:

# Search for any comments or references to "$20" or "20 transaction" in the codebase
rg -i "\$20|20.*transaction|transaction.*20" --max-count 20

Repository: paycrest/noblocks

Length of output: 1344


🏁 Script executed:

# Search around the MIN_TX_VOLUME_USD constant for context/comments
rg -B 5 -A 5 "MIN_TX_VOLUME_USD" app/api/referral/claim/route.ts

Repository: paycrest/noblocks

Length of output: 1422


🏁 Script executed:

# Check for any PR-related files or requirements documentation
git ls-files | grep -i -E "(pr|requirement|spec|objective|referral)" | head -20

Repository: paycrest/noblocks

Length of output: 730


Fix MIN_TX_VOLUME_USD threshold mismatch with user-facing requirements.

The UI explicitly promises "$1 on your first $20 transaction" (ReferralModal.tsx, ReferralDashboard.tsx), but the claim endpoint enforces a $100 minimum transaction volume threshold. Update MIN_TX_VOLUME_USD to $20 or correct the UI messaging to match the actual requirement.

🤖 Prompt for AI Agents
In app/api/referral/claim/route.ts around lines 13 to 16, the MIN_TX_VOLUME_USD
constant is set to 100 while the UI promises "$1 on your first $20 transaction";
change MIN_TX_VOLUME_USD to 20 to match the UI (or alternatively update the UI
messaging to reflect the 100 USD threshold if that is intended) so the backend
and frontend thresholds are consistent; update any related tests or comments to
reflect the new minimum and run the app to verify claim logic uses the updated
constant.

Copy link

@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: 0

♻️ Duplicate comments (3)
app/components/ReferralDashboard.tsx (1)

24-27: Use ReferralData instead of any for stronger type‑safety

referralData and filteredReferrals are currently typed as any, which loses the structure guaranteed by getReferralData and weakens autocomplete and compile‑time checks. Reusing the shared ReferralData type here aligns the UI with the API contract and removes the need for explicit any.

You can tighten this up along these lines:

-import { getReferralData } from "@/app/api/aggregator";
+import { getReferralData } from "@/app/api/aggregator";
+import type { ReferralData } from "@/app/types";
@@
-    const [referralData, setReferralData] = useState<any | null>(null);
+    const [referralData, setReferralData] = useState<ReferralData | null>(null);
@@
-    const filteredReferrals: any[] = (referralData?.referrals || []).filter(
-        (r: any) => r.status === activeTab
-    );
+    const filteredReferrals =
+        (referralData?.referrals ?? []).filter((r) => r.status === activeTab);

This keeps the implementation the same while letting TypeScript enforce the data shape.

Also applies to: 71-73

app/components/wallet-mobile-modal/ReferralDashboardView.tsx (2)

23-27: Replace any with ReferralData for referral state

referralData and filteredReferrals are still typed as any, which defeats the benefit of the typed getReferralData response and makes it easier to introduce shape mismatches.

You can align this with the API types like so:

-import { getReferralData } from "@/app/api/aggregator";
+import { getReferralData } from "@/app/api/aggregator";
+import type { ReferralData } from "@/app/types";
@@
-    const [referralData, setReferralData] = useState<any | null>(null);
+    const [referralData, setReferralData] = useState<ReferralData | null>(null);
@@
-    const filteredReferrals: any[] = (referralData?.referrals || []).filter(
-        (r: any) => r.status === activeTab
-    );
+    const filteredReferrals =
+        (referralData?.referrals ?? []).filter((r) => r.status === activeTab);

That keeps runtime behavior unchanged while tightening the types around the referral payload.

Also applies to: 71-73


274-276: Guard referral.amount.toFixed(1) to avoid runtime errors

Here referral.amount.toFixed(1) will throw if amount is ever null/undefined in the API response (e.g., partial data, new backend field, or migration), crashing the whole mobile dashboard.

Use a null‑safe fallback similar to the summary totals:

-                                                                        <p className="text-sm font-medium text-text-secondary dark:text-white/60">
-                                                                            {referral.amount.toFixed(1)} USDC
-                                                                        </p>
+                                                                        <p className="text-sm font-medium text-text-secondary dark:text-white/60">
+                                                                            {(referral.amount ?? 0).toFixed(1)} USDC
+                                                                        </p>

This preserves formatting while making the UI resilient to missing amounts.

🧹 Nitpick comments (1)
app/components/ReferralModal.tsx (1)

31-72: Referral submission flow is solid; consider tightening error handling and validation

Overall flow (local validation → token fetch → submitReferralCode call → success/error toasts + loading state) is correct and matches the product requirements. A couple of small refinements could simplify behavior and avoid misleading messages:

  • The outer catch at Lines 65‑68 currently defaults to "Invalid referral code. Please check and try again.", but errors there are more likely things like token failures or unexpected runtime issues. A generic “Something went wrong” message would be more accurate.
  • You can likely merge the nested try (Lines 48‑64) into the outer try and use a single catch with branching on error type where needed, which will simplify reasoning about the control flow.
  • Because the submit button is already disabled when !referralCode.trim() (Line 125), the empty‑input guard at Lines 31‑35 is effectively redundant and could be dropped to reduce noise.

These are non‑blocking cleanups that would make this handler easier to maintain.

-    const handleSubmit = async () => {
-        if (!referralCode.trim()) {
-            toast.error("Please enter a referral code");
-            return;
-        }
-
-        setIsSubmitting(true);
-
-        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);
-            }
-        } catch (error) {
-            toast.error(
-                error instanceof Error ? error.message : "Invalid referral code. Please check and try again."
-            );
-        } finally {
-            setIsSubmitting(false);
-        }
-    };
+    const handleSubmit = async () => {
+        if (!referralCode.trim()) return;
+
+        setIsSubmitting(true);
+        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();
+            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) {
+            const message =
+                error instanceof Error
+                    ? error.message
+                    : "Something went wrong while submitting your referral code. Please try again.";
+            toast.error(message);
+        } finally {
+            setIsSubmitting(false);
+        }
+    };
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7b030a and f2b546b.

📒 Files selected for processing (3)
  • app/components/ReferralDashboard.tsx (1 hunks)
  • app/components/ReferralModal.tsx (1 hunks)
  • app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-10-10T16:44:32.125Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 244
File: app/components/CopyAddressWarningModal.tsx:48-52
Timestamp: 2025-10-10T16:44:32.125Z
Learning: In the CopyAddressWarningModal component (app/components/CopyAddressWarningModal.tsx), selectedNetwork from useNetwork() is always defined and does not require null safety checks when accessing its properties like selectedNetwork.chain.name.

Applied to files:

  • app/components/wallet-mobile-modal/ReferralDashboardView.tsx
  • app/components/ReferralDashboard.tsx
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is true (verification failed/returned "Ok"), the recipient safety alert should display when: isRecipientNameEditable && recipientName && !errors.recipientName && !recipientNameError. The !isFetchingRecipientName check is redundant because recipientName is cleared at fetch start and only populated after fetching completes or when the user manually enters it.

Applied to files:

  • app/components/wallet-mobile-modal/ReferralDashboardView.tsx
  • app/components/ReferralDashboard.tsx
📚 Learning: 2025-11-06T07:37:39.036Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:539-552
Timestamp: 2025-11-06T07:37:39.036Z
Learning: In RecipientDetailsForm (app/components/recipient/RecipientDetailsForm.tsx), when isRecipientNameEditable is false (verification succeeded), the recipient safety alert should display when: !isRecipientNameEditable && recipientName && !recipientNameError. The !errors.recipientName check is unnecessary because in non-editable mode the recipient name is displayed as read-only text (not an input field), so form validation errors cannot occur.

Applied to files:

  • app/components/wallet-mobile-modal/ReferralDashboardView.tsx
📚 Learning: 2025-11-06T07:08:54.593Z
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 231
File: app/components/recipient/RecipientDetailsForm.tsx:271-280
Timestamp: 2025-11-06T07:08:54.593Z
Learning: In the RecipientDetailsForm component (app/components/recipient/RecipientDetailsForm.tsx), Mobile Money institutions always return "Ok" from the fetchAccountName verification endpoint. Therefore, checking for accountName.toLowerCase() === "ok" is sufficient to handle both verification failures and Mobile Money institutions without needing explicit institution type checks in the frontend.

Applied to files:

  • app/components/wallet-mobile-modal/ReferralDashboardView.tsx
🧬 Code graph analysis (1)
app/components/ReferralDashboard.tsx (4)
app/api/aggregator.ts (1)
  • getReferralData (550-599)
app/utils.ts (3)
  • handleCopyCode (1222-1233)
  • handleCopyLink (1239-1245)
  • getAvatarImage (1207-1215)
app/components/AnimatedComponents.tsx (1)
  • sidebarAnimation (117-127)
app/components/ReferralDashboardSkeleton.tsx (1)
  • ReferralDashboardSkeleton (1-64)
🔇 Additional comments (6)
app/components/ReferralModal.tsx (4)

1-12: Imports and client setup look consistent with usage

"use client" and the selected imports (DialogTitle, motion, Image, useState, toast, usePrivy, AnimatedModal, useNetwork, submitReferralCode) align with how they’re used below; no issues spotted here.


74-78: Skip behavior and state reset are appropriate

handleSkip correctly clears the local referralCode state before delegating to onClose, and wiring it into the modal’s onClose ensures overlay/Escape close behaves the same as the explicit “I don't have a referral code” button.


79-141: Modal UI and UX wiring align well with the referral flow

The modal composition (AnimatedModal + Motion + Headless UI DialogTitle), copy, input configuration, and button states all look consistent and user‑friendly:

  • Network‑aware copy using sponsorChain.
  • Uppercasing and maxLength={6} match the NB‑prefixed 6‑char code format.
  • isSubmitting and disabled flags on the primary button prevent double‑submits while still allowing the user to close.

No functional or structural issues spotted here.


13-30: Defensively optional‑chain chain when deriving sponsorChain

If selectedNetwork exists but selectedNetwork.chain is ever undefined or null, selectedNetwork?.chain.name will still attempt .name access and can throw at runtime. Safer to optional‑chain chain as well.

-    const sponsorChain = selectedNetwork?.chain.name || "Sponsor Chain";
+    const sponsorChain = selectedNetwork?.chain?.name || "Sponsor Chain";
⛔ Skipped due to learnings
Learnt from: Dprof-in-tech
Repo: paycrest/noblocks PR: 244
File: app/components/CopyAddressWarningModal.tsx:48-52
Timestamp: 2025-10-10T16:44:32.125Z
Learning: In the CopyAddressWarningModal component (app/components/CopyAddressWarningModal.tsx), selectedNetwork from useNetwork() is always defined and does not require null safety checks when accessing its properties like selectedNetwork.chain.name.
app/components/ReferralDashboard.tsx (1)

75-99: Desktop referral dashboard structure and loading flow look solid

The loading skeleton, modal layout, tab filtering, copy‑code/link handlers, and defensive rendering of totals (?.toFixed(...) ?? fallback) are all wired in a robust way and match the intended UX for the referral dashboard. No functional issues spotted in this section.

Also applies to: 101-283

app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1)

75-207: Mobile referral dashboard UX and data‑fetch flow look good

The Privy token fetch, referral data loading with skeleton, slide‑up modal behavior, tab filtering, copy‑code/link interactions, and safe summary totals (?.toFixed(...) ?? fallback) are all implemented cleanly and match the desktop pattern. Aside from the small amount/nullability and typing points, this component looks solid.

Also applies to: 209-281

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