feat: implement referral system with API endpoints and UI components#433
feat: implement referral system with API endpoints and UI components#433sundayonah wants to merge 10 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (9)
🚧 Files skipped from review as they are similar to previous changes (7)
📝 WalkthroughWalkthroughAdds a referral program: server APIs for submission, referral-data, and claim processing (with KYC, transaction-volume checks, and USDC payouts), client API helpers, desktop and mobile referral UI (input modal, dashboard, CTA, skeletons), types/config, utilities, Privy auth helper, and middleware routing for referral endpoints. ChangesReferral Program Feature
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
app/lib/privy.tsParsing error: '}' expected. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (13)
app/components/NetworkSelectionModal.tsx (1)
49-55: Remove commented-out dead code.The commented-out
handleClosefunction is no longer needed and adds noise to the file.🧹 Remove dead code
- // const handleClose = () => { - // if (user?.wallet?.address) { - // const storageKey = `hasSeenNetworkModal-${user.wallet.address}`; - // localStorage.setItem(storageKey, "true"); - // } - // setIsOpen(false); - // }; -🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/NetworkSelectionModal.tsx` around lines 49 - 55, Remove the dead/commented-out handleClose function: delete the commented block containing handleClose and its internals (references to user?.wallet?.address, storageKey = `hasSeenNetworkModal-${user.wallet.address}`, localStorage.setItem, and setIsOpen(false)) so the file no longer contains the unused commented code and noise.app/components/ReferralCTA.tsx (1)
5-12: Remove unused import and simplify handler.
useRouteris imported but never used. Also, theif (onViewReferrals)check is redundant since the prop is required (not optional with?).♻️ Proposed cleanup
"use client"; import { motion } from "framer-motion"; import Image from "next/image"; -import { useRouter } from "next/navigation"; export const ReferralCTA = ({ onViewReferrals }: { onViewReferrals: () => void }) => { - const router = useRouter(); - - const handleViewReferrals = () => { - if (onViewReferrals) return onViewReferrals(); - }; return ( <motion.divThen update the button's onClick:
<button type="button" - onClick={handleViewReferrals} + onClick={onViewReferrals} className="min-h-11 w-full rounded-xl bg-accent-gray py-2.5 text-sm font-medium text-gray-900 transition-colors hover:bg-accent-gray/80 dark:bg-white/5 dark:text-white dark:hover:bg-white/10" >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/ReferralCTA.tsx` around lines 5 - 12, Remove the unused useRouter import and simplify the click handler in ReferralCTA: delete the import of useRouter, remove the const router = useRouter() line, and replace the handleViewReferrals implementation so it directly calls the required prop (e.g., const handleViewReferrals = () => onViewReferrals();). Update any button onClick to use handleViewReferrals if not already.app/components/FundWalletForm.tsx (1)
23-32: Use importedMobileSheetViewinstead of duplicating the type.
MobileSheetViewis imported but not used. The localMobileViewtype duplicates it. Consider using the shared type directly to avoid drift and duplication.♻️ Proposed fix
import { Token, type MobileSheetView } from "../types"; import Image from "next/image"; -type MobileView = - | "wallet" - | "settings" - | "transfer" - | "fund" - | "history" - | "referrals"; +type MobileView = MobileSheetView;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/FundWalletForm.tsx` around lines 23 - 32, Replace the duplicated local type alias MobileView with the imported MobileSheetView: remove the local MobileView declaration and update any usages of MobileView in this file (e.g., props, state, handlers) to use MobileSheetView instead so the component relies on the shared MobileSheetView type; ensure the import { MobileSheetView } remains used and run type checks to confirm no remaining references to MobileView.app/components/TransferForm.tsx (1)
30-36: ImportedMobileSheetViewis unused; localMobileViewtype is inconsistent.
MobileSheetViewis imported but not used. The localMobileViewtype on line 36 lacks the"referrals"view that exists in the shared type. Consider usingMobileSheetViewdirectly to ensure consistency across components.♻️ Proposed fix to use shared type
-import { Token, type MobileSheetView } from "../types"; +import { Token, type MobileSheetView } from "../types"; import { networks } from "../mocks"; import { getNetworkImageUrl } from "../utils"; import { useActualTheme } from "../hooks/useActualTheme"; import Image from "next/image"; -type MobileView = "wallet" | "settings" | "transfer" | "fund" | "history"; +type MobileView = MobileSheetView; export const TransferForm: React.FC<{Or simply remove the import if you prefer keeping the local type and just add
"referrals"to it for completeness.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/TransferForm.tsx` around lines 30 - 36, The file defines a local MobileView type but also imports MobileSheetView which is unused and the local type is missing the "referrals" variant; either remove the unused import and add "referrals" to the MobileView union, or (preferably) delete the local MobileView and replace its uses with the shared MobileSheetView type so the component consistently uses MobileSheetView (update any references in TransferForm.tsx to MobileView → MobileSheetView and remove the unused import or local type accordingly).app/components/MobileDropdown.tsx (1)
48-55: Consider using the importedMobileSheetViewtype directly.The
currentViewstate uses an inline union type that duplicates what's likely defined in the importedMobileSheetViewtype. This could lead to type drift ifMobileSheetViewis updated elsewhere.♻️ Suggested refactor
- const [currentView, setCurrentView] = useState< - | "wallet" - | "settings" - | "transfer" - | "fund" - | "history" - | "referrals" - >("wallet"); + const [currentView, setCurrentView] = useState<MobileSheetView>("wallet");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/MobileDropdown.tsx` around lines 48 - 55, Replace the inline union type on the currentView state with the imported MobileSheetView type: change useState<...>("wallet") to useState<MobileSheetView>("wallet") and ensure the imported MobileSheetView symbol is used (and remove the inline union) so currentView and setCurrentView rely on the canonical MobileSheetView type.app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1)
71-73: Consider adding type safety for referral data.The
referralDataandfilteredReferralsare typed asany, which bypasses TypeScript's type checking. Consider defining proper interfaces for the referral data structure.♻️ Suggested type definition
interface Referral { id: string; wallet_address: string; wallet_address_short: string; status: "pending" | "earned"; amount: number; } interface ReferralData { referral_code: string; total_earned: number; total_pending: number; referrals: Referral[]; }Then update:
-const [referralData, setReferralData] = useState<any | null>(null); +const [referralData, setReferralData] = useState<ReferralData | null>(null);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/wallet-mobile-modal/ReferralDashboardView.tsx` around lines 71 - 73, Define proper TypeScript interfaces (e.g., Referral and ReferralData) and replace the any types used for referralData and filteredReferrals in ReferralDashboardView.tsx: type the referralData variable/state/prop as ReferralData | undefined and change filteredReferrals to Referral[] by updating the expression that computes it (the filteredReferrals declaration and any place that consumes referralData.referrals). Also update any function signatures or props that pass referral data (e.g., props or hooks that return referralData) to use the new types so the component benefits from compile-time checking.app/components/MainPageContent.tsx (1)
199-212: Consider adding null-check for wallet address availability.The
handleNetworkSelectedcallback checksuser?.wallet?.addressbutauthenticatedcan be true before the wallet is fully linked. This could cause the modal to be skipped silently if the wallet address isn't available yet.♻️ Suggested defensive check
const handleNetworkSelected = useCallback(() => { - if (!authenticated || !user?.wallet?.address) { + // Wait for wallet to be fully available before checking referral modal + if (!authenticated || !ready || !user?.wallet?.address) { return; } // Check if user has already seen the referral modal const referralStorageKey = `hasSeenReferralModal-${user.wallet.address}`; const hasSeenReferralModal = localStorage.getItem(referralStorageKey); if (!hasSeenReferralModal) { // Show referral modal after network selection setShowReferralModal(true); } - }, [authenticated, user?.wallet?.address]); + }, [authenticated, ready, user?.wallet?.address]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/MainPageContent.tsx` around lines 199 - 212, handleNetworkSelected currently returns early when authenticated is true but user.wallet.address is missing, causing the referral modal to be skipped; update this callback to explicitly null-check user.wallet and user.wallet.address and defer or enqueue showing the modal until the wallet address becomes available instead of silently returning. Specifically, inside handleNetworkSelected check for user and user.wallet first, compute referralStorageKey using user.wallet.address only when present, and if authenticated but address is not yet present either (a) store a temporary flag/state to attempt showing the referral modal once user.wallet.address is populated (use an effect that watches user?.wallet?.address) or (b setShowReferralModal after the address appears); keep references to referralStorageKey, localStorage.getItem, and setShowReferralModal when implementing the fix.app/components/ReferralModal.tsx (1)
39-64: Simplify nested try-catch structure.The nested try-catch at lines 48-64 is redundant since
submitReferralCodealready returns anApiResponseobject rather than throwing. The outer catch at lines 65-68 would only catch errors fromgetAccessToken()which is already handled by the inner try-catch's logic.♻️ Simplified structure
try { const code = referralCode.trim().toUpperCase(); if (!/^NB[A-Z0-9]{4}$/.test(code)) { toast.error("Invalid referral code format"); return; } const token = await getAccessToken(); - - try { - const res = await submitReferralCode(code, token ?? undefined); - - if (res && res.success) { - toast.success(res.data?.message || "Referral code applied! Complete KYC and your first transaction to earn rewards."); - onSubmitSuccess(); - onClose(); - } else { - // API returned a well-formed error response - const message = res && !res.success ? res.error : "Failed to submit referral code. Please try again."; - toast.error(message); - } - } catch (err) { - // Unexpected errors (should be rare since submitReferralCode returns ApiResponse) - const message = err instanceof Error ? err.message : "Failed to submit referral code. Please try again."; - toast.error(message); + const res = await submitReferralCode(code, token ?? undefined); + + if (res && res.success) { + toast.success(res.data?.message || "Referral code applied! Complete KYC and your first transaction to earn rewards."); + onSubmitSuccess(); + onClose(); + } else { + const message = res && !res.success ? res.error : "Failed to submit referral code. Please try again."; + toast.error(message); } } catch (error) { toast.error( - error instanceof Error ? error.message : "Invalid referral code. Please check and try again." + error instanceof Error ? error.message : "Failed to submit referral code. Please try again." ); } finally { setIsSubmitting(false); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/ReferralModal.tsx` around lines 39 - 64, The nested try-catch is unnecessary: remove the inner try/catch around submitReferralCode and instead use a single try/catch that covers getAccessToken() and submitReferralCode(code, token), validate the code with the existing regex (referralCode.trim().toUpperCase()), then check the returned ApiResponse (res && res.success) to call toast.success + onSubmitSuccess() + onClose() or toast.error with res.error/default message; let the outer catch handle any unexpected thrown errors from getAccessToken or submitReferralCode and surface a generic toast.error.app/api/referral/referral-data/route.ts (1)
60-77: Sequential uniqueness checks may be slow under high contention.The collision-check loop runs up to 10 sequential database queries to find a unique code. With a 36^4 (≈1.7M) code space, collisions should be rare, but under heavy concurrent signups this could become a bottleneck.
Consider using a single query with
INSERT ... ON CONFLICT DO NOTHINGand checking the result, or generating multiple candidates and checking them in a singleINquery.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/referral/referral-data/route.ts` around lines 60 - 77, The current loop in route.ts that calls generateReferralCode() and does sequential uniqueness checks against supabaseAdmin.from("users").select(...).eq("referral_code", ...) should be replaced with an atomic approach to avoid many round-trips under contention: either (A) attempt an INSERT into the users/referral table (or a dedicated referrals table) with the generated code using Postgres' INSERT ... ON CONFLICT DO NOTHING and check the insert result to know if the code was accepted, or (B) generate N candidates via generateReferralCode() and issue a single supabaseAdmin.from("users").select("referral_code").in("referral_code", candidates) to filter out collisions and then pick a remaining candidate; implement this logic inside the same function that currently houses the while loop so callers of generateReferralCode/route.ts see the new atomic/batched behavior and avoid the sequential queries.app/components/ReferralDashboard.tsx (1)
24-24: Consider using theReferralDatatype instead ofany.The
referralDatastate is typed asany | null, butReferralDatais already imported in the codebase and matches the API response structure.♻️ Proposed fix for type safety
+import type { ReferralData } from "../types"; + export const ReferralDashboard = ({ isOpen, onClose, }: { isOpen: boolean; onClose: () => void; }) => { const { getAccessToken } = usePrivy(); - const [referralData, setReferralData] = useState<any | null>(null); + const [referralData, setReferralData] = useState<ReferralData | null>(null);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/ReferralDashboard.tsx` at line 24, The state referralData is currently typed as any | null; update its type to use the existing ReferralData type for better type safety by changing the useState declaration for referralData (and setReferralData) to ReferralData | null and adjust any places consuming referralData to satisfy that stronger type (ensuring functions expecting ReferralData still accept the state or are null-guarded).app/api/referral/submit/route.ts (1)
67-79: Consider logging the validation failure for invalid format codes.Format validation rejects codes that don't match
^NB[A-Z0-9]{4}$, but unlike the "missing code" case (lines 50-56), there's notrackApiErrorcall. This makes it harder to detect if users are confused about the format or if there's a UI issue passing malformed codes.📊 Proposed fix to add tracking
// Validate code format (6 characters, starts with NB) if (!/^NB[A-Z0-9]{4}$/.test(normalizedCode)) { + trackApiError( + request, + "/api/referral/submit", + "POST", + new Error("Invalid referral code format"), + 400 + ); return NextResponse.json(🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/referral/submit/route.ts` around lines 67 - 79, Add a tracking call when a referral code fails the format regex: in the same route handler where normalizedCode is computed and the regex /^NB[A-Z0-9]{4}$/ is tested, call trackApiError (the same helper used for the "missing code" branch) before returning the 400 NextResponse.json. Include the invalid normalizedCode and an error code like "INVALID_REFERRAL_FORMAT" in the tracking payload so malformed submissions are logged consistently with the missing-code case.app/api/referral/claim/route.ts (1)
139-155: KYC service failure returns generic 500 error.Per context snippet 1,
fetchKYCStatusthrows on network errors. The catch block (lines 190-202) returns a generic "VERIFICATION_ERROR" with status 500. Users won't know if the issue is with their KYC status or a temporary service outage.Consider catching KYC-specific errors separately to provide better user feedback (e.g., "KYC service temporarily unavailable, please try again").
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/referral/claim/route.ts` around lines 139 - 155, The KYC check should distinguish service/network failures from user verification failures: wrap the fetchKYCStatus call (used to set callerKyc and callerVerified) in its own try/catch and, on network/remote-service errors thrown by fetchKYCStatus, return a specific NextResponse.json with a 503 (or appropriate 5xx) status and a code like "KYC_SERVICE_UNAVAILABLE" and message such as "KYC service temporarily unavailable, please try again"; keep the existing 400 response for callerVerified === false and let the outer catch still handle other unexpected errors.app/api/aggregator.ts (1)
815-828: ThewalletAddressparameter is unused by the backend endpoint.Per the
referral-data/route.tsimplementation (context snippet 1, lines 13-33), the endpoint always uses the authenticated user's wallet address extracted from thex-user-idheader viagetSmartWalletAddressFromPrivyUserId(userId). It does not read anywallet_addressquery parameter from the request.The
walletAddressparameter and the conditional URL construction on lines 826-828 are dead code that may mislead future maintainers into thinking they can fetch referral data for arbitrary wallets.♻️ Proposed fix to remove unused parameter
export async function getReferralData( accessToken: string, - walletAddress?: string, ): Promise<ApiResponse<ReferralData>> { if (!accessToken) { return { success: false, error: "Authentication token is required", }; } - const url = walletAddress - ? `/api/referral/referral-data?wallet_address=${encodeURIComponent(walletAddress)}` - : `/api/referral/referral-data`; + const url = `/api/referral/referral-data`; try {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/aggregator.ts` around lines 815 - 828, The getReferralData function currently accepts a walletAddress and builds a conditional URL, but the backend endpoint (referral-data/route.ts) ignores any query param and always uses the authenticated user's wallet via the x-user-id header and getSmartWalletAddressFromPrivyUserId; remove the unused walletAddress parameter and the conditional URL logic in getReferralData so it always requests `/api/referral/referral-data`, update the function signature (remove walletAddress), adjust all call sites to stop passing a walletAddress, and remove any encodeURIComponent usage tied to this param to avoid dead code and confusion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/api/referral/claim/route.ts`:
- Line 34: The call in route.ts uses an undefined function
getWalletAddressFromPrivyUserId causing a runtime ReferenceError; update the
code to call the correctly imported function
getSmartWalletAddressFromPrivyUserId (or adjust the import to match the intended
function) where walletAddress is assigned so the symbol names match (e.g.,
replace getWalletAddressFromPrivyUserId(userId) with
getSmartWalletAddressFromPrivyUserId(userId) or import
getWalletAddressFromPrivyUserId if that was intended).
- Around line 349-369: walletClient.writeContract currently writes and
immediately returns txHash which is used to mark the claim completed; change the
flow to call publicClient.waitForTransactionReceipt(txHash) after
walletClient.writeContract and only update referral_claims to "completed" when
the receipt shows success (e.g., status === 1); if the receipt indicates failure
or times out, do not mark completed—update the claim to a failure status or log
for manual review using the same supabaseAdmin.update call (referencing
pendingClaim.id and txHash) and ensure errors from waitForTransactionReceipt are
caught and handled.
In `@app/api/referral/referral-data/route.ts`:
- Around line 154-175: The merged referrals array allReferrals currently loses
perspective info, so update the mapping for entries created from
referralsAsReferrer and referralsAsReferred to include a role field (e.g., role:
"referrer" for items from referralsAsReferrer and role: "referred" for items
from referralsAsReferred) so the UI can distinguish relationship direction;
modify the two .map callbacks that build objects (referencing
referralsAsReferrer and referralsAsReferred) to add this role property while
keeping existing fields like id, wallet_address, wallet_address_short, status,
amount, created_at, and completed_at.
In `@app/components/NetworkSelectionModal.tsx`:
- Around line 57-71: handleClose no longer calls markNetworkModalDismissed(),
which prevents the reactive signal used by useIsNetworkModalDismissed() in
MigrationContext from updating; restore the store update by invoking
markNetworkModalDismissed() inside handleClose (before or after setting
localStorage and setIsOpen(false)) so dismissedViaStore flips immediately, and
keep the existing onNetworkSelected timeout behavior unchanged.
- Line 39: The storage key in NetworkSelectionModal is using the raw
user.wallet.address which is inconsistent with the rest of the app; update the
storageKey construction to use user.wallet.address.toLowerCase() and ensure the
modal's handleClose logic (the function named handleClose in
NetworkSelectionModal) reads/writes/removes the same lowercase key so dismissal
and session cleanup match other components that expect lowercase keys.
In `@app/components/ReferralDashboard.tsx`:
- Around line 40-44: The API success branch currently sets referral state but
when response.success is false it only calls setReferralData(null) without user
feedback; update the conditional in ReferralDashboard.tsx (the block checking
mounted && response.success) to call the same toast error used in the catch
block (e.g., toast.error(...)) when mounted && !response.success, passing the
response error/message (fallback to a generic message) and still
setReferralData(null); this keeps UI behavior consistent with the existing catch
handler.
In `@app/components/ReferralModal.tsx`:
- Around line 101-103: Update the referral copy in the ReferralModal component
so the threshold matches the design (change "$100" to "$20"); locate the
paragraph inside ReferralModal.tsx that renders the sentence containing
sponsorChain (the <p className="text-sm text-text-secondary dark:text-white/50">
node) and replace the hardcoded "$100" text with "$20" (or better, use a shared
constant/prop if one exists for referralThreshold to avoid future mismatches).
In `@app/components/wallet-mobile-modal/ReferralDashboardView.tsx`:
- Around line 274-276: In ReferralDashboardView.tsx the JSX directly calls
referral.amount.toFixed(1) which will throw if referral.amount is
undefined/non-numeric; update the rendering to defensively check referral.amount
(e.g., typeof referral.amount === 'number' && Number.isFinite(referral.amount'))
and only call toFixed when valid, otherwise render a safe fallback such as "—"
or "0.0 USDC"; adjust the <p> that currently contains
{referral.amount.toFixed(1)} USDC to use this conditional formatting so
referral.amount is never invoked with toFixed on an invalid value.
In `@app/components/WalletDetails.tsx`:
- Around line 373-384: The CNGN USD calculation currently uses (rate || 1) which
hides missing rates and misrepresents USD; update the conditional rendering in
WalletDetails (the token, balance and rate variables) so that when
token.toUpperCase() === "CNGN" and rate is null/undefined you render a
placeholder or the existing rateError message instead of performing
(balance)/(rate||1); only compute (balance/rate).toFixed(2) when rate is a valid
number, otherwise show a clear fallback like "—" or rateError.
In `@app/utils.ts`:
- Around line 1295-1318: Both handleCopyCode and handleCopyLink must handle
clipboard errors: convert them to async functions and wrap the
navigator.clipboard.writeText(...) call in a try-catch. In handleCopyCode, await
writeText, keep the onCopied(true) / setTimeout(... false) on success, and in
the catch call onCopied(false) if provided and surface the error (e.g.,
toast.error("Failed to copy") or similar). In handleCopyLink, await writeText
and on success call toast.success as now; in the catch call toast.error with a
clear message. This ensures failures (permission/HTTPS unsupported) are handled
gracefully.
---
Nitpick comments:
In `@app/api/aggregator.ts`:
- Around line 815-828: The getReferralData function currently accepts a
walletAddress and builds a conditional URL, but the backend endpoint
(referral-data/route.ts) ignores any query param and always uses the
authenticated user's wallet via the x-user-id header and
getSmartWalletAddressFromPrivyUserId; remove the unused walletAddress parameter
and the conditional URL logic in getReferralData so it always requests
`/api/referral/referral-data`, update the function signature (remove
walletAddress), adjust all call sites to stop passing a walletAddress, and
remove any encodeURIComponent usage tied to this param to avoid dead code and
confusion.
In `@app/api/referral/claim/route.ts`:
- Around line 139-155: The KYC check should distinguish service/network failures
from user verification failures: wrap the fetchKYCStatus call (used to set
callerKyc and callerVerified) in its own try/catch and, on
network/remote-service errors thrown by fetchKYCStatus, return a specific
NextResponse.json with a 503 (or appropriate 5xx) status and a code like
"KYC_SERVICE_UNAVAILABLE" and message such as "KYC service temporarily
unavailable, please try again"; keep the existing 400 response for
callerVerified === false and let the outer catch still handle other unexpected
errors.
In `@app/api/referral/referral-data/route.ts`:
- Around line 60-77: The current loop in route.ts that calls
generateReferralCode() and does sequential uniqueness checks against
supabaseAdmin.from("users").select(...).eq("referral_code", ...) should be
replaced with an atomic approach to avoid many round-trips under contention:
either (A) attempt an INSERT into the users/referral table (or a dedicated
referrals table) with the generated code using Postgres' INSERT ... ON CONFLICT
DO NOTHING and check the insert result to know if the code was accepted, or (B)
generate N candidates via generateReferralCode() and issue a single
supabaseAdmin.from("users").select("referral_code").in("referral_code",
candidates) to filter out collisions and then pick a remaining candidate;
implement this logic inside the same function that currently houses the while
loop so callers of generateReferralCode/route.ts see the new atomic/batched
behavior and avoid the sequential queries.
In `@app/api/referral/submit/route.ts`:
- Around line 67-79: Add a tracking call when a referral code fails the format
regex: in the same route handler where normalizedCode is computed and the regex
/^NB[A-Z0-9]{4}$/ is tested, call trackApiError (the same helper used for the
"missing code" branch) before returning the 400 NextResponse.json. Include the
invalid normalizedCode and an error code like "INVALID_REFERRAL_FORMAT" in the
tracking payload so malformed submissions are logged consistently with the
missing-code case.
In `@app/components/FundWalletForm.tsx`:
- Around line 23-32: Replace the duplicated local type alias MobileView with the
imported MobileSheetView: remove the local MobileView declaration and update any
usages of MobileView in this file (e.g., props, state, handlers) to use
MobileSheetView instead so the component relies on the shared MobileSheetView
type; ensure the import { MobileSheetView } remains used and run type checks to
confirm no remaining references to MobileView.
In `@app/components/MainPageContent.tsx`:
- Around line 199-212: handleNetworkSelected currently returns early when
authenticated is true but user.wallet.address is missing, causing the referral
modal to be skipped; update this callback to explicitly null-check user.wallet
and user.wallet.address and defer or enqueue showing the modal until the wallet
address becomes available instead of silently returning. Specifically, inside
handleNetworkSelected check for user and user.wallet first, compute
referralStorageKey using user.wallet.address only when present, and if
authenticated but address is not yet present either (a) store a temporary
flag/state to attempt showing the referral modal once user.wallet.address is
populated (use an effect that watches user?.wallet?.address) or (b
setShowReferralModal after the address appears); keep references to
referralStorageKey, localStorage.getItem, and setShowReferralModal when
implementing the fix.
In `@app/components/MobileDropdown.tsx`:
- Around line 48-55: Replace the inline union type on the currentView state with
the imported MobileSheetView type: change useState<...>("wallet") to
useState<MobileSheetView>("wallet") and ensure the imported MobileSheetView
symbol is used (and remove the inline union) so currentView and setCurrentView
rely on the canonical MobileSheetView type.
In `@app/components/NetworkSelectionModal.tsx`:
- Around line 49-55: Remove the dead/commented-out handleClose function: delete
the commented block containing handleClose and its internals (references to
user?.wallet?.address, storageKey =
`hasSeenNetworkModal-${user.wallet.address}`, localStorage.setItem, and
setIsOpen(false)) so the file no longer contains the unused commented code and
noise.
In `@app/components/ReferralCTA.tsx`:
- Around line 5-12: Remove the unused useRouter import and simplify the click
handler in ReferralCTA: delete the import of useRouter, remove the const router
= useRouter() line, and replace the handleViewReferrals implementation so it
directly calls the required prop (e.g., const handleViewReferrals = () =>
onViewReferrals();). Update any button onClick to use handleViewReferrals if not
already.
In `@app/components/ReferralDashboard.tsx`:
- Line 24: The state referralData is currently typed as any | null; update its
type to use the existing ReferralData type for better type safety by changing
the useState declaration for referralData (and setReferralData) to ReferralData
| null and adjust any places consuming referralData to satisfy that stronger
type (ensuring functions expecting ReferralData still accept the state or are
null-guarded).
In `@app/components/ReferralModal.tsx`:
- Around line 39-64: The nested try-catch is unnecessary: remove the inner
try/catch around submitReferralCode and instead use a single try/catch that
covers getAccessToken() and submitReferralCode(code, token), validate the code
with the existing regex (referralCode.trim().toUpperCase()), then check the
returned ApiResponse (res && res.success) to call toast.success +
onSubmitSuccess() + onClose() or toast.error with res.error/default message; let
the outer catch handle any unexpected thrown errors from getAccessToken or
submitReferralCode and surface a generic toast.error.
In `@app/components/TransferForm.tsx`:
- Around line 30-36: The file defines a local MobileView type but also imports
MobileSheetView which is unused and the local type is missing the "referrals"
variant; either remove the unused import and add "referrals" to the MobileView
union, or (preferably) delete the local MobileView and replace its uses with the
shared MobileSheetView type so the component consistently uses MobileSheetView
(update any references in TransferForm.tsx to MobileView → MobileSheetView and
remove the unused import or local type accordingly).
In `@app/components/wallet-mobile-modal/ReferralDashboardView.tsx`:
- Around line 71-73: Define proper TypeScript interfaces (e.g., Referral and
ReferralData) and replace the any types used for referralData and
filteredReferrals in ReferralDashboardView.tsx: type the referralData
variable/state/prop as ReferralData | undefined and change filteredReferrals to
Referral[] by updating the expression that computes it (the filteredReferrals
declaration and any place that consumes referralData.referrals). Also update any
function signatures or props that pass referral data (e.g., props or hooks that
return referralData) to use the new types so the component benefits from
compile-time checking.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2e881261-330c-4ae8-8e21-8730f29fe57e
⛔ Files ignored due to path filters (11)
public/images/avatar/Avatar.pngis excluded by!**/*.pngpublic/images/avatar/Avatar1.pngis excluded by!**/*.pngpublic/images/avatar/Avatar2.pngis excluded by!**/*.pngpublic/images/avatar/Avatar3.pngis excluded by!**/*.pngpublic/images/avatar/Avatar4.pngis excluded by!**/*.pngpublic/images/avatar/Avatar5.pngis excluded by!**/*.pngpublic/images/avatar/Avatar6.pngis excluded by!**/*.pngpublic/images/avatar/Avatar7.pngis excluded by!**/*.pngpublic/images/referral-cta-dollar.pngis excluded by!**/*.pngpublic/images/referral-cta.pngis excluded by!**/*.pngpublic/images/referral-graphic.pngis excluded by!**/*.png
📒 Files selected for processing (22)
app/api/aggregator.tsapp/api/referral/claim/route.tsapp/api/referral/referral-data/route.tsapp/api/referral/submit/route.tsapp/components/FundWalletForm.tsxapp/components/MainPageContent.tsxapp/components/MobileDropdown.tsxapp/components/NetworkSelectionModal.tsxapp/components/ReferralCTA.tsxapp/components/ReferralDashboard.tsxapp/components/ReferralDashboardSkeleton.tsxapp/components/ReferralDashboardViewSkeleton.tsxapp/components/ReferralModal.tsxapp/components/TransferForm.tsxapp/components/WalletDetails.tsxapp/components/index.tsapp/components/wallet-mobile-modal/ReferralDashboardView.tsxapp/components/wallet-mobile-modal/WalletView.tsxapp/components/wallet-mobile-modal/index.tsapp/types.tsapp/utils.tsmiddleware.ts
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (1)
app/utils.ts (1)
1295-1327:⚠️ Potential issue | 🟡 MinorAwait clipboard writes before reporting success.
navigator.clipboard.writeText()is async. The currenttry/catchonly handles synchronous failures, so rejected writes still mark the code as copied or fire the success toast. Make both helpers wait for the clipboard promise before updating UI state.Proposed fix
export const handleCopyCode = ( referralCode: string | undefined, onCopied?: (value: boolean) => void ): void => { if (referralCode) { - try { - navigator.clipboard.writeText(referralCode); - if (onCopied) { - onCopied(true); - setTimeout(() => onCopied(false), 2000); - } - } catch (error) { - console.error("Failed to copy referral code:", error); - } + void navigator.clipboard.writeText(referralCode) + .then(() => { + onCopied?.(true); + setTimeout(() => onCopied?.(false), 2000); + }) + .catch((error) => { + onCopied?.(false); + console.error("Failed to copy referral code:", error); + toast.error("Failed to copy referral code"); + }); } }; export const handleCopyLink = (referralCode: string | undefined): void => { if (referralCode) { const link = `${window.location.origin}?ref=${referralCode}`; - try { - navigator.clipboard.writeText(link); - toast.success("Referral link copied!"); - } catch (error) { - console.error("Failed to copy referral link:", error); - toast.error("Failed to copy link"); - } + void navigator.clipboard.writeText(link) + .then(() => { + toast.success("Referral link copied!"); + }) + .catch((error) => { + console.error("Failed to copy referral link:", error); + toast.error("Failed to copy link"); + }); } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/utils.ts` around lines 1295 - 1327, The clipboard write calls in handleCopyCode and handleCopyLink are not awaited so rejected promises still trigger success UI; make both functions async (returning Promise<void>), await navigator.clipboard.writeText(...) inside their existing try/catch blocks, and only call onCopied(true)/setTimeout(...) and toast.success(...) after the awaited call succeeds; keep the catch to log the error and show toast.error or call onCopied(false) as appropriate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/api/referral/claim/route.ts`:
- Around line 89-90: Change the status filter to only accept already-earned
referrals by removing "pending" from the .in("status", ["pending","earned"])
calls (search for that exact expression) so claims cannot be made against
pending referrals; ensure the volume/KYC check uses the referred user's identity
(not the caller/referrer) when evaluating the qualifying transaction threshold;
replace any hard-coded $100 threshold with the $20/first-qualifying-transaction
rule (search for numeric literal 100 used in referral checks); and only flip the
parent referral to "earned" (the code that updates status to "earned") after the
payout completes, not before.
- Around line 204-209: The current check using existingClaim (from
supabaseAdmin.from("referral_claims").select(...).single()) is race-prone; add a
unique DB constraint on (referral_id, wallet_address) and change the code that
creates claims to perform a single atomic upsert/insert-with-conflict-handling
instead of separate read+insert. Specifically, add the unique constraint in your
migrations for referral_claims, replace the read-then-create pattern that
populates existingClaim with an insert/upsert call (using Supabase/Postgres ON
CONFLICT or Supabase .upsert/.insert(...).onConflict equivalent) that returns
the existing row on conflict, and in the route handler check the returned
row/error to decide whether to call writeContract; also defensively catch a
duplicate-key DB error (e.g. Postgres 23505) and treat it as “already claimed”
to prevent double payouts (apply same change to the other create path referenced
around lines 238-247).
- Around line 225-236: The current early-return uses existingClaim to
short-circuit all statuses; change the logic so only existingClaim.status ===
"completed" returns immediately (keep the NextResponse.json shape with amount:
existingClaim.reward_amount, status, txHash: existingClaim.tx_hash), and for
statuses "pending" or "failed" do NOT return—allow the handler to continue to
attempt retry/recovery (e.g., re-validate wallet/balance and update or recreate
the claim). Apply the same change to the other similar blocks that check
existingClaim and return (the other occurrences handling status checks and
NextResponse.json) so only "completed" is terminal and transient
"pending"/"failed" flows can proceed.
In `@app/api/referral/referral-data/route.ts`:
- Around line 156-175: The mapping that builds referral DTOs is using the
boolean-or operator (e.g., r.reward_amount || 1.0 and r.amount || 0) which
treats an explicit 0 as missing; change those to nullish coalescing (use ??) so
explicit zero reward values are preserved (update occurrences in the
referralsAsReferrer and referralsAsReferred mapping blocks and the later mapping
that uses r.amount). Ensure the default fallback values remain the same (1.0 or
0) but use r.reward_amount ?? 1.0 and r.amount ?? 0 (or equivalent) so zero is
not overridden.
- Around line 54-109: The referral-code generation is race-prone; add a DB-level
unique constraint/index on users.referral_code and replace the preflight
select+check flow in the block using generateReferralCode() with a retrying
upsert that attempts to write the generated code and, on unique-constraint
conflict from supabaseAdmin.from("users").upsert (handle the Postgres
unique-violation error returned by Supabase), regenerates and retries up to
maxAttempts before failing; keep the upsert target (wallet_address) and
updated_at logic, set isNewlyGenerated only after a successful upsert returning
referral_code, and remove the separate .select(...) existence check to rely on
DB enforcement.
In `@app/components/ReferralDashboard.tsx`:
- Around line 34-39: In fetchData (ReferralDashboard) handle the case when
getAccessToken() returns null instead of early-returning: set referralData to
null/empty and set an auth error state (or call the existing error handler)
before letting the finally block clear loading so the UI can show an auth error
rather than the empty-dashboard UI; apply the same change to the analogous
branch in wallet-mobile-modal/ReferralDashboardView.tsx. Specifically, update
the token-null branch in fetchData and the corresponding place that calls
getAccessToken() to call setReferralData(null) or setAuthError(...) and avoid
silently returning while leaving prior referral state in place.
---
Duplicate comments:
In `@app/utils.ts`:
- Around line 1295-1327: The clipboard write calls in handleCopyCode and
handleCopyLink are not awaited so rejected promises still trigger success UI;
make both functions async (returning Promise<void>), await
navigator.clipboard.writeText(...) inside their existing try/catch blocks, and
only call onCopied(true)/setTimeout(...) and toast.success(...) after the
awaited call succeeds; keep the catch to log the error and show toast.error or
call onCopied(false) as appropriate.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: b29fb6cf-f3eb-4040-95c8-8f4e5e92f363
📒 Files selected for processing (7)
app/api/referral/claim/route.tsapp/api/referral/referral-data/route.tsapp/components/ReferralDashboard.tsxapp/components/TransferForm.tsxapp/components/wallet-mobile-modal/ReferralDashboardView.tsxapp/types.tsapp/utils.ts
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (4)
app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1)
214-217:⚠️ Potential issue | 🟠 MajorKeep the reward-threshold copy consistent here too.
This duplicate string still says
$100even though the backend and the PR contract are$20. It should be updated together withapp/components/ReferralDashboard.tsxso desktop and mobile agree.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/components/wallet-mobile-modal/ReferralDashboardView.tsx` around lines 214 - 217, Update the reward-threshold copy in the ReferralDashboardView component so it matches the backend/PR contract ($20) and stays consistent with the desktop component ReferralDashboard; locate the paragraph string inside ReferralDashboardView (the <p> with className "mb-6 text-sm leading-relaxed text-text-secondary dark:text-white/60") and change the "$100" mention to "$20" to mirror the copy used in app/components/ReferralDashboard.tsx.app/api/referral/claim/route.ts (2)
179-195:⚠️ Potential issue | 🔴 CriticalThis rewards aggregate volume, not the promised first qualifying transaction.
The current check sums every completed transaction on the qualifying wallet. That lets multiple smaller transfers satisfy the rule, even though the acceptance criteria here are a first qualifying
$20transaction. Eligibility needs to be driven by a single qualifying transaction, not lifetime aggregate volume.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/referral/claim/route.ts` around lines 179 - 195, The code currently sums all completed transactions (volTxs) to compute totalUsd, but the rule requires checking for a single first qualifying transaction >= MIN_QUALIFYING_VOLUME_USD; replace the aggregation with logic that finds the earliest completed transaction for qualifyingWallet whose amount_usd or amount_received is >= MIN_QUALIFYING_VOLUME_USD (e.g., query/select created_at and order by created_at asc or filter in SQL), then set meetsVolume to true only if that single qualifying transaction exists (and use that transaction’s amount for any further checks) instead of summing volTxs; keep the existing error handling around volTxError and reference volTxs, volTxError, qualifyingWallet, and MIN_QUALIFYING_VOLUME_USD when making the change.
264-379:⚠️ Potential issue | 🔴 CriticalA single pending claim can still broadcast twice.
Every path that finds or creates a
pendingrow proceeds straight towriteContract. A concurrent request can reuse the samependingClaim, and any failure after broadcast drops back tofailed, so a retry can resend USDC even if the first transfer is already in flight or mined. This needs an atomicpending -> processinghandoff and to persisttx_hashas soon as the broadcast succeeds; otherwise the claim table is not a real idempotency barrier.Also applies to: 471-550
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/referral/claim/route.ts` around lines 264 - 379, The current flow allows two concurrent requests to grab the same pendingClaimRow and both call writeContract; fix by making the handoff atomic: when you find/insert a pending row (variable pendingClaimRow) immediately perform an update on referral_claims that sets status = "processing" (or similar) with a conditional WHERE id = pendingClaimRow.id AND status = "pending" (so the update only succeeds for the first request) and use the returned row to decide whether to proceed to writeContract; after broadcasting, persist tx_hash and set status = "completed" (or set status back to "failed" on error) via another update, and apply the same atomic conditional update pattern for the other duplicate handling branch that also proceeds to writeContract.app/api/referral/referral-data/route.ts (1)
56-99:⚠️ Potential issue | 🟠 MajorCode auto-generation can still overwrite itself on first load.
Two concurrent first opens for the same wallet can both enter this block and both
upsert(..., { onConflict: "wallet_address" }). The later write replaces the earlier code, so one client can display or copy a referral code that no longer exists inusers.referral_code. This still needs a database-enforced uniqueusers.referral_codeplus a write path that only fills a missing code instead of overwriting an existing one.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/referral/referral-data/route.ts` around lines 56 - 99, Add a DB-constraint and make the write idempotent: ensure users.referral_code has a UNIQUE constraint in the database, then stop using upsert to assign the generated code; instead attempt to atomically set the code only when it is currently missing (e.g., an UPDATE on users where wallet_address = walletAddress AND referral_code IS NULL, returning the referral_code) and retry on unique-violation errors (generateReferralCode + attempted UPDATE) until success or attempts exhausted; reference the generateReferralCode helper, the supabaseAdmin write path (replace the upsert(...) onConflict logic), and the users.referral_code and wallet_address columns when making these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/api/referral/claim/route.ts`:
- Around line 99-104: The referral lookup is incorrectly filtering by
.eq("status", "pending") which makes a referral unclaimable after the first
payout; change the query that fetches the referral (the supabaseAdmin
.from("referrals") .select("*") .eq("id", referralId) call) to load the referral
regardless of status, then consult the referral_claims table to determine if the
current wallet has already been paid (use the existing referral_claims logic to
decide idempotency) and only flip the parent referral to "earned" when
appropriate; apply the same fix to the other occurrences referenced (the blocks
around lines 229-262 and 519-526) so all claim paths load referrals without the
status filter and rely on referral_claims for payout checks.
In `@app/api/referral/submit/route.ts`:
- Around line 81-97: Add a DB uniqueness constraint on
referrals.referred_wallet_address and stop relying on the preflight read;
instead, attempt the insert with supabaseAdmin.from("referrals").insert(...) and
handle the unique-conflict error as a 409 "You have already used a referral
code". Update the code paths that currently check existingReferral (the
preflight select and the identical check around lines with
existingReferral/existingError) to remove the race-prone read and catch the
insert error (Postgres unique-violation) for the same user; also apply the same
change to the other submit branch noted (around the second block 135-150) so
both create paths are atomic and return the same 409 on unique constraint
violation.
In `@app/components/ReferralDashboard.tsx`:
- Around line 209-212: Referral copy in ReferralDashboard.tsx and the duplicated
string in wallet-mobile-modal/ReferralDashboardView.tsx incorrectly says "$100"
instead of the backend/default "$20"; update the UI copy in both components
(ReferralDashboard and ReferralDashboardView) to read "$20" and, if feasible,
centralize the threshold text into a single constant or i18n key to avoid future
divergence (e.g., create a REFERRAL_THRESHOLD constant used by both components).
---
Duplicate comments:
In `@app/api/referral/claim/route.ts`:
- Around line 179-195: The code currently sums all completed transactions
(volTxs) to compute totalUsd, but the rule requires checking for a single first
qualifying transaction >= MIN_QUALIFYING_VOLUME_USD; replace the aggregation
with logic that finds the earliest completed transaction for qualifyingWallet
whose amount_usd or amount_received is >= MIN_QUALIFYING_VOLUME_USD (e.g.,
query/select created_at and order by created_at asc or filter in SQL), then set
meetsVolume to true only if that single qualifying transaction exists (and use
that transaction’s amount for any further checks) instead of summing volTxs;
keep the existing error handling around volTxError and reference volTxs,
volTxError, qualifyingWallet, and MIN_QUALIFYING_VOLUME_USD when making the
change.
- Around line 264-379: The current flow allows two concurrent requests to grab
the same pendingClaimRow and both call writeContract; fix by making the handoff
atomic: when you find/insert a pending row (variable pendingClaimRow)
immediately perform an update on referral_claims that sets status = "processing"
(or similar) with a conditional WHERE id = pendingClaimRow.id AND status =
"pending" (so the update only succeeds for the first request) and use the
returned row to decide whether to proceed to writeContract; after broadcasting,
persist tx_hash and set status = "completed" (or set status back to "failed" on
error) via another update, and apply the same atomic conditional update pattern
for the other duplicate handling branch that also proceeds to writeContract.
In `@app/api/referral/referral-data/route.ts`:
- Around line 56-99: Add a DB-constraint and make the write idempotent: ensure
users.referral_code has a UNIQUE constraint in the database, then stop using
upsert to assign the generated code; instead attempt to atomically set the code
only when it is currently missing (e.g., an UPDATE on users where wallet_address
= walletAddress AND referral_code IS NULL, returning the referral_code) and
retry on unique-violation errors (generateReferralCode + attempted UPDATE) until
success or attempts exhausted; reference the generateReferralCode helper, the
supabaseAdmin write path (replace the upsert(...) onConflict logic), and the
users.referral_code and wallet_address columns when making these changes.
In `@app/components/wallet-mobile-modal/ReferralDashboardView.tsx`:
- Around line 214-217: Update the reward-threshold copy in the
ReferralDashboardView component so it matches the backend/PR contract ($20) and
stays consistent with the desktop component ReferralDashboard; locate the
paragraph string inside ReferralDashboardView (the <p> with className "mb-6
text-sm leading-relaxed text-text-secondary dark:text-white/60") and change the
"$100" mention to "$20" to mirror the copy used in
app/components/ReferralDashboard.tsx.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 773bac82-c667-46bc-8e1a-a5f73e89d506
📒 Files selected for processing (6)
app/api/referral/claim/route.tsapp/api/referral/referral-data/route.tsapp/api/referral/submit/route.tsapp/components/ReferralDashboard.tsxapp/components/wallet-mobile-modal/ReferralDashboardView.tsxapp/lib/privy.ts
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
app/api/referral/referral-data/route.ts (1)
251-262: Consider forwarding only the Authorization header for background claim requests.The background fetch manually sets
x-user-idheader (line 258). While theuserIdoriginates from the validated request, directly setting this header bypasses the normal authentication flow. The claim route'sgetPrivyUserIdFromRequesttrustsx-user-idheaders without re-validation when present.If middleware doesn't run on this internal fetch (or fails silently), the manually-set
x-user-idwould be trusted directly. Relying solely on the Authorization header would ensure the claim route re-validates the user.♻️ Proposed safer approach
if (claimableReferrals.length > 0) { const authHeader = request.headers.get("Authorization"); + if (!authHeader) { + // Skip auto-claim if no auth header to forward + console.warn("Auto-claim skipped: no Authorization header to forward"); + } else { const origin = request.headers.get("origin") || `https://${request.headers.get("host")}`; fetch(`${origin}/api/referral/claim`, { method: "GET", headers: { - ...(authHeader ? { Authorization: authHeader } : {}), - "x-user-id": userId!, + Authorization: authHeader, }, }).catch((e) => console.error("Auto-claim background request failed:", e), ); + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/referral/referral-data/route.ts` around lines 251 - 262, The background auto-claim fetch in the claimableReferrals handling should not set x-user-id directly; update the fetch call inside route.ts (the block using claimableReferrals and fetch(...)) to remove the "x-user-id": userId! header and forward only the Authorization header (authHeader) so the claim route's getPrivyUserIdFromRequest and normal auth middleware re-validate the user; ensure nothing else depends on x-user-id being present for this internal request.app/api/referral/claim/route.ts (1)
266-274: Consider adding an explicit timeout towaitForTransactionReceiptto fail faster on issues.While viem's
waitForTransactionReceipthas a default timeout of 3 minutes (180,000 ms), explicitly setting a shorter timeout (e.g., 60 seconds) is a good practice for API requests to fail faster if the transaction gets stuck or the RPC node becomes unresponsive. This prevents lengthy request hangs and improves user experience.⏱️ Proposed fix with timeout
- const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 1 }); + const receipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + confirmations: 1, + timeout: 60_000, // 60 second timeout + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/api/referral/claim/route.ts` around lines 266 - 274, Add an explicit timeout to the viem waitForTransactionReceipt call (e.g., timeout: 60_000) so the function fails faster if the RPC stalls; update the call to publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 1, timeout: 60000 }) and add a try/catch around it to handle timeout/errors, ensuring you mark the referral_claims row for pendingClaim.id as failed via supabaseAdmin.from("referral_claims").update({ status: "failed", updated_at: new Date().toISOString() }).eq("id", pendingClaim.id) and return a failure response (e.g., code "TRANSFER_TIMEOUT" or similar) when the wait times out or throws.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.env.example:
- Around line 143-145: The dotenv entries
NEXT_PUBLIC_REFERRAL_MIN_QUALIFYING_VOLUME_USD and
NEXT_PUBLIC_REFERRAL_REWARD_AMOUNT_USD currently have inline comments that may
be parsed as part of the value; move the comments to their own lines above each
variable (or remove inline trailing comments) so the variables remain plain
numeric values (e.g., place a line like "# in USDC" immediately above each
variable) to avoid ValueWithoutQuotes/parsing issues.
In `@app/api/referral/claim/route.ts`:
- Around line 54-62: Wrap the call to fetchKYCStatus(qualifyingWallet) in a
try-catch inside the route handler so network/API exceptions don't bubble up; in
the catch, log the error and return a structured response (e.g., { success:
false, code: "KYC_SERVICE_UNAVAILABLE", message: "Unable to verify KYC at this
time. Please try again later." }) instead of allowing an internal server error,
while keeping the existing verified-status check for kyc?.data?.status !==
"verified".
In `@app/lib/config.ts`:
- Around line 47-50: The two config values referralMinQualifyingVolumeUsd and
referralRewardAmountUsd are currently parsed with Number(...) and can become NaN
for invalid env values; update their parsing to mirror the other numeric configs
by using Number(...) then validate with Number.isFinite(...) and fall back to 0
(or the existing safe default) when not finite so these fields never end up as
NaN and comparisons behave correctly; modify the initialization for
referralMinQualifyingVolumeUsd and referralRewardAmountUsd to perform this check
and assign the safe default.
---
Nitpick comments:
In `@app/api/referral/claim/route.ts`:
- Around line 266-274: Add an explicit timeout to the viem
waitForTransactionReceipt call (e.g., timeout: 60_000) so the function fails
faster if the RPC stalls; update the call to
publicClient.waitForTransactionReceipt({ hash: txHash, confirmations: 1,
timeout: 60000 }) and add a try/catch around it to handle timeout/errors,
ensuring you mark the referral_claims row for pendingClaim.id as failed via
supabaseAdmin.from("referral_claims").update({ status: "failed", updated_at: new
Date().toISOString() }).eq("id", pendingClaim.id) and return a failure response
(e.g., code "TRANSFER_TIMEOUT" or similar) when the wait times out or throws.
In `@app/api/referral/referral-data/route.ts`:
- Around line 251-262: The background auto-claim fetch in the claimableReferrals
handling should not set x-user-id directly; update the fetch call inside
route.ts (the block using claimableReferrals and fetch(...)) to remove the
"x-user-id": userId! header and forward only the Authorization header
(authHeader) so the claim route's getPrivyUserIdFromRequest and normal auth
middleware re-validate the user; ensure nothing else depends on x-user-id being
present for this internal request.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: d7af56ce-912a-4b5b-b405-03960af74336
📒 Files selected for processing (5)
.env.exampleapp/api/referral/claim/route.tsapp/api/referral/referral-data/route.tsapp/lib/config.tsapp/types.ts
cac70dc to
447a769
Compare
* Added new API routes for submitting and retrieving referral data. * Introduced referral-related types and utility functions for handling referral codes. * Created UI components for referral dashboard and call-to-action, enhancing user engagement. * Updated mobile dropdown and main page content to include referral options and modals. * Enhanced middleware to support new referral API routes.
* Added optional role property to referral data structure for better clarity. * Improved clipboard copy functionality with error handling for referral codes and links. * Updated referral data retrieval to include role information for referrers and referred users. * Enhanced error handling in the ReferralDashboard and ReferralDashboardView components for better user feedback.
* Refactored referral API routes to utilize a new method for retrieving user IDs from requests. * Updated referral data retrieval to ensure accurate wallet address handling. * Enhanced error handling for referral code generation and transaction volume checks. * Improved user feedback in the ReferralDashboard and ReferralDashboardView components for better user experience. * Standardized handling of referral amounts to ensure consistency across the application.
… API * Introduced new environment variables for minimum qualifying volume and reward amount in the referral system. * Updated types to include referral configuration parameters. * Refactored referral claim logic to utilize new configuration values for volume checks and reward distribution. * Enhanced referral data retrieval to reflect user-specific claim statuses and auto-claim functionality for eligible referrals.
* Updated .env.example to better document referral program variables. * Improved KYC status verification in the referral claim API with enhanced error handling for better user feedback. * Refactored referral configuration values to ensure proper parsing and validation in the application.
- Renamed `getAvatarImage` to `getAvatarImageFromAddress` for clarity in its purpose. - Updated all references to the renamed function across components, ensuring consistent usage. - Added comments to improve code documentation and understanding of avatar image retrieval logic.
edfe6a1 to
866c809
Compare
…heck Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (2)
.env.example (1)
169-170:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winMove inline comments off referral env assignments.
Trailing inline comments can be parsed as part of the value by some dotenv tooling; keep these values plain and put comments on separate lines.
Proposed fix
-NEXT_PUBLIC_REFERRAL_MIN_QUALIFYING_VOLUME_USD=100 # in USDC -NEXT_PUBLIC_REFERRAL_REWARD_AMOUNT_USD=1 # in USDC +# Referral minimum qualifying volume (in USDC) +NEXT_PUBLIC_REFERRAL_MIN_QUALIFYING_VOLUME_USD=100 +# Referral reward amount (in USDC) +NEXT_PUBLIC_REFERRAL_REWARD_AMOUNT_USD=1🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.env.example around lines 169 - 170, Remove the trailing inline comments from the two env assignments so the dotenv values are plain; instead place explanatory comments on their own lines above (or below) the entries for NEXT_PUBLIC_REFERRAL_MIN_QUALIFYING_VOLUME_USD and NEXT_PUBLIC_REFERRAL_REWARD_AMOUNT_USD to avoid the comment being parsed as part of the value.app/components/ReferralModal.tsx (1)
101-103:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUpdate referral threshold copy to match the implemented program.
Line 102 still says “$100 transaction”, but this flow is defined as first $20 transaction in the PR objective/design.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/components/ReferralModal.tsx` around lines 101 - 103, The referral copy in the ReferralModal component still reads "$100 transaction" but the program is for the first $20 transaction; update the JSX paragraph that uses sponsorChain (the <p> containing "Enter your referral code below...") to replace "$100" with "$20" so the displayed text matches the implemented flow. Ensure only the numeric threshold is changed and keep sponsorChain interpolation intact.
🧹 Nitpick comments (1)
app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1)
27-27: ⚡ Quick winReplace
anytype with properReferralDatatype.The
referralDatastate usesany, reducing type safety. The API aggregator returnsApiResponse<ReferralData>, so you should type this state accordingly.♻️ Proposed fix
+import type { ReferralData } from "`@/app/types`"; + export const ReferralDashboardView = ({ isOpen, onClose, }: { isOpen: boolean; onClose: () => void; }) => { const { getAccessToken } = usePrivy(); - const [referralData, setReferralData] = useState<any | null>(null); + const [referralData, setReferralData] = useState<ReferralData | null>(null);And update the filtered referrals type:
- const filteredReferrals: any[] = (referralData?.referrals || []).filter( + const filteredReferrals = (referralData?.referrals || []).filter( (r: any) => r.status === activeTab );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/components/wallet-mobile-modal/ReferralDashboardView.tsx` at line 27, The state referralData is typed as any which weakens type safety; change its type to ReferralData | null (and/or ApiResponse<ReferralData> | null if storing the raw API response) and update setReferralData usage accordingly; also replace any usages/filters that declare filtered referrals with proper types (e.g., ReferralData[] or Array<ReferralData>) so functions referencing referralData, setReferralData, and the filtered referrals variable use the concrete ReferralData type throughout.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/components/MainPageContent.tsx`:
- Around line 111-117: The success toast is shown twice because
ReferralInputModal already displays a toast on submit; remove the duplicate
toast call passed via the onSubmitSuccess prop in MainPageContent by replacing
the inline toast.success callback with either a no-op or a different handler
that performs non-UI follow-up actions (e.g., state updates). Locate the
ReferralInputModal usage and delete the toast.success invocation from the
onSubmitSuccess prop, keeping only non-toast logic in that callback if needed so
ReferralModal.tsx remains the sole place showing the success notification.
- Around line 293-304: The referral modal is only triggered inside
handleNetworkSelected so users who never open the network modal miss the prompt;
add an independent eligibility check (based on authenticated and walletAddress)
that runs on mount and whenever authenticated or walletAddress changes to mirror
the same logic: compute referralStorageKey =
`hasSeenReferralModal-${walletAddress.toLowerCase()}`, read localStorage, and
call setShowReferralModal(true) when no flag exists; keep the existing
handleNetworkSelected but extract the shared eligibility logic into a reusable
helper (e.g., isEligibleForReferral or showReferralIfEligible) and invoke it
from both handleNetworkSelected and a useEffect that depends on [authenticated,
walletAddress].
In `@app/components/ReferralModal.tsx`:
- Line 136: In ReferralModal.tsx update the JSX text node "I don't have a
referral code" to avoid the unescaped apostrophe lint error; locate the string
in the ReferralModal component and replace it with a properly escaped version
(for example use {"I don't have a referral code"}, use the HTML entity ' or
a typographic apostrophe ’) so the JSX no longer triggers
react/no-unescaped-entities.
In `@app/components/wallet-mobile-modal/ReferralDashboardView.tsx`:
- Around line 219-220: The referral instruction text in the
ReferralDashboardView component is inconsistent: update the copy that currently
reads "$100 transaction" to "$20 transaction" so it matches the PR objectives
and other UI text; locate the string inside the ReferralDashboardView (the JSX
text block that starts "Earn when you refer your friends...") and change the
amount from 100 to 20.
---
Duplicate comments:
In @.env.example:
- Around line 169-170: Remove the trailing inline comments from the two env
assignments so the dotenv values are plain; instead place explanatory comments
on their own lines above (or below) the entries for
NEXT_PUBLIC_REFERRAL_MIN_QUALIFYING_VOLUME_USD and
NEXT_PUBLIC_REFERRAL_REWARD_AMOUNT_USD to avoid the comment being parsed as part
of the value.
In `@app/components/ReferralModal.tsx`:
- Around line 101-103: The referral copy in the ReferralModal component still
reads "$100 transaction" but the program is for the first $20 transaction;
update the JSX paragraph that uses sponsorChain (the <p> containing "Enter your
referral code below...") to replace "$100" with "$20" so the displayed text
matches the implemented flow. Ensure only the numeric threshold is changed and
keep sponsorChain interpolation intact.
---
Nitpick comments:
In `@app/components/wallet-mobile-modal/ReferralDashboardView.tsx`:
- Line 27: The state referralData is typed as any which weakens type safety;
change its type to ReferralData | null (and/or ApiResponse<ReferralData> | null
if storing the raw API response) and update setReferralData usage accordingly;
also replace any usages/filters that declare filtered referrals with proper
types (e.g., ReferralData[] or Array<ReferralData>) so functions referencing
referralData, setReferralData, and the filtered referrals variable use the
concrete ReferralData type throughout.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 0680e9d5-9142-4601-9f19-9d737fcf3fa3
⛔ Files ignored due to path filters (11)
public/images/avatar/Avatar.pngis excluded by!**/*.pngpublic/images/avatar/Avatar1.pngis excluded by!**/*.pngpublic/images/avatar/Avatar2.pngis excluded by!**/*.pngpublic/images/avatar/Avatar3.pngis excluded by!**/*.pngpublic/images/avatar/Avatar4.pngis excluded by!**/*.pngpublic/images/avatar/Avatar5.pngis excluded by!**/*.pngpublic/images/avatar/Avatar6.pngis excluded by!**/*.pngpublic/images/avatar/Avatar7.pngis excluded by!**/*.pngpublic/images/referral-cta-dollar.pngis excluded by!**/*.pngpublic/images/referral-cta.pngis excluded by!**/*.pngpublic/images/referral-graphic.pngis excluded by!**/*.png
📒 Files selected for processing (24)
.env.exampleapp/api/aggregator.tsapp/api/referral/claim/route.tsapp/api/referral/referral-data/route.tsapp/api/referral/submit/route.tsapp/components/FundWalletForm.tsxapp/components/MainPageContent.tsxapp/components/MobileDropdown.tsxapp/components/NetworkSelectionModal.tsxapp/components/ReferralCTA.tsxapp/components/ReferralDashboard.tsxapp/components/ReferralDashboardSkeleton.tsxapp/components/ReferralDashboardViewSkeleton.tsxapp/components/ReferralModal.tsxapp/components/WalletDetails.tsxapp/components/index.tsapp/components/wallet-mobile-modal/ReferralDashboardView.tsxapp/components/wallet-mobile-modal/WalletView.tsxapp/components/wallet-mobile-modal/index.tsapp/lib/config.tsapp/lib/privy.tsapp/types.tsapp/utils.tsmiddleware.ts
✅ Files skipped from review due to trivial changes (2)
- app/components/wallet-mobile-modal/index.ts
- app/components/ReferralDashboardSkeleton.tsx
🚧 Files skipped from review as they are similar to previous changes (16)
- app/lib/config.ts
- middleware.ts
- app/lib/privy.ts
- app/components/ReferralDashboardViewSkeleton.tsx
- app/api/aggregator.ts
- app/components/ReferralCTA.tsx
- app/components/FundWalletForm.tsx
- app/types.ts
- app/components/MobileDropdown.tsx
- app/components/index.ts
- app/components/NetworkSelectionModal.tsx
- app/api/referral/submit/route.ts
- app/components/ReferralDashboard.tsx
- app/api/referral/referral-data/route.ts
- app/api/referral/claim/route.ts
- app/utils.ts
Remove duplicate submit toast, show referral prompt on login without requiring network modal, fix unescaped apostrophe, and standardize qualifying volume copy and env default to $20. Co-authored-by: Cursor <cursoragent@cursor.com>
Remove race-prone preflight read, return 409 on referred_wallet_address unique violations, and add a case-insensitive unique index migration. Co-authored-by: Cursor <cursoragent@cursor.com>
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
Existing User Referral & Tracking Flow
Technical Integrations
References
Design Reference: Figma - Noblocks Web App
closes #279
Testing
Checklist
mainBy submitting a PR, I agree to Noblocks's Contributor Code of Conduct and Contribution Guide.
Summary by CodeRabbit
New Features
Chores