Skip to content

feat: Implement Earnings & Payout Tracking#96

Closed
Oluwatos94 wants to merge 1 commit intoboundlessfi:mainfrom
Oluwatos94:feature/Earnings-
Closed

feat: Implement Earnings & Payout Tracking#96
Oluwatos94 wants to merge 1 commit intoboundlessfi:mainfrom
Oluwatos94:feature/Earnings-

Conversation

@Oluwatos94
Copy link
Contributor

@Oluwatos94 Oluwatos94 commented Feb 22, 2026

feat: implement earnings & payout tracking in My Claims

Adds an EarningsSummary component to the My Claims tab on the profile page, showing total earned, pending amount (by currency), and a payout history list with processing/completed statuses. Earnings data is derived from the user's bounty claims, with a fallback to reputation stats when no claim data is available.

Screenshot (1588)

closes #84

Summary by CodeRabbit

  • New Features

    • Added earnings summary section displaying total earned, pending amounts, and payout history on user profiles.
  • Improvements

    • Enhanced profile page layout with organized tabs for history, analytics, and claims.
    • Improved error handling with clearer messages for missing profiles and load failures.
    • Added loading skeleton display during profile data retrieval.

@vercel
Copy link

vercel bot commented Feb 22, 2026

@Oluwatos94 is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

The pull request implements earnings and payout tracking by introducing a new EarningsSummary React component that displays total earnings, pending amounts, and transaction history. The profile page is updated to derive earnings data from bounty and reputation sources and render it alongside existing profile information.

Changes

Cohort / File(s) Summary
New Earnings Component
components/reputation/earnings-summary.tsx
Introduces a new React component that displays earnings summary with total earned and pending amounts formatted by currency, and a payout history list showing transaction details with status badges and formatted dates.
Profile Page Enhancement
app/profile/[userId]/page.tsx
Extends imports to include earnings and normalization utilities. Reorganizes data fetching with improved state handling, adds memoized earnings computation from bounty data, generates mock history from reputation stats, and enhances error/loading states with 404 vs. generic error distinction. Updates UI to render earnings summary in the claims tab alongside existing tabs for history and analytics.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • Benjtalkshow

Poem

🐰 Carrots counted, coins aligned,
Earnings tracked with care refined,
Pending payouts, history clear,
A rabbit's wealth throughout the year! 🥕💰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: Implement Earnings & Payout Tracking' accurately describes the main changes introducing earnings and payout tracking functionality in the My Claims section.
Linked Issues check ✅ Passed The PR implements all requirements from issue #84: EarningsSummary type with correct shape, earnings display in My Claims tab, and payout history rendering with proper status handling.
Out of Scope Changes check ✅ Passed All changes are directly related to earnings and payout tracking features. Profile page refactoring supports the new EarningsSummary component integration without introducing unrelated modifications.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


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

❤️ Share

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

@Oluwatos94 Oluwatos94 closed this Feb 22, 2026
@Oluwatos94 Oluwatos94 deleted the feature/Earnings- branch February 22, 2026 06:16
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
components/reputation/earnings-summary.tsx (2)

6-15: Rename the EarningsSummary type to avoid a name collision with the component.

Both the exported type (Line 6) and the exported function (Line 21) are named EarningsSummary. While TypeScript allows a value and a type to share the same name, it forces every consumer to alias one of them — as seen in page.tsx with type EarningsSummary as EarningsSummaryType. Rename the type to something like EarningsSummaryData to make the public API unambiguous.

