Skip to content

Feat/perk rework#1353

Merged
Hugo0 merged 4 commits intopeanut-wallet-devfrom
feat/perk-rework
Oct 23, 2025
Merged

Feat/perk rework#1353
Hugo0 merged 4 commits intopeanut-wallet-devfrom
feat/perk-rework

Conversation

@Hugo0
Copy link
Contributor

@Hugo0 Hugo0 commented Oct 23, 2025

No description provided.

@vercel
Copy link

vercel bot commented Oct 23, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
peanut-wallet Ready Ready Preview Comment Oct 23, 2025 0:37am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 23, 2025

Walkthrough

This PR consolidates modal management logic by removing duplicate balance warning effects and introducing a centralized modal coordination system. It adds a new leaderboard feature with time filtering, refactors perk reward handling across transaction detail components with new types and utilities, and enhances QR pay interactions.

Changes

Cohort / File(s) Summary
Modal Management & Coordination
src/app/(mobile-ui)/home/page.tsx, src/hooks/useModalManager.ts
Removes duplicate useEffect for balance warning modal; consolidates into single effect with isPostSignupActionModalVisible guard. Introduces new centralized modal registry system with useModalManager and useModalVisibility hooks supporting priority-based modal visibility coordination.
Leaderboard Feature
src/app/(mobile-ui)/dev/leaderboard/page.tsx, src/app/(mobile-ui)/dev/page.tsx, src/services/points.ts
Adds new client-side leaderboard page with time filters, mock data generation, and auto-refresh. Integrates into dev tools menu. Adds getTimeLeaderboard() API method with limit and since parameters.
Perk Rewards Refactoring
src/components/TransactionDetails/PerkIcon.tsx, src/components/TransactionDetails/TransactionCard.tsx, src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx, src/components/TransactionDetails/TransactionDetailsReceipt.tsx, src/components/TransactionDetails/transactionTransformer.ts, src/services/services.types.ts, src/utils/history.utils.ts
Restructures perk reward data flow by replacing HistoryEntryPerk with HistoryEntryPerkReward type; adds PERK_REWARD entry type; creates reusable PerkIcon component; updates transaction detail rendering to show perk rewards via dedicated UI path with amount, status badge, and receipt details.
QR Pay Enhancements
src/app/(mobile-ui)/qr-pay/page.tsx
Enhances perk banner text to handle three discount scenarios (100%, >100%, standard); improves button interaction with touch-manipulation and select-none classes; prevents context menu on perk claim button.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Multiple heterogeneous features with significant logic density: new modal coordination system with observable registry pattern, substantial refactoring of perk reward handling across multiple transaction components with type changes, new leaderboard page with stateful filtering and API integration, and QR pay interactions. Requires understanding interactions between new modal system, perk data flow transformations, and transaction detail rendering paths.

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • jjramirezn
  • kushagrasarathe

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description Check ❓ Inconclusive The pull request has no description provided by the author. Since there is no content to evaluate, it is not possible to determine whether the description relates to any part of the changeset. The absence of a description means there is no attempt to explain the changes, making it impossible to establish a meaningful relationship with the changeset contents. Add a pull request description that explains the objectives and scope of this changeset. Given the breadth of changes (perk system rework, modal manager introduction, leaderboard feature, and modal consolidation), a description would help reviewers understand the overall intent and dependencies between these modifications.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "Feat/perk rework" is concise and directly related to a significant portion of the changeset. The majority of changes focus on refactoring the perk system, including renaming HistoryEntryPerk to HistoryEntryPerkReward, introducing a new PERK_REWARD transaction type, creating a PerkIcon component, and updating multiple components' perk-related logic. However, the title does not capture other substantial changes in this PR, including the new centralized modal manager system (useModalManager hook and ModalPriority enum), the balance warning modal consolidation, or the new leaderboard developer tool. While the title accurately reflects a major theme, it is partially related to the full changeset rather than fully summarizing the primary change.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/perk-rework

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

