-
Notifications
You must be signed in to change notification settings - Fork 46
feat(referrals): implement referral system with code gen, tracking, a… #291
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughAdds 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
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
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
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this 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_ADDRESSvariable (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:
- Moving it to an appropriate section (e.g., under "External Services" or a new "Wallet & Transactions" section)
- Adding inline documentation explaining its role in the referral reward system
- Clarifying whether it should be
NEXT_PUBLIC_(exposed to client) or a server-only variableapp/components/ReferralModal.tsx (1)
31-64: Simplify handleSubmit error handling to a single clear path
handleSubmitcurrently 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 andsubmitReferralCode, 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/unitsThe
ReferralDatainterface 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 forreferrals[].status.- Add a brief comment about units (e.g. “USDC in whole units, not micro‑units”) for
total_earned,total_pending, andamountto avoid ambiguity with the micro‑denominated crediting path.app/components/ReferralDashboard.tsx (1)
184-201: Strengthen referral item typing and avatar color fallbackTwo small robustness improvements:
filteredReferralsandreferralDataare typed asany. UsingReferralDataand 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);
getAvatarColorassumes an0x‑prefixed address and will produceNaNindices 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 safetyVerification confirms the following points need addressing:
The
amountvalidation (line 45) only checkstypeof amount !== "number". Add explicit validation thatamountis 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_DECIMALSis defined at line 109 but never used. Line 116 passesamountdirectly toBigNumber.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
ethersand 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 whenonNetworkSelectedshould fire; currently it runs on every close
handleCloseis used for all close paths (backdrop click, “Close” button, and the success callback fromhandleNetworkSwitch), but it always invokesonNetworkSelectedif 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
hasSelectedNetworkflag and only callingonNetworkSelectedwhen that’s true, or- Calling
onNetworkSelecteddirectly fromhandleNetworkSelect’s success path and keepinghandleCloseas a generic close handler, or- Renaming the prop to something like
onNetworkModalClosedif 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 implementationThis file still contains a full copy of the previous
MainPageContentandPageLayoutimplementation 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 guardThe 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 returndataasnullwhen something odd happens even ifreferralsErroris falsy, which would makereferrals.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
referralsas you already do.app/api/referral/claim/route.ts (1)
45-64: Supabase.single()error handling – consider.maybeSingle()or handling multi‑row cases explicitlyRight now:
- You call
.single()and treaterror.code !== "PGRST116"as fatal, then treat lack ofreferralas 404.- This implicitly handles the “no rows” case, but if multiple
pendingreferrals exist for the samereferred_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‑nullerroras 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
⛔ Files ignored due to path filters (3)
public/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 (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.tsxapp/components/ReferralModal.tsxapp/components/MainPageContent.tsxapp/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 exportRe-exports for
ReferralInputModalandReferralCTAfollow 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 patternImporting
ReferralDatahere 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 harmlessThe 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 verifiedVerification 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 expectationsThe new
ReferralCTA→isReferralOpen→ReferralDashboardflow is clean:
- Clicking the CTA closes the wallet sidebar and opens the referral sidebar in one place (
onViewReferrals).ReferralDashboardis controlled by a singleisReferralOpenflag with a simpleonClosehandler.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 withNetworkSelectionModalThe new wiring for the referral input flow looks good overall:
MainPageContentownsshowReferralModalplushandleNetworkSelectedandhandleReferralModalClose, and persists a per-wallethasSeenReferralModal-*flag inlocalStorage.PageLayoutreceivesshowReferralModal,onReferralModalClose, andonNetworkSelected, and renders:
NetworkSelectionModalwithonNetworkSelected, andReferralInputModaldriven solely byshowReferralModal.handleReferralModalCloseproperly hides the modal and sets thehasSeenReferralModal-*flag so the user isn’t prompted again.Given the current
NetworkSelectionModalimplementation,onNetworkSelectedis 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 inNetworkSelectionModalas 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 consistentUsing
x-wallet-addressfrom headers, lowercasing it, and short‑circuiting with a 401 plustrackApiErrormatches the pattern inreferral/submitand keeps auth surface small. ThetrackApiRequestcall immediately after is also well placed for successful requests.
104-132: Double‑check “first qualifying transaction” semantics and any required stablecoin filtersThe transaction check currently:
- Selects the earliest completed transaction for the wallet (
order("created_at", { ascending: true }).limit(1)).- Uses
amount_usd ?? amount_receivedand 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 safepending → processingstatus transitionThe update with both
.eq("id", referral.id)and.eq("status", "pending")plus thelength === 0guard 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.
| /** | ||
| * 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 || ""; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Tighten typing and error handling for referral helpers
The overall flow looks good, but there are a few improvements worth making:
submitReferralCode/getReferralDatacurrently returnany. It would be safer to type the responses, e.g. something like:Promise<{ success: true; data: ReferralData }>(forgetReferralData)- or a shared
{ success: boolean; data?: …; error?: string }shape, to catch backend/shape changes at compile time.
generateReferralCodereturns""if it can’t find a code in the payload even whensuccessis 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
accessTokenas optional, but the middleware requires a JWT for/api/referral*. Unless you have known internal call sites that rely on this being optional, makingaccessTokenrequired 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 -SLength 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 -20Length 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.tsLength 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 likePromise<{ 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
accessTokenparameter: Valid concern forsubmitReferralCode(the only actively used function). InReferralModal.tsx:49, the token fromgetAccessToken()is passed with?? undefined, which allows unauthenticated calls to fail at runtime rather than catch-time. MakingaccessTokenrequired would force callers to handle the undefined case explicitly before calling the function. Note:getReferralDataproduction code is commented out inReferralDashboard.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.
app/api/referral/claim/route.ts
Outdated
| 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 }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Missing INTERNAL_API_KEY is treated as success, which can silently drop rewards in production
At the moment:
creditWalletreturns{ ok: false, skipped: true }whenINTERNAL_API_KEYis 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
earnedand 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
creditWalletreturn 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 -5Length 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 catLength 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 3Length 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 2Length 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 3Length of output: 4317
🏁 Script executed:
#!/bin/bash
# Look for environment configuration patterns
rg -n "isProd|isProduction|NODE_ENV.*production" --type ts --type js | head -20Length 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 2Length 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.tsLength 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 2Length 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.tsLength 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 3Length 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 -10Length 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.tsLength 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.tsLength 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.tsLength 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.
app/components/ReferralDashboard.tsx
Outdated
| // 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: [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this 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
fetchDataalways setsreferralData(MOCK_DATA)and the production path withgetAccessToken+getReferralDataremains commented, so the dashboard will never show a user’s real referral code or earnings in production.isLoadingand the loading UI are effectively unused as well.Before shipping this feature, wire
fetchDatato call the live API by default (usinggetAccessToken+getReferralData), and gate or remove MOCK_DATA so it’s only used under an explicit dev flag. While you’re in there, typingreferralDataasReferralData | nullwould 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 solidThe generator +
while (!code && attempts < maxAttempts)loop correctly enforces a bounded uniqueness check and handles the SupabasePGRST116(“no row”) case explicitly. The upsert withonConflict: "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_codeat 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 simplifiedThe required
onViewReferralsprop fixes the earlier no-op issue and the USDC wording now matches the backend reward currency. Given the prop is non-optional, theif (onViewReferrals)guard inhandleViewReferralsis redundant—you could passonViewReferralsdirectly to the button’sonClick(or drop the wrapper) to trim a tiny bit of indirection.app/components/ReferralModal.tsx (1)
31-71: Improve error messaging and auth handling inhandleSubmitThe control flow and loading state handling look solid, but the outer
catchcurrently surfaces any failure fromgetAccessToken()(or other unexpected errors) as “Invalid referral code…”, which is misleading for auth/network issues. It also silently proceeds ifgetAccessToken()returnsnull, relying on the backend to fail with 401.Consider:
- Handling a missing/failed access token explicitly before calling
submitReferralCodeand surfacing a more accurate message (e.g. “Session expired, please reconnect your wallet”), and/or- Using a generic fallback message in the outer
catchthat doesn’t imply the code itself is invalid.This keeps user feedback accurate and makes debugging auth issues easier while preserving your inner
try/catchfor well‑formed API responses.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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.tsxapp/components/ReferralDashboard.tsxapp/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 correctThe POST handler’s sequence—pending referral lookup, dual KYC verification, first-completed-transaction check with a 20 USD minimum, guarded
pending → processingstatus update, and rollback on wallet credit failure—is sound.The updated
creditWallethelper that throws in production whenINTERNAL_API_KEYis 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
isReferralOpenstate, theonViewReferralscallback intoMobileDropdown, and the colocated<ReferralDashboard>render form a clean flow for opening/closing the referral drawer. Rendering it only when(ready && authenticated) || isInjectedWalletensures 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 solidThe new
ReferralCTAandReferralDashboardintegration 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 simpleisOpen/onCloseAPI. 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-friendlyThe 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 viatrackApiError.- 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 cleanThe new optional
onViewReferralsprop is threaded intoWalletViewwithout impacting existing flows, and the logout tracking helper remains non-blocking with the AbortError guard. TheCopyAddressWarningModalplacement still aligns with howisWarningModalOpenis 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 changesThe verification found no evidence supporting the proposed changes:
- submitReferralCode: The sole caller (ReferralModal.tsx:49) accesses only
res.data?.message, neverreferral_id. The current typeSubmitReferralResult = { 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.
There was a problem hiding this 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
isReferralOpenand<ReferralDashboard>are only controlled viaonViewReferrals={() => setIsReferralOpen(true)}passed intoMobileDropdown, butMobileDropdownnever invokes this prop. As written, this sidebar dashboard will never open.Either:
- Wire
MobileDropdownto call itsonViewReferralsprop when the user taps “View referrals” (and potentially drop the internalreferralsview there), or- If the mobile flow is meant to use
ReferralDashboardViewinstead, remove theonViewReferralsprop andisReferralOpen/ReferralDashboardfromNavbarto 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 fallbackThe new
onViewReferralsprop 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
onViewReferralsand the CTA silently becomes a no‑op, consider either:
- Making
onViewReferralsrequired onWalletViewProps, or- Rendering
<ReferralCTA>only whenonViewReferralsis 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 propInside
MobileDropdown:
- The
onViewReferralsprop is defined on the component but never used.- Instead,
WalletViewgetsonViewReferrals={() => setCurrentView("referrals")}, and the"referrals"branch rendersReferralDashboardView.At the same time,
NavbarpassesonViewReferrals={() => setIsReferralOpen(true)}intoMobileDropdown, expecting it to be called—this never happens, so the nav‑levelReferralDashboardis dead.Recommend one of:
- Drop the
onViewReferralsprop fromMobileDropdownand stop passing it fromNavbar, 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
ReferralDashboardViewitself renders a full‑screenDialogwith its own overlay and focus trap, but here it is mounted inside theDialogPanelofMobileDropdown, creating two overlapping HeadlessUIDialogs.While it works visually, this can complicate focus management and accessibility. Consider instead:
- Rendering
ReferralDashboardViewas a sibling toMobileDropdown(controlled by shared state), or- Extracting a content‑only
ReferralDashboardContentused here inside the existingDialogPanel, leavingReferralDashboardViewfor 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)
getAvatarColorassumesaddress.slice(2, 4)is valid hex; if an unexpected string slips through,parseIntcan returnNaN, 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
📒 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.tsxapp/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 goodPublicly exposing
ReferralDashboardViewvia this barrel file matches its usage inMobileDropdownand keeps the modal API consistent.app/components/Navbar.tsx (1)
187-189: Swap link className change is behavior‑neutralThe 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 behaviorThe updated
.filtercondition 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 safeAdding
.catchand.finallyaroundtrackLogoutWithFetchmakes 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.
There was a problem hiding this 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: Replaceanytype with properReferralDatatype.Using
anydefeats TypeScript's type safety and prevents catching potential bugs at compile time.Import the
ReferralDatatype 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.amountis always a number, but if the API returnsnullorundefined,.toFixed(1)will throw a runtime error.Apply safe chaining:
-{referral.amount.toFixed(1)} USDC +{referral.amount?.toFixed(1) ?? "0.0"} USDCOr add type guards in the filter/map logic to exclude referrals with missing amounts.
app/api/aggregator.ts (1)
517-606: Tighten auth: makeaccessTokenmandatory for referral helpers (or clearly justify optional use)Both
submitReferralCode(Lines 517–544) andgenerateReferralCode(Lines 604–626) treataccessTokenas optional, but the backing/api/referral/*routes rely on authenticated middleware to populatex-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
accessTokena required parameter forsubmitReferralCodeandgenerateReferralCode, matchinggetReferralDataand 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.errorshould 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-hideplugin 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” statusOn credit failure, the rollback to
pending(Line 128) and the finalearnedupdate (Line 133) both ignore potential errors from the DB client. If either update fails, you can end up with referrals stuck inprocessingor with status out of sync with actual payouts, and you won’t have logs/metrics for it.Consider capturing and handling the returned
errorfrom 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 endpointsThe
generateReferralCodehelper and uniqueness/upsert sequence here (Lines 11–19 and 77–121) are almost identical to the logic inapp/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 underapp/api/referral/) that encapsulates:
- Generating a candidate code
- Checking for collisions
- Upserting the user with the new code
- Returning the final
referral_codeBoth 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-datarouteIn 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, andtrackApiErrorto"/api/referral/referral-data"so your observability pipeline reflects the real endpoint name consistently (and matches client helpers likegetReferralData).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
📒 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 whenreferralDatais null.✅ Addressed in commit 0e3de13
Also applies to: 278-278
app/components/ReferralDashboardSkeleton.tsx (1)
1-63: Referral dashboard skeleton looks clean and appropriateThe 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-riskLayout mirrors the main dashboard skeleton with mobile-friendly scrolling; nothing here affects behavior beyond loading visuals.
| const token = await getAccessToken(); | ||
| if (!token) return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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); |
There was a problem hiding this comment.
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.
| <p className="text-sm font-medium text-text-secondary dark:text-white/60"> | ||
| {referral.amount.toFixed(1)} USDC | ||
| </p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| <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.
There was a problem hiding this 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‑opThe JSON/body check only verifies the presence of a
transactionIdstring and then discards it. If the route does not actually need a body, consider removing this block; if atransactionIdis required for auditing or idempotency, enforce and persist it instead.
69-81: Tighten environment/config checks for payout infrastructureYou already 503 when
CASHBACK_WALLET_PRIVATE_KEYis missing, and later validate its shape. However,RPC_URLandUSDC_CONTRACT_ADDRESSsilently fall back to defaults; if those are misconfigured (e.g., placeholder RPC), you’ll create a pending claim that is then markedfailedon 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 permanentfailedclaims.Also applies to: 245-251
243-275: Clarify/strengthen handling of partial transfer failures and on‑chain client usageThe two
writeContractcalls are executed sequentially inside a singletry, and any thrown error leads to afailedclaim 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
viemwallet client and RPC configuration, it’s worth double‑checking against the current viem docs that thecreateWalletClient+writeContractusage 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 onlyThis route fully trusts
x-user-idfor 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-idis 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, andtrackBusinessEventare imported but never used here, while/api/referral/submitis fully instrumented. For observability and funnel analysis, consider:
- Tracking the incoming request at the top of the handler.
- Emitting
trackApiResponseon successful claim.- Using
trackApiErrorin 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
📒 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 specThe code pays
$1.0to both referrer and referred user (two transfers, lines 263-275) and enforces a$100minimum 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.
| // 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(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 inreferral_claimsis actuallywallet_address, this insert will error at runtime. Please confirm the column name and align it with the DB schema. - On completion, only
referrerTxHashis stored intx_hash, while bothreferrerTxHashandreferredTxHashare 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";
fiLength 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 -50Length 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 -30Length 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 -40Length 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 sqlLength 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:
-
Field naming error (lines 218-228): The code inserts
walletAddress(camelCase), but every other table in the codebase useswallet_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 insupabase/migrations/, which means either the table schema is undefined or the schema definition is missing from version control. This will cause a runtime error. -
Incomplete tx hash storage (lines 276-283): Only
referrerTxHashis stored intx_hash(line 281), while bothreferrerTxHashandreferredTxHashare 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
There was a problem hiding this 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: Replaceanytype with properReferralDatatype.This is the same issue as in the desktop
ReferralDashboardcomponent. Usinganydefeats 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 ifamountis 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.writeTextcall 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: Replaceanytype with properReferralDatatype.Using
anydefeats TypeScript's type checking benefits. Import and use theReferralDatatype 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: Replaceany[]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
ReferralDatatyping on state, TypeScript will infer the correct type forfilteredReferralsautomatically.app/components/wallet-mobile-modal/ReferralDashboardView.tsx (1)
71-73: Replaceany[]with proper type annotation.Same as desktop component—with proper
ReferralDatatyping, 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
⛔ Files ignored due to path filters (8)
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!**/*.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.tsxapp/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-pressedattribute 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
getReferralDatainstead 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-labelfor 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.toFixedcrash on initial render.
…allet configuration and error handling
…im route, update wallet address field
There was a problem hiding this 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
walletAddressis 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 updatesTwo related points in the claim / transfer section:
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);}
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-
completedstatuses 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 debuggabilityThe 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
📒 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 totrackTransactionEventrather thantrackAuthEvent. 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-structuredUsing
x-user-id+ Privy for wallet resolution, guarding on a pending referral tied toreferred_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 viaPGRST116is also handled correctly.
…ous error tracking in POST route
…referral data retrieval with improved error handling
There was a problem hiding this 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 totrackFundingEventeven 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.
walletAddressis already lowercase fromgetSmartWalletAddressFromPrivyUserId, 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. MakingaccessTokenrequired 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/databut 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
📒 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
walletAddressfrom the request body as a fallback when thex-wallet-addressheader 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:
- The endpoint should require authentication (header-only approach).
- Unauthenticated tracking is acceptable for your analytics use case.
- 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
responseTimecalculation 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 ofany- 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:
- Failed claims should block retries (current behavior allows retries)
- 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.
| // Referral program configuration | ||
| const REWARD_AMOUNT_USD = 1; | ||
| const MIN_TX_VOLUME_USD = 100; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 -20Repository: 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 20Repository: 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.tsRepository: 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 -20Repository: 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.
… across components
There was a problem hiding this 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: UseReferralDatainstead ofanyfor stronger type‑safety
referralDataandfilteredReferralsare currently typed asany, which loses the structure guaranteed bygetReferralDataand weakens autocomplete and compile‑time checks. Reusing the sharedReferralDatatype here aligns the UI with the API contract and removes the need for explicitany.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: ReplaceanywithReferralDatafor referral state
referralDataandfilteredReferralsare still typed asany, which defeats the benefit of the typedgetReferralDataresponse 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: Guardreferral.amount.toFixed(1)to avoid runtime errorsHere
referral.amount.toFixed(1)will throw ifamountis evernull/undefinedin 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 validationOverall flow (local validation → token fetch →
submitReferralCodecall → 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
catchat 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 outertryand use a singlecatchwith 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
📒 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.tsxapp/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.tsxapp/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
handleSkipcorrectly clears the localreferralCodestate before delegating toonClose, and wiring it into the modal’sonCloseensures 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 flowThe 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.isSubmittinganddisabledflags 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‑chainchainwhen derivingsponsorChainIf
selectedNetworkexists butselectedNetwork.chainis everundefinedornull,selectedNetwork?.chain.namewill still attempt.nameaccess and can throw at runtime. Safer to optional‑chainchainas 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 solidThe 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 goodThe 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
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
✏️ Tip: You can customize this high-level summary in your review settings.