♻️ Proposed rename
-export type EarningsSummary = {
+export type EarningsSummaryData = {
   totalEarned: number;
   pendingAmount: number;
   currency: string;
   payoutHistory: Array<{
     amount: number;
     date: string;
     status: "processing" | "completed";
   }>;
 };

 interface EarningsSummaryProps {
-  earnings: EarningsSummary;
+  earnings: EarningsSummaryData;
 }

And update the import in app/profile/[userId]/page.tsx:

 import {
   EarningsSummary,
-  type EarningsSummary as EarningsSummaryType,
+  type EarningsSummaryData,
 } from "@/components/reputation/earnings-summary";

Also applies to: 21-21

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

In `@components/reputation/earnings-summary.tsx` around lines 6 - 15, The exported
type EarningsSummary conflicts with the exported component function of the same
name; rename the type to EarningsSummaryData (update the export type declaration
currently named EarningsSummary) and update all consumers/imports (e.g., the
import in page.tsx that currently aliases the type) to use EarningsSummaryData
so the component function can remain EarningsSummary without forcing callers to
alias the type.

68-90: Use a stable key for payout history rows instead of array index.

key={index} causes React to mis-identify rows if the list ever reorders or items are prepended. Prefer a composite of stable fields such as date + amount.

♻️ Proposed fix
-                <div
-                  key={index}
+                <div
+                  key={`${payout.date}-${payout.amount}-${index}`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/reputation/earnings-summary.tsx` around lines 68 - 90, The list
rendering of earnings.payoutHistory uses key={index}, which is unstable; update
the key on the mapped element (the div inside earnings.payoutHistory.map) to use
a stable unique identifier such as payout.id if present, otherwise a composite
stable string like `${payout.date}-${payout.amount}`; keep the rest of the
render (Badge, formatDate, formatCurrency) unchanged and ensure the key
expression references the payout object's stable fields rather than the array
index.
app/profile/[userId]/page.tsx (1)

86-96: Extract the duplicated status-derivation logic into a shared helper.

The logic that maps a bounty's raw status + claimExpiresAt into "completed" | "in-review" | "active" is copy-pasted verbatim between the earningsSummary memo (Lines 86–96) and the myClaims memo (Lines 133–146). Any future change (e.g., adding a new status) must be applied in both places.

♻️ Proposed extraction
+function deriveBountyStatus(
+  status: string | undefined,
+  claimExpiresAt: string | undefined | null,
+): string {
+  if (status === "closed") return "completed";
+  if (status === "claimed" && claimExpiresAt) {
+    const expiry = new Date(claimExpiresAt);
+    if (!Number.isNaN(expiry.getTime()) && expiry < new Date())
+      return "in-review";
+  }
+  return "active";
+}

Then replace both duplication sites:

-      let status = "active";
-      if (bounty.status === "closed") {
-        status = "completed";
-      } else if (bounty.status === "claimed" && bounty.claimExpiresAt) {
-        const expiry = new Date(bounty.claimExpiresAt);
-        if (!Number.isNaN(expiry.getTime()) && expiry < new Date()) {
-          status = "in-review";
-        }
-      }
+      const status = deriveBountyStatus(bounty.status, bounty.claimExpiresAt);

Also applies to: 133-146

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

In `@app/profile/`[userId]/page.tsx around lines 86 - 96, The status-derivation
logic duplicated in the earningsSummary memo (lines around the for-loop over
userBounties) and the myClaims memo should be extracted into a single helper
function (e.g., deriveBountyStatus or getBountyDisplayStatus) that accepts a
bounty object (or its status and claimExpiresAt) and returns the union
"completed" | "in-review" | "active"; implement the same parsing/Number.isNaN
expiry check and comparison to new Date() inside that helper, then replace both
inlined blocks by calling this helper from earningsSummary and myClaims so
future changes only update one function.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/profile/`[userId]/page.tsx:
- Around line 121-123: When userBounties is empty the code currently only sets
totalEarned from reputation.stats.totalEarnings but leaves pendingAmount and
payoutHistory at their defaults, causing inconsistent fallback behavior; update
the fallback in the block that checks userBounties and reputation to populate
pendingAmount and payoutHistory from the corresponding reputation.stats fields
(e.g., reputation.stats.pendingAmount and reputation.stats.payoutHistory) if
present, or explicitly set them to consistent safe defaults, and if the omission
was intentional add a clarifying comment near the userBounties/reputation logic
explaining why only totalEarned is used.
- Around line 101-115: The payoutHistory entries incorrectly use
bounty.claimExpiresAt as the payout date; update the two places where
payoutHistory.push is called (the "completed" branch and the "processing" branch
inside app/profile/[userId]/page.tsx) to use a proper timestamp field such as
bounty.completedAt or fallback to bounty.updatedAt, e.g. use bounty.completedAt
?? bounty.updatedAt ?? null, and if neither field exists leave a clear TODO
comment or null so we don't silently show claimExpiresAt as the transaction
date; ensure all references to bounty.claimExpiresAt in these payoutHistory
pushes are replaced accordingly.
- Around line 34-63: mockHistory (built with MAX_MOCK_HISTORY inside the useMemo
that depends on reputation) contains hardcoded/fabricated bounty records and
must not be shipped to production; replace it by fetching real bounty history
from the backend or gate the mock generation behind an explicit development-only
flag (e.g., only create mockHistory when process.env.NODE_ENV === "development"
or a dedicated feature flag), and ensure any UI rendering uses the real API
response (or an empty array) when the flag is off so fabricated projectName,
dates, rewardAmount, feedback, etc. never appear to real users.
- Around line 162-163: The Skeleton components are using an invalid Tailwind
class "h-100" which is ignored and collapses the elements; update the two
Skeleton usages (the JSX lines rendering Skeleton with className="h-100
md:col-span-1" and className="h-100 md:col-span-2") to use a valid height
utility (for example h-24, h-40, or h-96) that matches the desired visual
height, or if you truly need h-100, add that value to your Tailwind config's
theme.extend.height and rebuild so the class exists; change the className
strings accordingly to either a valid built-in h-* or the new configured key.
- Around line 25-26: The code unsafely asserts params.userId as a string; update
the use of useParams() in page.tsx to validate the param type before using it
(e.g., check params.userId !== undefined and typeof params.userId === "string"
or if it's an array take the first element), assign a properly-typed userId
string variable only after that check, and handle the absent/array case by
returning a safe fallback (render an error, call notFound(), or redirect) so
downstream calls that use userId (fetches/filters) never receive undefined or
string[].

In `@components/reputation/earnings-summary.tsx`:
- Around line 22-23: The ternary that defines currencySymbol is a no-op
(currencySymbol and earnings.currency are identical); update the code in the
earnings-summary component by either removing currencySymbol and using
earnings.currency directly, or implement a real mapping so currencySymbol
reflects a symbol (e.g., map "USD" -> "$", "USDC" -> "USDC", "EUR" -> "€")
before rendering; locate the current const currencySymbol and replace it with
the chosen approach, updating any uses of currencySymbol in the component
(render/props) to match.

---

Nitpick comments:
In `@app/profile/`[userId]/page.tsx:
- Around line 86-96: The status-derivation logic duplicated in the
earningsSummary memo (lines around the for-loop over userBounties) and the
myClaims memo should be extracted into a single helper function (e.g.,
deriveBountyStatus or getBountyDisplayStatus) that accepts a bounty object (or
its status and claimExpiresAt) and returns the union "completed" | "in-review" |
"active"; implement the same parsing/Number.isNaN expiry check and comparison to
new Date() inside that helper, then replace both inlined blocks by calling this
helper from earningsSummary and myClaims so future changes only update one
function.

In `@components/reputation/earnings-summary.tsx`:
- Around line 6-15: The exported type EarningsSummary conflicts with the
exported component function of the same name; rename the type to
EarningsSummaryData (update the export type declaration currently named
EarningsSummary) and update all consumers/imports (e.g., the import in page.tsx
that currently aliases the type) to use EarningsSummaryData so the component
function can remain EarningsSummary without forcing callers to alias the type.
- Around line 68-90: The list rendering of earnings.payoutHistory uses
key={index}, which is unstable; update the key on the mapped element (the div
inside earnings.payoutHistory.map) to use a stable unique identifier such as
payout.id if present, otherwise a composite stable string like
`${payout.date}-${payout.amount}`; keep the rest of the render (Badge,
formatDate, formatCurrency) unchanged and ensure the key expression references
the payout object's stable fields rather than the array index.

Comment on lines +25 to +26
const params = useParams();
const userId = params.userId as string;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

params.userId as string is unsafe — Next.js dynamic params can be string | string[].

useParams() returns string | string[] per segment; asserting as string without a check will pass undefined or an array to downstream calls (API fetch, filter) when the segment is absent or caught by a catch-all route.

🛠️ Proposed fix
-  const userId = params.userId as string;
+  const rawUserId = params.userId;
+  const userId = Array.isArray(rawUserId) ? rawUserId[0] : rawUserId ?? "";
📝 Committable suggestion

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

Suggested change
const params = useParams();
const userId = params.userId as string;
const params = useParams();
const rawUserId = params.userId;
const userId = Array.isArray(rawUserId) ? rawUserId[0] : rawUserId ?? "";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx around lines 25 - 26, The code unsafely
asserts params.userId as a string; update the use of useParams() in page.tsx to
validate the param type before using it (e.g., check params.userId !== undefined
and typeof params.userId === "string" or if it's an array take the first
element), assign a properly-typed userId string variable only after that check,
and handle the absent/array case by returning a safe fallback (render an error,
call notFound(), or redirect) so downstream calls that use userId
(fetches/filters) never receive undefined or string[].

Comment on lines +34 to +63
const MAX_MOCK_HISTORY = 50;

const mockHistory = useMemo(() => {
if (!reputation) return [];
const count = Math.min(
reputation.stats.totalCompleted ?? 0,
MAX_MOCK_HISTORY,
);
return Array(count)
.fill(null)
.map((_, i) => ({
id: `bounty-${i}`,
bountyId: `b-${i}`,
bountyTitle: `Implemented feature #${100 + i}`,
projectName: "Drips Protocol",
projectLogoUrl: null,
difficulty: ["BEGINNER", "INTERMEDIATE", "ADVANCED"][i % 3] as
| "BEGINNER"
| "INTERMEDIATE"
| "ADVANCED",
rewardAmount: 500,
rewardCurrency: "USDC",
claimedAt: "2023-01-01T00:00:00Z",
completedAt: "2024-01-15T12:00:00Z",
completionTimeHours: 48,
maintainerRating: 5,
maintainerFeedback: "Great work!",
pointsEarned: 150,
}));
}, [reputation]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove (or clearly gate) the hardcoded mock history data before production.

mockHistory is built entirely from fabricated values: a fixed project name ("Drips Protocol"), static dates ("2023-01-01" / "2024-01-15"), a hardcoded reward (500 USDC), and canned feedback. This data is rendered live on the Bounty History tab and will be shown to real users as if it were their actual claim records.

Either drive this from real API data or gate it behind an explicit flag (e.g. process.env.NODE_ENV === "development") so it can never reach production.

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

In `@app/profile/`[userId]/page.tsx around lines 34 - 63, mockHistory (built with
MAX_MOCK_HISTORY inside the useMemo that depends on reputation) contains
hardcoded/fabricated bounty records and must not be shipped to production;
replace it by fetching real bounty history from the backend or gate the mock
generation behind an explicit development-only flag (e.g., only create
mockHistory when process.env.NODE_ENV === "development" or a dedicated feature
flag), and ensure any UI rendering uses the real API response (or an empty
array) when the flag is off so fabricated projectName, dates, rewardAmount,
feedback, etc. never appear to real users.

Comment on lines +101 to +115
payoutHistory.push({
amount,
date: bounty.claimExpiresAt ?? new Date().toISOString(),
status: "completed",
});
} else if (
pendingStatuses.some((s) => normalizeStatus(s) === normalized)
) {
pendingAmount += amount;
if (normalized === "in-review") {
payoutHistory.push({
amount,
date: bounty.claimExpiresAt ?? new Date().toISOString(),
status: "processing",
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

claimExpiresAt is used as the payout date, which is semantically incorrect.

claimExpiresAt is the deadline by which a claimer must deliver — not the date a payout occurred or was initiated. Displaying it as the transaction date in Payout History will confuse users (e.g., the date shown precedes the bounty being marked complete). A completedAt, updatedAt, or similar field should be used instead. If such a field doesn't yet exist on the bounty shape, this should be a placeholder/TODO rather than silently showing the wrong date.

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

In `@app/profile/`[userId]/page.tsx around lines 101 - 115, The payoutHistory
entries incorrectly use bounty.claimExpiresAt as the payout date; update the two
places where payoutHistory.push is called (the "completed" branch and the
"processing" branch inside app/profile/[userId]/page.tsx) to use a proper
timestamp field such as bounty.completedAt or fallback to bounty.updatedAt, e.g.
use bounty.completedAt ?? bounty.updatedAt ?? null, and if neither field exists
leave a clear TODO comment or null so we don't silently show claimExpiresAt as
the transaction date; ensure all references to bounty.claimExpiresAt in these
payoutHistory pushes are replaced accordingly.

Comment on lines +121 to 123
if (userBounties.length === 0 && reputation) {
totalEarned = reputation.stats.totalEarnings;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reputation-stats fallback is inconsistent — only totalEarned is populated.

When userBounties is empty and reputation data is present, the fallback sets totalEarned from reputation.stats.totalEarnings but leaves pendingAmount at 0 and payoutHistory as []. If the reputation object also carries a pending amount (or if the intent is purely a display fallback), the three fields should either all be sourced from reputation stats or the comment should make the intentional incompleteness explicit.

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

In `@app/profile/`[userId]/page.tsx around lines 121 - 123, When userBounties is
empty the code currently only sets totalEarned from
reputation.stats.totalEarnings but leaves pendingAmount and payoutHistory at
their defaults, causing inconsistent fallback behavior; update the fallback in
the block that checks userBounties and reputation to populate pendingAmount and
payoutHistory from the corresponding reputation.stats fields (e.g.,
reputation.stats.pendingAmount and reputation.stats.payoutHistory) if present,
or explicitly set them to consistent safe defaults, and if the omission was
intentional add a clarifying comment near the userBounties/reputation logic
explaining why only totalEarned is used.

Comment on lines +162 to +163
<Skeleton className="h-100 md:col-span-1" />
<Skeleton className="h-100 md:col-span-2" />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

h-100 is not a default Tailwind spacing utility — skeletons will render with no height.

Tailwind's height scale is a combination of the default spacing scale as well as some additional values specific to heights, and the default numeric scale tops out at h-96 (24 rem). h-100 will be silently ignored unless it is explicitly extended in tailwind.config, leaving the skeleton <div>s collapsed and invisible during the loading state.

🛠️ Proposed fix
-          <Skeleton className="h-100 md:col-span-1" />
-          <Skeleton className="h-100 md:col-span-2" />
+          <Skeleton className="h-[400px] md:col-span-1" />
+          <Skeleton className="h-[400px] md:col-span-2" />
📝 Committable suggestion

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

Suggested change
<Skeleton className="h-100 md:col-span-1" />
<Skeleton className="h-100 md:col-span-2" />
<Skeleton className="h-[400px] md:col-span-1" />
<Skeleton className="h-[400px] md:col-span-2" />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx around lines 162 - 163, The Skeleton
components are using an invalid Tailwind class "h-100" which is ignored and
collapses the elements; update the two Skeleton usages (the JSX lines rendering
Skeleton with className="h-100 md:col-span-1" and className="h-100
md:col-span-2") to use a valid height utility (for example h-24, h-40, or h-96)
that matches the desired visual height, or if you truly need h-100, add that
value to your Tailwind config's theme.extend.height and rebuild so the class
exists; change the className strings accordingly to either a valid built-in h-*
or the new configured key.

Comment on lines +22 to +23
const currencySymbol =
earnings.currency === "USDC" ? "USDC" : earnings.currency;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

currencySymbol ternary is a no-op — both branches return earnings.currency.

const currencySymbol = earnings.currency === "USDC" ? "USDC" : earnings.currency;

Whether the condition is true or false, currencySymbol === earnings.currency. The variable is indistinguishable from earnings.currency directly. If the intent was to map other currency codes to symbols (e.g., "USD""$"), that logic is missing. Remove the dead ternary or implement the actual mapping.

🛠️ Proposed fix (if no symbol mapping is needed)
-  const currencySymbol =
-    earnings.currency === "USDC" ? "USDC" : earnings.currency;
+  const currencySymbol = earnings.currency;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/reputation/earnings-summary.tsx` around lines 22 - 23, The ternary
that defines currencySymbol is a no-op (currencySymbol and earnings.currency are
identical); update the code in the earnings-summary component by either removing
currencySymbol and using earnings.currency directly, or implement a real mapping
so currencySymbol reflects a symbol (e.g., map "USD" -> "$", "USDC" -> "USDC",
"EUR" -> "€") before rendering; locate the current const currencySymbol and
replace it with the chosen approach, updating any uses of currencySymbol in the
component (render/props) to match.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Earnings & Payout Tracking

1 participant