@coderabbitai coderabbitai bot added the enhancement New feature or request label Oct 23, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (9)
src/app/(mobile-ui)/qr-pay/page.tsx (1)

936-945: Consider extracting perk text logic for testability.

The inline function determining perk banner text works correctly but could be extracted to a helper function for improved testability and reusability. The three-case logic (100%, >100%, else) is clear but would benefit from unit testing.

Example extraction:

function getPerkBannerText(discountPercentage: number): string {
  if (discountPercentage === 100) {
    return 'We paid for this bill! Earn points, climb tiers and unlock even better perks.'
  } else if (discountPercentage > 100) {
    return `We gave you ${discountPercentage}% back — that's more than you paid! Earn points, climb tiers and unlock even better perks.`
  } else {
    return `We gave you ${discountPercentage}% cashback! Earn points, climb tiers and unlock even better perks.`
  }
}
src/services/points.ts (1)

106-108: Limit param check should allow 0 if ever needed.

Use explicit undefined check instead of truthiness to avoid silently dropping 0.

- if (params?.limit) queryParams.append('limit', params.limit.toString())
+ if (params?.limit !== undefined) queryParams.append('limit', params.limit.toString())
src/app/(mobile-ui)/dev/leaderboard/page.tsx (3)

72-73: Fix timer typings for browser code.

NodeJS.Timeout can mis-type in DOM builds. Prefer ReturnType.

-    const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)
+    const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)

107-115: Avoid polluting history during typing — use replace for debounced updates.

Typing in the datetime input pushes a new entry per keystroke. Use router.replace for debounced updates; keep push for explicit actions.

-    const updateURL = useCallback(
-        (timestamp: string) => {
+    const updateURL = useCallback(
+        (timestamp: string, { replace = false }: { replace?: boolean } = {}) => {
             const params = new URLSearchParams()
             params.set('since', timestamp)
-            router.push(`/dev/leaderboard?${params.toString()}`, { scroll: false })
+            const url = `/dev/leaderboard?${params.toString()}`
+            replace ? router.replace(url, { scroll: false }) : router.push(url, { scroll: false })
         },
         [router]
     )
@@
-                updateURL(timestamp)
-                debouncedFetch(timestamp)
+                updateURL(timestamp, { replace: true })
+                debouncedFetch(timestamp)

Also applies to: 190-208


318-324: “Last update” should reflect data, not initial mount.

Initialize as null and render only after first successful fetch to avoid a misleading timestamp.

-    const [lastUpdate, setLastUpdate] = useState<Date>(new Date())
+    const [lastUpdate, setLastUpdate] = useState<Date | null>(null)
-    <span className="text-sm text-gray-500">Last update: {lastUpdate.toLocaleTimeString()}</span>
+    <span className="text-sm text-gray-500">
+      Last update: {lastUpdate ? lastUpdate.toLocaleTimeString() : '—'}
+    </span>

Also applies to: 86-90

src/components/TransactionDetails/TransactionDetailsReceipt.tsx (1)

181-183: Dead code toggle: peanutFee is hard-disabled.

rowVisibilityConfig.peanutFee is false while a UI row still exists later. Remove the unreachable row or leave a clear TODO to avoid confusion.

-            peanutFee: false, // Perk fee logic removed - perks now show as separate transactions
+            // Perk fee logic removed - perks now show as separate transactions
+            // peanutFee remains off; consider deleting the row below.
+            peanutFee: false,

Optionally delete lines 963–969 rendering the Peanut fee row.

Also applies to: 963-969

src/components/TransactionDetails/transactionTransformer.ts (3)

70-79: Avoid inline 'perk' shape; extract a named domain type and clarify semantics

Define a shared type (e.g., HistoryEntryPerkOffer) in services.types and use it here to prevent drift and clarify that this is the offer/claim context, distinct from perkReward (the event). Also document units for discountPercentage (0–100 vs 0–1).

Apply in this file:

-import { type HistoryEntryPerkReward } from '@/services/services.types'
+import { type HistoryEntryPerkReward, type HistoryEntryPerkOffer } from '@/services/services.types'
-        perk?: {
-            claimed: boolean
-            discountPercentage: number
-            amountSponsored?: number
-            txHash?: string
-            merchantInfo?: {
-                promoDescription?: string
-            }
-        }
+        perk?: HistoryEntryPerkOffer

Add in services.types (example):

export interface HistoryEntryPerkOffer {
  claimed: boolean
  discountPercentage: number // percent 0–100
  amountSponsored?: number
  txHash?: string
  merchantInfo?: { promoDescription?: string }
}

Ensure no other files define a conflicting shape for perk/offer.


512-521: Avoid unsafe as casts for API data; add a narrow normalizer/guard

Casting unknown API shapes can mask bugs. Add small validators to coerce/guard fields.

-            perkReward: entry.extraData?.perkReward as HistoryEntryPerkReward | undefined,
-            perk: entry.extraData?.perk as
-                | {
-                      claimed: boolean
-                      discountPercentage: number
-                      amountSponsored?: number
-                      txHash?: string
-                      merchantInfo?: { promoDescription?: string }
-                  }
-                | undefined,
+            perkReward: normalizePerkReward(entry.extraData?.perkReward),
+            perk: normalizePerkOffer(entry.extraData?.perk),

Add helpers (example, place below function or in a utils module):

function normalizePerkReward(raw: any): HistoryEntryPerkReward | undefined {
  if (!raw) return undefined
  const discount = Number(raw.discountPercentage)
  return {
    reason: String(raw.reason ?? ''),
    discountPercentage: Number.isFinite(discount) ? discount : 0,
    originatingTxId: raw.originatingTxId ? String(raw.originatingTxId) : undefined,
    originatingTxType: raw.originatingTxType ? String(raw.originatingTxType) : undefined,
    perkName: raw.perkName ? String(raw.perkName) : undefined,
  }
}

function normalizePerkOffer(raw: any): HistoryEntryPerkOffer | undefined {
  if (!raw) return undefined
  const discount = Number(raw.discountPercentage)
  return {
    claimed: Boolean(raw.claimed),
    discountPercentage: Number.isFinite(discount) ? discount : 0,
    amountSponsored: raw.amountSponsored != null ? Number(raw.amountSponsored) : undefined,
    txHash: raw.txHash ? String(raw.txHash) : undefined,
    merchantInfo: raw.merchantInfo?.promoDescription
      ? { promoDescription: String(raw.merchantInfo.promoDescription) }
      : undefined,
  }
}

Confirm these helpers align with the server payload shapes to avoid accidental data loss.


333-339: Use data-driven display names instead of hard-coded copy for i18n support

The suggested refactor is valid. The perkName and reason properties exist on the HistoryEntryPerkReward type. This change aligns with the existing pattern in the same switch statement where other transaction types derive values from the entry object (e.g., lines 325–330 for merchant payments, line 315 for deposits).

         case EHistoryEntryType.PERK_REWARD:
             direction = 'receive'
             transactionCardType = 'receive'
             nameForDetails =
-                'Peanut Perk!'
+                entry.extraData?.perkReward?.perkName ??
+                entry.extraData?.perkReward?.reason ??
+                'Peanut Perk!'
             fullName = 'Peanut Perks'
             isPeerActuallyUser = false
             break

Note: Other occurrences of "Peanut Perk" in TransactionDetailsReceipt.tsx and qr-pay/page.tsx appear to be static UI labels; consider centralizing these separately if i18n coverage is needed beyond the data transformation layer.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 63b5aa8 and 7c7295d.

📒 Files selected for processing (14)
  • MINIMAL_MODAL_FIX.md (1 hunks)
  • MODAL_MANAGER_PR_PLAN.md (1 hunks)
  • src/app/(mobile-ui)/dev/leaderboard/page.tsx (1 hunks)
  • src/app/(mobile-ui)/dev/page.tsx (1 hunks)
  • src/app/(mobile-ui)/home/page.tsx (1 hunks)
  • src/app/(mobile-ui)/qr-pay/page.tsx (2 hunks)
  • src/components/TransactionDetails/PerkIcon.tsx (1 hunks)
  • src/components/TransactionDetails/TransactionCard.tsx (4 hunks)
  • src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx (1 hunks)
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx (5 hunks)
  • src/components/TransactionDetails/transactionTransformer.ts (4 hunks)
  • src/services/points.ts (1 hunks)
  • src/services/services.types.ts (1 hunks)
  • src/utils/history.utils.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/app/(mobile-ui)/dev/leaderboard/page.tsx (3)
src/services/points.ts (1)
  • pointsApi (6-131)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (206-215)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/services/points.ts (2)
src/utils/sentry.utils.ts (1)
  • fetchWithSentry (36-150)
src/constants/general.consts.ts (1)
  • PEANUT_API_URL (43-47)
src/components/TransactionDetails/TransactionDetailsReceipt.tsx (5)
src/hooks/useTransactionHistory.ts (1)
  • EHistoryEntryType (11-11)
src/components/TransactionDetails/PerkIcon.tsx (1)
  • PerkIcon (30-40)
src/components/Payment/PaymentInfoRow.tsx (1)
  • PaymentInfoRow (18-87)
src/utils/general.utils.ts (1)
  • formatDate (932-944)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (206-215)
src/components/TransactionDetails/transactionTransformer.ts (1)
src/services/services.types.ts (1)
  • HistoryEntryPerkReward (466-472)
src/components/TransactionDetails/TransactionCard.tsx (2)
src/hooks/useTransactionHistory.ts (1)
  • EHistoryEntryType (11-11)
src/components/TransactionDetails/PerkIcon.tsx (1)
  • PerkIcon (30-40)
🪛 markdownlint-cli2 (0.18.1)
MINIMAL_MODAL_FIX.md

72-72: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)


103-103: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

🔇 Additional comments (16)
src/app/(mobile-ui)/dev/page.tsx (1)

10-16: LGTM! Clean addition of the leaderboard dev tool.

The new entry follows the established pattern and integrates well with the existing dev tools list.

MODAL_MANAGER_PR_PLAN.md (2)

1-50: Documentation: Modal Manager Implementation Plan

This appears to be a planning document for future work, marked as "Planned for post-release" (line 3). The proposed priority-based modal coordination system is well-designed, but since this is deferred work, ensure it's clearly tracked and not blocking the current PR.


283-439: Example implementation code in planning document.

The proposed TypeScript implementation looks reasonable, with proper use of hooks, global state management, and priority-based coordination. Since this is deferred to a future PR, no action needed now.

src/utils/history.utils.ts (1)

25-25: LGTM! Clean addition of PERK_REWARD entry type.

The new enum member integrates correctly with the existing historyTypeToNumber/historyTypeFromNumber utility functions.

MINIMAL_MODAL_FIX.md (1)

1-101: Documentation: Duplicate Modal Fix

This document clearly describes the duplicate balance warning modal bug and the fix applied in home/page.tsx. The documentation matches the actual implementation changes.

Note: Static analysis flagged minor markdown formatting issues (lines 72, 103) suggesting headings instead of emphasis, but these don't affect functionality.

src/components/TransactionDetails/PerkIcon.tsx (1)

1-40: LGTM! Well-structured reusable component.

The PerkIcon component is clean and follows React best practices:

  • Proper TypeScript typing
  • Configurable sizing
  • Good use of Next.js Image optimization
  • Appropriate alt text for accessibility
src/app/(mobile-ui)/home/page.tsx (1)

168-175: LGTM! Correctly fixes the duplicate modal effect issue.

This change consolidates the duplicate balance warning modal effects by adding the missing isPostSignupActionModalVisible dependency. The dependency array now correctly includes all values referenced in the effect body (line 163), preventing stale closures and ensuring proper modal coordination.

This fix aligns with the documented solution in MINIMAL_MODAL_FIX.md.

src/app/(mobile-ui)/qr-pay/page.tsx (1)

970-981: LGTM! Excellent mobile interaction handling.

The added event handlers and styles properly prevent unintended mobile interactions:

  • onContextMenu prevents long-press context menu
  • touch-manipulation optimizes touch event handling
  • select-none prevents text selection during hold gesture
  • iOS-specific webkit styles prevent callout and tap highlighting

These improvements enhance the hold-to-claim UX on mobile devices.

src/components/TransactionDetails/TransactionCard.tsx (3)

22-23: LGTM! Proper imports for perk reward support.

The new imports correctly support the PERK_REWARD transaction type rendering.


76-76: LGTM! Clean conditional rendering for perk rewards.

The isPerkReward check (line 76) correctly identifies PERK_REWARD transactions, and the avatar rendering logic (lines 109-114) cleanly handles three cases:

  1. Perk rewards → PerkIcon
  2. Transactions with avatarUrl → Image
  3. Other transactions → TransactionAvatarBadge

StatusPill is consistently rendered across all cases when present.

Also applies to: 109-114


154-154: LGTM! Appropriate action text for perk rewards.

Displaying "Refund" for perk reward transactions is semantically correct and user-friendly.

src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx (1)

225-228: Amount strike-through tied only to cancel — good simplification.

Removing perk-driven strike-through avoids mixed semantics in amount styling. Looks correct.

src/services/services.types.ts (1)

465-472: Verify actual backend API constraints before narrowing types.

The originatingTxType field appears unused throughout the codebase (no assignments or reads found), while the reason field is only accessed for display without validation. The suggestions to narrow originatingTxType to HistoryEntryType and brand the reason field depend on backend API constraints that aren't enforced or validated in the frontend code. Before implementing these refactors, confirm with your backend team whether:

  • originatingTxType is actively populated with HistoryEntryType values
  • reason has a defined set of known values that should be enforced
  • Both fields are essential or if originatingTxId (currently commented out) indicates incomplete implementation
src/components/TransactionDetails/TransactionDetailsReceipt.tsx (2)

351-353: The enum value EHistoryEntryType.PERK_REWARD exists and the code will compile successfully.

The verification confirms that EHistoryEntryType.PERK_REWARD is properly defined in the enum and correctly exported through the import chain. The file src/hooks/useTransactionHistory.ts (line 11) re-exports EHistoryEntryType from @/utils/history.utils, where the PERK_REWARD member is defined (line 25 in the enum). The import path in TransactionDetailsReceipt.tsx is correct.


391-397: Incorrect — transaction.date is provided by the transformer; no crash expected.

TransactionDetails.date is non-optional and is set in mapTransactionDataForDrawer (src/components/TransactionDetails/transactionTransformer.ts → date: new Date(entry.timestamp)); TransactionDetailsReceipt also returns early when !transaction, so formatDate(new Date(transaction.date)) is safe. If you’re worried about malformed upstream timestamps, validate entry.timestamp in the transformer or make formatDate tolerate invalid Dates.

Likely an incorrect or invalid review comment.

src/components/TransactionDetails/transactionTransformer.ts (1)

16-16: Type-only import for perk reward — LGTM

Good use of a type import to avoid runtime cost. No issues spotted.

Comment on lines +461 to +486
{/* Perk eligibility banner */}
{transaction.extraDataForDrawer?.perk?.claimed && transaction.status !== 'pending' && (
<Card position="single" className="px-4 py-4">
<div className="flex items-center gap-3">
<PerkIcon size="small" />
<div className="flex flex-col gap-1">
<span className="font-semibold text-gray-900">Eligible for a Peanut Perk!</span>
<span className="text-sm text-gray-600">
{(() => {
const percentage = transaction.extraDataForDrawer.perk.discountPercentage
const amount = transaction.extraDataForDrawer.perk.amountSponsored
const amountStr = amount ? `$${amount.toFixed(2)}` : ''

if (percentage === 100) {
return `You received a full refund${amount ? ` (${amountStr})` : ''} as a Peanut Perk.`
} else if (percentage > 100) {
return `You received ${percentage}% back${amount ? ` (${amountStr})` : ''} — that's more than you paid!`
} else {
return `You received ${percentage}% cashback${amount ? ` (${amountStr})` : ''} as a Peanut Perk.`
}
})()}
</span>
</div>
</div>
</Card>
)}
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 23, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inconsistent perk shape vs new types.

This block reads extraDataForDrawer.perk.claimed and amountSponsored, but the new type HistoryEntryPerkReward removes amountSponsored and renames the entity. Align on perkReward and avoid amountSponsored-dependent copy.

- {transaction.extraDataForDrawer?.perk?.claimed && transaction.status !== 'pending' && (
+ {transaction.extraDataForDrawer?.perkReward && transaction.status !== 'pending' && (
     <Card position="single" className="px-4 py-4">
       <div className="flex items-center gap-3">
         <PerkIcon size="small" />
         <div className="flex flex-col gap-1">
           <span className="font-semibold text-gray-900">Eligible for a Peanut Perk!</span>
           <span className="text-sm text-gray-600">
-            {(() => {
-              const percentage = transaction.extraDataForDrawer.perk.discountPercentage
-              const amount = transaction.extraDataForDrawer.perk.amountSponsored
-              const amountStr = amount ? `$${amount.toFixed(2)}` : ''
-              if (percentage === 100) return `You received a full refund${amount ? ` (${amountStr})` : ''} as a Peanut Perk.`
-              else if (percentage > 100) return `You received ${percentage}% back${amount ? ` (${amountStr})` : ''} — that's more than you paid!`
-              else return `You received ${percentage}% cashback${amount ? ` (${amountStr})` : ''} as a Peanut Perk.`
-            })()}
+            {`You received ${transaction.extraDataForDrawer.perkReward.discountPercentage}% as a Peanut Perk.`}
           </span>
         </div>
       </div>
     </Card>
 )}
📝 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
{/* Perk eligibility banner */}
{transaction.extraDataForDrawer?.perk?.claimed && transaction.status !== 'pending' && (
<Card position="single" className="px-4 py-4">
<div className="flex items-center gap-3">
<PerkIcon size="small" />
<div className="flex flex-col gap-1">
<span className="font-semibold text-gray-900">Eligible for a Peanut Perk!</span>
<span className="text-sm text-gray-600">
{(() => {
const percentage = transaction.extraDataForDrawer.perk.discountPercentage
const amount = transaction.extraDataForDrawer.perk.amountSponsored
const amountStr = amount ? `$${amount.toFixed(2)}` : ''
if (percentage === 100) {
return `You received a full refund${amount ? ` (${amountStr})` : ''} as a Peanut Perk.`
} else if (percentage > 100) {
return `You received ${percentage}% back${amount ? ` (${amountStr})` : ''} — that's more than you paid!`
} else {
return `You received ${percentage}% cashback${amount ? ` (${amountStr})` : ''} as a Peanut Perk.`
}
})()}
</span>
</div>
</div>
</Card>
)}
{/* Perk eligibility banner */}
{transaction.extraDataForDrawer?.perkReward && transaction.status !== 'pending' && (
<Card position="single" className="px-4 py-4">
<div className="flex items-center gap-3">
<PerkIcon size="small" />
<div className="flex flex-col gap-1">
<span className="font-semibold text-gray-900">Eligible for a Peanut Perk!</span>
<span className="text-sm text-gray-600">
{`You received ${transaction.extraDataForDrawer.perkReward.discountPercentage}% as a Peanut Perk.`}
</span>
</div>
</div>
</Card>
)}
🤖 Prompt for AI Agents
In src/components/TransactionDetails/TransactionDetailsReceipt.tsx around lines
461-486, update the perk UI to use the new shape: check
transaction.extraDataForDrawer.perkReward.claimed (not .perk.claimed) and read
discountPercentage from perkReward; remove references to amountSponsored (it was
removed) — either drop the amount-dependent text or replace it with the new
sponsored amount field name if your type defines one (e.g., sponsoredAmount),
guarding for null/undefined. Also update local variable names and type guards
accordingly so the component compiles with the new HistoryEntryPerkReward shape.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

meh

Copy link
Contributor

Choose a reason for hiding this comment

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

@Hugo0, understood! I'll leave this as-is. Let me know if you need anything else. 👍

Comment on lines +104 to +121
try {
const queryParams = new URLSearchParams()
if (params?.limit) queryParams.append('limit', params.limit.toString())
if (params?.since) queryParams.append('since', params.since)

const response = await fetchWithSentry(
`${PEANUT_API_URL}/points/time-leaderboard?${queryParams.toString()}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
}
)

if (!response.ok) {
console.error('getTimeLeaderboard: API request failed', response.status, response.statusText)
return { success: false, data: null }
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 23, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Add auth header when available and avoid trailing “?” in URL.

If the endpoint requires auth, current call will 401. Also, when no params are set, you’ll request …time-leaderboard? with a dangling “?”. Prefer optional Authorization and conditional query join.

-        try {
-            const queryParams = new URLSearchParams()
-            if (params?.limit) queryParams.append('limit', params.limit.toString())
-            if (params?.since) queryParams.append('since', params.since)
-
-            const response = await fetchWithSentry(
-                `${PEANUT_API_URL}/points/time-leaderboard?${queryParams.toString()}`,
-                {
-                    method: 'GET',
-                    headers: {
-                        'Content-Type': 'application/json',
-                    },
-                }
-            )
+        try {
+            const queryParams = new URLSearchParams()
+            if (params?.limit !== undefined) queryParams.append('limit', String(params.limit))
+            if (params?.since) queryParams.append('since', params.since)
+
+            const qs = queryParams.toString()
+            const url = `${PEANUT_API_URL}/points/time-leaderboard${qs ? `?${qs}` : ''}`
+
+            const jwtToken = Cookies.get('jwt-token')
+            const headers: Record<string, string> = { 'Content-Type': 'application/json' }
+            if (jwtToken) headers.Authorization = `Bearer ${jwtToken}`
+
+            const response = await fetchWithSentry(url, {
+                method: 'GET',
+                headers,
+            })

Run this to confirm whether the endpoint is public or requires Bearer auth:


🏁 Script executed:

#!/bin/bash
rg -nP 'time-leaderboard' -C3

Length of output: 485


I need to verify a few more details about the auth pattern and existing code structure:<!-- [scratchpad_update]
[observations]

  • Web search for "getTimeLeaderboard Peanut API authentication" returned results about different projects (PeaNUT UPS dashboard, Peanut Protocol for tokens) - not the actual PEANUT_API being called
  • The actual Peanut API mentioned in the code appears to be a different service than the public search results
  • Peanut Protocol docs show API keys are required (index 8-1,8-3)
  • The code references PEANUT_API_URL environment variable
  • The code looks for jwt-token in cookies, not API keys

[pending]

  • Must search the actual codebase to understand endpoint requirements
  • Need to check if Cookies is imported in the file
  • Need to check for similar auth patterns in the codebase
    [scratchpad_end] -->

Let me search the codebase directly for patterns and dependencies:<!-- [scratchpad_update]
[observations]

  • Web search didn't find specifics about the actual "Peanut" time-leaderboard endpoint in the codebase
  • The search returned general API documentation and some Nansen API results
  • One result showed Nansen Points API: "No API key or authentication required" for their leaderboard (index 18-1, 18-5, 18-12)
  • This is likely a different Peanut service than the internal one in the code

[pending]

  • Must check codebase directly for:
    1. Whether Cookies library is imported in points.ts
    2. Other similar API calls to see the auth pattern
    3. Any environment docs/comments about the endpoint requirements
    4. Whether the trailing "?" issue is real (check URLSearchParams behavior)
      [actions]
  • Search for Cookies usage in the file
  • Look for other API calls with auth patterns
  • Check file imports and structure
    [scratchpad_end] -->

[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Check imports and Cookies usage in the file
head -30 src/services/points.ts
echo "---"
# Check if Cookies is imported
rg -n "import.*Cookies" src/services/points.ts
echo "---"
# Find other API calls in the same file for pattern comparison
rg -nP 'fetchWithSentry\(' src/services/points.ts -A5 | head -40

Length of output: 2198


[/run_scripts]


🏁 Script executed:

#!/bin/bash
# Get full getTimeLeaderboard function to see complete context
rg -nP 'getTimeLeaderboard' src/services/points.ts -A30 -B5

Length of output: 1836


Add missing auth header and fix trailing "?" in URL construction.

The getTimeLeaderboard endpoint is missing the JWT Bearer authorization header that all other protected endpoints in this service include (see getTierInfo and calculatePoints). Without it, requests will likely fail with 401. Additionally, when no query parameters are provided, the URL becomes time-leaderboard? with a dangling question mark.

Lines 104–117 need updating:

  • Conditionally add Authorization header with JWT token from cookies (following the established pattern in this file)
  • Construct the query string conditionally to omit trailing "?" when empty
        try {
            const queryParams = new URLSearchParams()
            if (params?.limit) queryParams.append('limit', params.limit.toString())
            if (params?.since) queryParams.append('since', params.since)

+           const qs = queryParams.toString()
+           const url = `${PEANUT_API_URL}/points/time-leaderboard${qs ? `?${qs}` : ''}`
+
+           const jwtToken = Cookies.get('jwt-token')
+           const headers: Record<string, string> = { 'Content-Type': 'application/json' }
+           if (jwtToken) headers.Authorization = `Bearer ${jwtToken}`

            const response = await fetchWithSentry(
-               `${PEANUT_API_URL}/points/time-leaderboard?${queryParams.toString()}`,
+               url,
                {
                    method: 'GET',
-                   headers: {
-                       'Content-Type': 'application/json',
-                   },
+                   headers,
                }
            )
🤖 Prompt for AI Agents
In src/services/points.ts around lines 104 to 121, the getTimeLeaderboard call
is missing the JWT Bearer Authorization header and always appends a trailing "?"
to the URL; fix by retrieving the JWT from cookies the same way other functions
in this file do, add an Authorization header 'Authorization': `Bearer ${token}`
when present, and only append the query string when queryParams.toString() is
non-empty (i.e., build the URL as `${PEANUT_API_URL}/points/time-leaderboard${qs
? `?${qs}` : ''}`) so no dangling question mark is produced.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

meh not an issue here

Copy link
Contributor

Choose a reason for hiding this comment

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

@Hugo0, understood! I'll leave it as is. 👍

    (\(\
    (- .-)
    o_(")(")

@notion-workspace
Copy link

Perk Rework

Copy link
Contributor

@Zishan-7 Zishan-7 left a comment

Choose a reason for hiding this comment

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

Pls checks comments

@Hugo0 Hugo0 merged commit 148ce6d into peanut-wallet-dev Oct 23, 2025
5 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Nov 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants