Skip to content

Feat/points#1317

Merged
Hugo0 merged 32 commits intopeanut-wallet-devfrom
feat/points
Oct 14, 2025
Merged

Feat/points#1317
Hugo0 merged 32 commits intopeanut-wallet-devfrom
feat/points

Conversation

@Hugo0
Copy link
Contributor

@Hugo0 Hugo0 commented Oct 12, 2025

POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS POINTS PERKS

@vercel
Copy link

vercel bot commented Oct 12, 2025

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

Project Deployment Preview Comments Updated (UTC)
peanut-wallet (dev-1) Ready Ready Preview Comment Oct 14, 2025 7:13pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 12, 2025

Walkthrough

Adds a points & perks subsystem (services, types, confetti, perk UI/hold-to-claim), integrates estimated-points across payments/withdraws/claims, updates transaction receipts/drawers for perks/points, adds dev tools (shake test), accessibility DrawerTitle inserts, crypto polyfills, badge assets, styles, and documentation edits.

Changes

Cohort / File(s) Change summary
Policies & Docs
./.cursorrules, ./.gitignore, ./WARP.md, docs/CHANGELOG.md, docs/consultant_feedback.md (deleted), docs/create_new_flow.md (deleted), docs/nextjs_api.md (deleted)
Adds cursor/editor rules, ignores PR.md, adds CHANGELOG, and removes legacy docs.
Dependencies & Config
package.json, next.config.js, src/config/wagmi.config.tsx, instrumentation.ts
Upgrades/adds deps (@headlessui/react, canvas-confetti, types), minor webpack whitespace, disables app-kit analytics, adds Node SSR crypto.randomUUID polyfill import.
Assets & Styles
src/assets/badges/index.ts, src/assets/index.ts, src/styles/globals.css
Re-exports tier badge SVGs and adds perk-shake keyframes + utility classes.
Points/Perks Services & Types
src/services/points.ts, src/services/services.types.ts, src/services/manteca.ts, src/services/invites.ts, src/utils/history.utils.ts, src/constants/general.consts.ts
New points API (getTierInfo, calculatePoints), new types/enums (tiers/invites/actions/perks), QrPayment.perk + claimPerk API, invites endpoint/shape changed, HistoryEntry.points added, and PERK_HOLD_DURATION_MS constant.
Utilities
src/utils/confetti.ts, src/utils/perk.utils.ts, src/utils/crypto-polyfill.ts
Adds confetti helpers/presets, getShakeClass and ShakeIntensity type, and browser crypto.randomUUID polyfill.
Points UI & Invites
src/app/(mobile-ui)/points/page.tsx, src/app/(mobile-ui)/points/invites/page.tsx
Points page shows tiers, badges, progress, inviter info and invitee list; invites page lists invitees and computed referral points.
QR Pay Perk Flow
src/app/(mobile-ui)/qr-pay/page.tsx
Implements hold-to-claim perk interaction (shaking, haptics, progress, optimistic claim, confetti) and perk eligibility/success UI.
Withdraw Points Estimation
src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
Adds React Query to calculate & pass estimated points for withdraw flow.
Payment/Claim Success Points
src/app/[...recipient]/client.tsx, src/components/Payment/Views/Status.payment.view.tsx, src/components/Claim/Link/Onchain/Success.view.tsx
Prefetches/calculates estimated points and renders points row with confetti on success screens.
Transaction Details (Perks/Points)
src/components/TransactionDetails/TransactionCard.tsx, src/components/TransactionDetails/TransactionDetailsReceipt.tsx, src/components/TransactionDetails/TransactionDetailsDrawer.tsx, src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx, src/components/TransactionDetails/transaction-details.utils.ts, src/components/TransactionDetails/transactionTransformer.ts
Adds perk indicators, perk banners, line-through amount handling, points rows, new hasPerk/points props and transformer wiring; drawer accessibility title added.
Drawer Accessibility Titles
src/components/Global/SupportDrawer/index.tsx, src/components/Global/TokenSelector/TokenSelector.tsx, src/components/Kyc/KycStatusDrawer.tsx, src/components/AddMoney/components/CryptoMethodDrawer.tsx, src/components/Global/Drawer
Inserts hidden DrawerTitle elements (screen-reader-only) into multiple drawers; Drawer export usage updated.
Dev Tools & Routes
src/app/(mobile-ui)/dev/page.tsx, src/app/(mobile-ui)/dev/shake-test/page.tsx, src/app/(mobile-ui)/dev/README.md, src/app/(mobile-ui)/layout.tsx, src/middleware.ts
Adds Dev Tools index and Shake Test page; exposes /dev as public path; middleware blocks /dev in production; empty dev README added.
Global/UI Tweaks & Types
src/components/Global/Modal/index.tsx, src/components/Global/Select/index.tsx, src/components/Home/AddMoneyPromptModal/index.tsx, src/components/LandingPage/yourMoney.tsx, src/interfaces/interfaces.ts, src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx
Modal border-radius tweak; Select value typing allows undefined; modal title string/style change; CTA sizing adjustments; removes legacy points/referral fields from interfaces; hasPerk prop added to header card.
Invites Service Changes
src/services/invites.ts
getInvites now calls /points/invites, returns PointsInvitesResponse, and throws on missing token or non-ok responses.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • jjramirezn
  • kushagrasarathe

Pre-merge checks and finishing touches

❌ Failed checks (2 inconclusive)
Check name Status Explanation Resolution
Title Check ❓ Inconclusive The title “Feat/points” is related to the introduction of points functionality but is overly generic and does not convey the full scope of the changeset, which also includes a new perks system, API endpoints, calculations, and extensive UI updates. It lacks specificity for team members scanning history to immediately understand the primary change. Consider renaming the pull request to a more descriptive title such as “Add Points and Perks V2 with Points API and Hold-to-Claim Flow” to clearly capture the main features introduced.
Description Check ❓ Inconclusive The description consists solely of repeated “POINTS PERKS” phrases without providing any substantive overview, making it too generic to inform reviewers about the scope, rationale, or key changes in the pull request. Please update the description to include a concise summary of the main objectives, notable implementations (e.g., pointsApi, perk claim flow, UI components), and any migration or breaking-change considerations for reviewers.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ 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/points

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.

@coderabbitai coderabbitai bot added the enhancement New feature or request label Oct 12, 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: 10

🧹 Nitpick comments (26)
src/config/wagmi.config.tsx (1)

58-58: LGTM! Analytics disabled as intended.

The change correctly disables AppKit analytics. This is a straightforward configuration update with no functional issues.

Optional: Consider a more professional comment style.

The comment works but is informal. For consistency with enterprise codebases, you could refactor to:

-        analytics: false, // no app-kit analytics plz
+        analytics: false, // Disable AppKit analytics
src/components/Global/ErrorSuppressor.tsx (1)

1-1: Remove unused ErrorSuppressor component

The file is empty and has no imports or usages—delete src/components/Global/ErrorSuppressor.tsx to eliminate dead code. If you intend to use an error‐suppressing boundary here, add an implementation (e.g., the previously suggested diff).

.cursorrules (1)

5-6: Consider documenting rationale for specific constraints.

The restrictions "never open SVG files, it crashes you" and "never run jq command, it crashes you" are very specific. Consider adding brief explanations of why these crash the system, as this context would help future maintainers understand the constraints.

src/services/manteca.ts (1)

143-164: LGTM: claimPerk method follows established patterns.

The implementation:

  • Uses fetchWithSentry for proper error tracking
  • Includes authentication via JWT token
  • Handles errors consistently with other API methods
  • Returns structured response with success flag and perk details

Consider adding validation for qrPaymentId parameter (e.g., checking it's a non-empty string) before making the API call to catch potential issues earlier.

docs/PERKS_FRONTEND_GAMEPLAN.md (1)

27-43: Replace hard tabs with spaces.

The TypeScript code block contains hard tabs instead of spaces, which is flagged by the markdown linter. While this doesn't affect functionality, it's inconsistent with standard markdown formatting.

Consider replacing the hard tabs with spaces (2 or 4 spaces) for consistency with markdown best practices.

docs/PERKS_FRONTEND_SPEC.md (1)

27-43: Replace hard tabs with spaces.

Similar to the gameplan document, this TypeScript code block uses hard tabs instead of spaces, triggering markdown linter warnings. For consistency with markdown formatting standards, consider using spaces.

src/styles/globals.css (1)

273-296: Respect reduced‑motion preferences for perk shake

Add a reduced‑motion override to avoid motion for users who prefer it.

 .perk-shake {
     animation: perkShake 0.5s ease-in-out;
 }
+
+@media (prefers-reduced-motion: reduce) {
+  .perk-shake {
+    animation: none !important;
+  }
+}
src/components/Claim/Link/Onchain/Success.view.tsx (2)

146-156: Optional: respect reduced‑motion for confetti

Skip confetti when users prefer reduced motion.

-    if (pointsData?.estimatedPoints && pointsDivRef.current) {
+    if (
+        pointsData?.estimatedPoints &&
+        pointsDivRef.current &&
+        window.matchMedia?.('(prefers-reduced-motion: no-preference)').matches
+    ) {

175-177: Optional: don’t block the whole view while points load

Consider rendering the success UI and lazily show the points row when ready, instead of a full‑page loader.

src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx (1)

46-56: Consider bypassing cache for fresh points (prior flow pattern)

Withdraw flow previously used crypto.randomUUID() in the queryKey to force fresh estimates every render. Current deterministic key may serve cached data.

If you want to keep the prior behavior, update the key:

- queryKey: ['calculate-points', 'withdraw', bankAccount?.id, amountToWithdraw],
+ queryKey: ['calculate-points', 'withdraw', bankAccount?.id, amountToWithdraw, crypto.randomUUID()],

Based on learnings

src/components/Payment/Views/Status.payment.view.tsx (3)

172-181: Guard confetti on numeric positivity; avoid truthy checks

Use an explicit numeric check to avoid firing on falsy/negative values and to be consistent with UI rendering.

-    if (points && pointsDivRef.current) {
+    if ((points ?? 0) > 0 && pointsDivRef.current) {
       // Calculate position of points div relative to viewport
       const rect = pointsDivRef.current.getBoundingClientRect()
       const x = (rect.left + rect.width / 2) / window.innerWidth
       const y = (rect.top + rect.height / 2) / window.innerHeight

       shootDoubleStarConfetti({ origin: { x, y } })
     }

251-258: Only render when points > 0; consider formatted display

  • Use a strict numeric check instead of relying on truthiness.
  • Optional: format points with toLocaleString for readability.
-                {points && (
+                {(points ?? 0) > 0 && (
                   <div ref={pointsDivRef} className="flex justify-center gap-2">
                     <Image src={STAR_STRAIGHT_ICON} alt="star" width={20} height={20} />
                     <p className="text-sm font-medium text-black">
-                        You&apos;ve earned {points} {points === 1 ? 'point' : 'points'}!
+                        You&apos;ve earned {Number(points).toLocaleString()} {points === 1 ? 'point' : 'points'}!
                     </p>
                   </div>
                 )}

25-29: Import consistency for STAR_STRAIGHT_ICON

Other modules import the star as a named export from '@/assets/icons'. Consider aligning to avoid duplicate bundles/loader paths.

Also applies to: 27-28

src/app/[...recipient]/client.tsx (2)

83-99: Include inputs in the queryKey to prevent stale caching

usdAmount and otherUserId influence results but are absent from the key. Add them to avoid serving stale points across states.

-const { data: pointsData } = useQuery({
-    queryKey: ['calculate-points', chargeDetails?.uuid, flow],
+const { data: pointsData } = useQuery({
+    queryKey: [
+        'calculate-points',
+        chargeDetails?.uuid,
+        flow,
+        usdAmount, // affects estimate
+        chargeDetails?.requestLink.recipientAccount.userId, // affects estimate
+    ],
     queryFn: () =>
         pointsApi.calculatePoints({
             actionType: PointsAction.P2P_REQUEST_PAYMENT,
             usdAmount: Number(usdAmount),
             otherUserId: chargeDetails?.requestLink.recipientAccount.userId,
         }),
     enabled: !!shouldFetchPoints,
     refetchOnWindowFocus: false,
 })

If you want to keep only the number in state, use select: data => data.estimatedPoints. Based on learnings


571-572: Propagate only when > 0 (optional)

Optional: pass undefined when 0 to avoid rendering logic complexity downstream.

-    points={pointsData?.estimatedPoints}
+    points={pointsData && pointsData.estimatedPoints > 0 ? pointsData.estimatedPoints : undefined}
src/app/(mobile-ui)/qr-pay/page.tsx (4)

235-268: Avoid stale state when updating qrPayment after perk claim

Use a functional state update to prevent stale closure issues and drop redundant perkClaimed state (claimed flag exists in qrPayment).

-const [perkClaimed, setPerkClaimed] = useState(false)
+// perkClaimed is derivable from qrPayment?.perk?.claimed; avoid duplicate state

 const claimPerk = useCallback(async () => {
   if (!qrPayment?.id) return
   setIsShaking(true)
   setIsClaimingPerk(true)

   try {
     const result = await mantecaApi.claimPerk(qrPayment.id)
     if (result.success) {
-      setQrPayment({
-        ...qrPayment,
-        perk: {
-          eligible: true,
-          discountPercentage: result.perk.discountPercentage,
-          claimed: true,
-          amountSponsored: result.perk.amountSponsored,
-          txHash: result.perk.txHash,
-        },
-      })
-      setPerkClaimed(true)
+      setQrPayment(prev =>
+        prev
+          ? {
+              ...prev,
+              perk: {
+                ...(prev.perk ?? { eligible: true }),
+                discountPercentage: result.perk.discountPercentage,
+                claimed: true,
+                amountSponsored: result.perk.amountSponsored,
+                txHash: result.perk.txHash,
+              },
+            }
+          : prev
+      )
       shootDoubleStarConfetti({ origin: { x: 0.5, y: 0.5 } })
     }
   } catch (error) {
     captureException(error)
     setErrorMessage('Could not claim perk. Please contact support')
   } finally {
     setIsClaimingPerk(false)
     setIsShaking(false)
   }
 }, [qrPayment])

Then update UI conditions to rely on qrPayment?.perk?.claimed only.

- {qrPayment?.perk?.eligible && !perkClaimed && !qrPayment.perk.claimed && ( ... )}
+ {qrPayment?.perk?.eligible && !qrPayment.perk.claimed && ( ... )}

- {(perkClaimed || qrPayment?.perk?.claimed) && ( ... )}
+ {qrPayment?.perk?.claimed && ( ... )}

151-166: Add abort handling for initiateQrPayment effect

Prevent setState on unmounted and cancel needless network work on rapid rescans.

 useEffect(() => {
   if (!qrCode || !isPaymentProcessorQR(qrCode)) return
   if (!!paymentLock) return
-  // Remove KYC gate blocking here - fetch immediately for lower latency
-  // The actual payment action is still gated by shouldBlockPay
+  const ac = new AbortController()

   setLoadingState('Fetching details')
   mantecaApi
-    .initiateQrPayment({ qrCode })
+    .initiateQrPayment({ qrCode /* , signal: ac.signal if supported */ })
     .then((pl) => setPaymentLock(pl))
     .catch((error) => setErrorInitiatingPayment(error.message))
     .finally(() => setLoadingState('Idle'))
-}, [paymentLock, qrCode, setLoadingState])
+  return () => {
+    try { ac.abort() } catch {}
+  }
+}, [paymentLock, qrCode, setLoadingState])

If fetchWithSentry supports AbortController signals, pass it; otherwise, guard with a mounted flag. Based on learnings


414-469: Copy: tighten eligible/claimed conditions and wording (optional)

  • Conditions should rely on qrPayment?.perk?.claimed only (once the refactor above is applied).
  • Optional: Harmonize star asset import to match other files (from '@/assets/icons').

Also applies to: 471-542


37-39: Import consistency for STAR_STRAIGHT_ICON

Elsewhere it comes from '@/assets/icons'. Consider aligning imports for consistency.

src/services/points.ts (1)

17-20: Optional: Remove Content-Type on GET.

Content-Type on GET is unnecessary and can cause some proxies to misbehave.

             headers: {
                 Authorization: `Bearer ${jwtToken}`,
-                'Content-Type': 'application/json',
             },
.optimization-notes.md (1)

14-14: Fix MD036 (no emphasis as heading).

Use plain text or a heading instead of bold-only.

-**Changed payment lock fetch to run in PARALLEL with KYC check**
+Changed payment lock fetch to run in parallel with KYC check.
src/app/(mobile-ui)/points/invites/page.tsx (3)

47-51: Use backend-provided contributedPoints instead of recomputing 20%.

Avoid drift/rounding mismatches by relying on API’s contributedPoints.

-    const totalPointsEarned =
-        invites?.invitees?.reduce((sum: number, invite: PointsInvite) => {
-            return sum + Math.floor(invite.totalPoints * 0.2)
-        }, 0) || 0
+    const totalPointsEarned =
+        invites?.invitees?.reduce((sum: number, invite: PointsInvite) => {
+            return sum + (invite.contributedPoints || 0)
+        }, 0) || 0

76-78: Also use contributedPoints per invite.

Keep display consistent with backend’s computation.

-                        const pointsEarned = Math.floor(invite.totalPoints * 0.2)
+                        const pointsEarned = invite.contributedPoints

69-74: Optional: Show an empty state when there are no invitees.

Improves UX vs. empty section.

                 <h2 className="font-bold">People you invited</h2>
 
-                {/* Full list */}
+                {/* Empty state */}
+                {invites?.invitees?.length === 0 && (
+                    <EmptyState icon="users" title="No invites yet" description="Share your invite link to start earning." />
+                )}
+
+                {/* Full list */}
src/app/(mobile-ui)/points/page.tsx (1)

161-161: Use contributedPoints from API (avoid manual 20% math).

Aligns with backend and ensures consistent rounding.

-                                const pointsEarned = Math.floor(invite.totalPoints * 0.2)
+                                const pointsEarned = invite.contributedPoints

Also applies to: 187-189

src/utils/confetti.ts (1)

24-27: Respect reduced motion preference.

Avoids animations for users with motion sensitivity.

 export const shootStarConfetti = (options: ConfettiOptions = {}) => {
     // Ensure we're on the client side
-    if (typeof window === 'undefined') return
+    if (typeof window === 'undefined') return
+    if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 51000ef and 17c389d.

⛔ Files ignored due to path filters (5)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • src/assets/badges/tier0.svg is excluded by !**/*.svg
  • src/assets/badges/tier1.svg is excluded by !**/*.svg
  • src/assets/badges/tier2.svg is excluded by !**/*.svg
  • src/assets/badges/tier3.svg is excluded by !**/*.svg
📒 Files selected for processing (37)
  • .cursorrules (1 hunks)
  • .gitignore (1 hunks)
  • .optimization-notes.md (1 hunks)
  • WARP.md (1 hunks)
  • docs/CHANGELOG.md (1 hunks)
  • docs/PERKS_FRONTEND_GAMEPLAN.md (1 hunks)
  • docs/PERKS_FRONTEND_SPEC.md (1 hunks)
  • package.json (3 hunks)
  • src/app/(mobile-ui)/points/invites/page.tsx (1 hunks)
  • src/app/(mobile-ui)/points/page.tsx (3 hunks)
  • src/app/(mobile-ui)/qr-pay/page.tsx (8 hunks)
  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx (3 hunks)
  • src/app/[...recipient]/client.tsx (3 hunks)
  • src/assets/badges/index.ts (1 hunks)
  • src/assets/index.ts (1 hunks)
  • src/components/AddMoney/components/CryptoMethodDrawer.tsx (2 hunks)
  • src/components/Claim/Link/Onchain/Success.view.tsx (6 hunks)
  • src/components/Global/ErrorSuppressor.tsx (1 hunks)
  • src/components/Global/Modal/index.tsx (1 hunks)
  • src/components/Global/ScreenOrientationLocker.tsx (1 hunks)
  • src/components/Global/SupportDrawer/index.tsx (1 hunks)
  • src/components/Global/TokenSelector/TokenSelector.tsx (2 hunks)
  • src/components/Home/AddMoneyPromptModal/index.tsx (2 hunks)
  • src/components/Kyc/KycStatusDrawer.tsx (2 hunks)
  • src/components/Payment/Views/Status.payment.view.tsx (5 hunks)
  • src/components/TransactionDetails/TransactionCard.tsx (4 hunks)
  • src/components/TransactionDetails/TransactionDetailsDrawer.tsx (2 hunks)
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx (4 hunks)
  • src/components/TransactionDetails/transaction-details.utils.ts (2 hunks)
  • src/components/TransactionDetails/transactionTransformer.ts (4 hunks)
  • src/config/wagmi.config.tsx (1 hunks)
  • src/services/invites.ts (2 hunks)
  • src/services/manteca.ts (2 hunks)
  • src/services/points.ts (1 hunks)
  • src/services/services.types.ts (1 hunks)
  • src/styles/globals.css (1 hunks)
  • src/utils/confetti.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-10-02T15:23:01.513Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1266
File: src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx:46-57
Timestamp: 2025-10-02T15:23:01.513Z
Learning: In the withdraw flow at src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx, the points calculation query intentionally uses crypto.randomUUID() in the queryKey dependency array to bypass React Query caching, ensuring fresh points estimates on every render. This is the intended behavior.

Applied to files:

  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
  • src/app/[...recipient]/client.tsx
  • src/components/Claim/Link/Onchain/Success.view.tsx
📚 Learning: 2025-05-22T15:38:48.586Z
Learnt from: kushagrasarathe
PR: peanutprotocol/peanut-ui#869
File: src/app/(mobile-ui)/withdraw/page.tsx:82-88
Timestamp: 2025-05-22T15:38:48.586Z
Learning: The country-specific withdrawal route exists at src/app/(mobile-ui)/withdraw/[...country]/page.tsx and renders the AddWithdrawCountriesList component with flow="withdraw".

Applied to files:

  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
📚 Learning: 2025-09-18T09:30:42.901Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1230
File: src/app/(mobile-ui)/withdraw/page.tsx:92-97
Timestamp: 2025-09-18T09:30:42.901Z
Learning: In src/app/(mobile-ui)/withdraw/page.tsx, the useEffect that calls setShowAllWithdrawMethods(true) when amountFromContext exists is intentionally designed to run only on component mount (empty dependency array), not when amountFromContext changes. This is the correct behavior for the withdraw flow where showing all methods should only happen on initial load when an amount is already present.

Applied to files:

  • src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx
📚 Learning: 2025-10-08T17:13:13.140Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1299
File: src/app/(mobile-ui)/points/page.tsx:41-51
Timestamp: 2025-10-08T17:13:13.140Z
Learning: In `src/app/(mobile-ui)/points/page.tsx`, the icon name "invite-heart" is intentionally used (not "inviter-heart") when displaying who invited the current user, as this is a deliberate design choice despite semantic differences with UserHeader usage.

Applied to files:

  • src/app/(mobile-ui)/points/page.tsx
  • src/app/(mobile-ui)/points/invites/page.tsx
🧬 Code graph analysis (19)
src/components/Global/TokenSelector/TokenSelector.tsx (1)
src/components/Global/Drawer/index.tsx (1)
  • DrawerTitle (89-89)
src/components/TransactionDetails/transactionTransformer.ts (1)
src/services/services.types.ts (1)
  • HistoryEntryPerk (463-468)
src/services/manteca.ts (2)
src/utils/sentry.utils.ts (1)
  • fetchWithSentry (36-150)
src/constants/general.consts.ts (1)
  • PEANUT_API_URL (43-47)
src/app/(mobile-ui)/withdraw/[country]/bank/page.tsx (1)
src/services/points.ts (1)
  • pointsApi (6-86)
src/components/Global/SupportDrawer/index.tsx (2)
src/context/SupportModalContext.tsx (1)
  • useSupportModalContext (22-28)
src/components/Global/Drawer/index.tsx (3)
  • Drawer (81-81)
  • DrawerContent (86-86)
  • DrawerTitle (89-89)
src/components/AddMoney/components/CryptoMethodDrawer.tsx (1)
src/components/Global/Drawer/index.tsx (1)
  • DrawerTitle (89-89)
src/app/(mobile-ui)/points/page.tsx (10)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/services/invites.ts (1)
  • invitesApi (7-89)
src/services/points.ts (1)
  • pointsApi (6-86)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/components/Global/EmptyStates/EmptyState.tsx (1)
  • EmptyState (13-28)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (210-219)
src/utils/general.utils.ts (2)
  • generateInvitesShareText (1323-1325)
  • getInitialsFromName (1263-1270)
src/services/services.types.ts (1)
  • PointsInvite (421-433)
src/components/Global/Card/index.tsx (1)
  • getCardPosition (14-19)
src/components/UserHeader/index.tsx (1)
  • VerifiedUserLabel (45-111)
src/app/(mobile-ui)/qr-pay/page.tsx (3)
src/components/Global/DirectSendQR/utils.ts (1)
  • isPaymentProcessorQR (108-115)
src/services/manteca.ts (1)
  • mantecaApi (102-290)
src/utils/confetti.ts (1)
  • shootDoubleStarConfetti (49-67)
src/services/services.types.ts (1)
src/utils/bridge-accounts.utils.ts (1)
  • BridgeKycStatus (34-34)
src/app/[...recipient]/client.tsx (1)
src/services/points.ts (1)
  • pointsApi (6-86)
src/components/TransactionDetails/TransactionDetailsReceipt.tsx (1)
src/components/Payment/PaymentInfoRow.tsx (1)
  • PaymentInfoRow (17-83)
src/components/Payment/Views/Status.payment.view.tsx (2)
src/constants/query.consts.ts (1)
  • TRANSACTIONS (2-2)
src/utils/confetti.ts (1)
  • shootDoubleStarConfetti (49-67)
src/components/Kyc/KycStatusDrawer.tsx (1)
src/components/Global/Drawer/index.tsx (2)
  • DrawerContent (86-86)
  • DrawerTitle (89-89)
src/components/TransactionDetails/TransactionCard.tsx (1)
src/services/services.types.ts (1)
  • HistoryEntryPerk (463-468)
src/services/invites.ts (3)
src/services/services.types.ts (1)
  • PointsInvitesResponse (435-445)
src/utils/sentry.utils.ts (1)
  • fetchWithSentry (36-150)
src/constants/general.consts.ts (1)
  • PEANUT_API_URL (43-47)
src/components/Claim/Link/Onchain/Success.view.tsx (4)
src/services/points.ts (1)
  • pointsApi (6-86)
src/utils/general.utils.ts (1)
  • formatTokenAmount (447-468)
src/utils/confetti.ts (1)
  • shootDoubleStarConfetti (49-67)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/services/points.ts (3)
src/services/services.types.ts (2)
  • TierInfo (410-419)
  • CalculatePointsRequest (456-460)
src/utils/sentry.utils.ts (1)
  • fetchWithSentry (36-150)
src/constants/general.consts.ts (1)
  • PEANUT_API_URL (43-47)
src/components/TransactionDetails/TransactionDetailsDrawer.tsx (1)
src/components/Global/Drawer/index.tsx (1)
  • DrawerTitle (89-89)
src/app/(mobile-ui)/points/invites/page.tsx (8)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/services/invites.ts (1)
  • invitesApi (7-89)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/components/Global/EmptyStates/EmptyState.tsx (1)
  • EmptyState (13-28)
src/services/services.types.ts (1)
  • PointsInvite (421-433)
src/components/Global/Card/index.tsx (1)
  • getCardPosition (14-19)
src/utils/general.utils.ts (1)
  • getInitialsFromName (1263-1270)
src/components/UserHeader/index.tsx (1)
  • VerifiedUserLabel (45-111)
🪛 markdownlint-cli2 (0.18.1)
.optimization-notes.md

14-14: Emphasis used instead of a heading

(MD036, no-emphasis-as-heading)

docs/PERKS_FRONTEND_SPEC.md

28-28: Hard tabs
Column: 1

(MD010, no-hard-tabs)


29-29: Hard tabs
Column: 1

(MD010, no-hard-tabs)


30-30: Hard tabs
Column: 1

(MD010, no-hard-tabs)


31-31: Hard tabs
Column: 1

(MD010, no-hard-tabs)


32-32: Hard tabs
Column: 1

(MD010, no-hard-tabs)


33-33: Hard tabs
Column: 1

(MD010, no-hard-tabs)


34-34: Hard tabs
Column: 1

(MD010, no-hard-tabs)


35-35: Hard tabs
Column: 1

(MD010, no-hard-tabs)


36-36: Hard tabs
Column: 1

(MD010, no-hard-tabs)


37-37: Hard tabs
Column: 1

(MD010, no-hard-tabs)


38-38: Hard tabs
Column: 1

(MD010, no-hard-tabs)


39-39: Hard tabs
Column: 1

(MD010, no-hard-tabs)


40-40: Hard tabs
Column: 1

(MD010, no-hard-tabs)


41-41: Hard tabs
Column: 1

(MD010, no-hard-tabs)

🔇 Additional comments (27)
src/components/Global/Modal/index.tsx (1)

80-80: LGTM! Standardized to Tailwind's built-in class.

The change from "border-rounded" to "rounded-md" correctly uses Tailwind's standard utility class, improving maintainability.

Note: The max-h-[] with empty brackets on the same line appears invalid (pre-existing). If this is unintentional, consider either removing it or specifying a value like max-h-screen.

src/components/Home/AddMoneyPromptModal/index.tsx (2)

36-36: LGTM! Clean simplification.

Defining the title as a plain string constant is straightforward and improves readability.


54-54: LGTM! Verify text size reduction is intentional.

The styling changes look good. The text size reduction from !text-xl to !text-lg should align with the design system. The dark:text-black class ensures the title remains black in dark mode, which is appropriate if parent components apply different text colors in dark mode.

Please confirm that the text size reduction aligns with the intended design changes across the application.

.gitignore (1)

5-6: LGTM!

Ignoring PR.md is a reasonable addition to prevent accidental commits of PR-related notes.

WARP.md (1)

1-1: Clarify the purpose and content of this file.

This markdown file contains only the text ".cursorrules", which seems unusual. Please verify:

  • Is this the intended content for WARP.md?
  • What is the purpose of this file?
  • Should this contain documentation or is it meant to be a reference/symlink?
package.json (2)

54-54: LGTM: canvas-confetti dependency addition.

The addition of canvas-confetti and its type definitions properly supports the new perk claim visual effects mentioned in the PR summary.

Based on learnings: canvas-confetti ^1.9.0 provides a lightweight browser confetti API and the version specified is current and stable.

Also applies to: 97-97


32-32: Verify @headlessui/react v2 compatibility
Major bump from v1 to v2 for Menu, Transition, Dialog, Listbox. Confirm all usages in src/components/Global/MoreInfo, Modal, Select, and ChainSelector have been updated to the v2 API (e.g. updated “as” prop patterns, “static” attribute, new component structure, revised Transition syntax).

src/components/Global/ScreenOrientationLocker.tsx (3)

8-11: LGTM: Proper environment detection.

Adding checks for window and screen API availability ensures the component works correctly in server-side rendering and restricted environments.


14-17: LGTM: Feature detection for orientation lock.

Checking for screen.orientation.lock availability before use prevents errors in browsers that don't support this API.


19-26: LGTM: Appropriate error handling for orientation lock.

Switching from captureException to console.debug is correct. Orientation lock failures are expected behavior in desktop browsers and shouldn't be treated as errors. The debug logging still provides visibility for troubleshooting.

.cursorrules (2)

10-15: LGTM: Good code quality guidelines.

The code quality guidelines promote important best practices like explicit imports, high code quality, risk awareness, and separation of concerns.


22-25: LGTM: Clear documentation guidelines.

The documentation guidelines establish clear expectations for documenting major changes and organizing documentation files.

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

410-467: LGTM: Well-structured points/perks type definitions.

The new type definitions are comprehensive and well-organized:

  • Clear interface names and field types
  • Appropriate use of optional fields
  • Enum provides type-safe action types
  • Integrates well with existing type system (reuses BridgeKycStatus)
src/services/manteca.ts (1)

37-43: LGTM: Perk field addition to QrPayment type.

The optional perk field is well-structured with appropriate sub-fields for eligibility tracking, discount information, and claim status.

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

49-49: LGTM! Excellent accessibility improvement.

The addition of a screen-reader-only title enhances accessibility by providing context for assistive technology users. This follows WCAG best practices and is consistently applied across drawer components in this PR.

src/assets/index.ts (1)

1-1: LGTM! Clean module export.

The badges re-export extends the public API appropriately, making tier badges accessible to consumers of this module.

src/components/Global/SupportDrawer/index.tsx (1)

9-9: LGTM! Consistent accessibility pattern.

The screen-reader-only title addition maintains consistency with the accessibility improvements applied across other drawer components in this PR.

src/components/AddMoney/components/CryptoMethodDrawer.tsx (1)

27-27: LGTM! Semantic alignment with visible heading.

The DrawerTitle text matches the visible heading on line 29, providing consistent messaging for both visual and screen-reader users. This is a well-implemented accessibility enhancement.

src/components/Global/TokenSelector/TokenSelector.tsx (1)

577-577: LGTM! Completes the accessibility enhancement pattern.

This is the final drawer component updated with a screen-reader-only title, completing the consistent accessibility pattern across all drawer components in this PR. The title accurately describes the drawer's purpose.

src/components/Kyc/KycStatusDrawer.tsx (1)

6-6: Good accessibility improvement with DrawerTitle

Screen-reader title inside DrawerContent looks good.

Confirm DrawerTitle is correctly associated as the dialog label (e.g., via aria-labelledby in the Drawer implementation).

Also applies to: 160-164

src/components/TransactionDetails/transaction-details.utils.ts (1)

17-17: LGTM: points row integration

Adding 'points' to the row key union and order is consistent with the new UI.

Also applies to: 37-37

src/assets/badges/index.ts (1)

1-4: Verify SVG module resolution/export shape

Re-exports look fine. Ensure your build config supports default exports for .svg (URL vs ReactComponent) consistently across the app; add a *.svg module declaration if needed.

If not present, consider a global.d.ts entry like:
declare module '*.svg' { const src: string; export default src }

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

156-158: Points row LGTM; ensure type coverage

The points visibility check and row rendering look good. Please verify TransactionDetails includes points?: number to keep types aligned.

Also applies to: 844-855


248-253: Good error handling upgrade for CoinGecko fetch

Explicit status check and clearer error logs improve DX. LGTM.

Also applies to: 260-261

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

91-94: Perk badge and discounted amount display look good; check overlay with StatusPill

  • UI logic for 100% vs partial discount is clear.
  • Verify the star at -right-1 -top-1 doesn’t clash with StatusPill positioning for avatars with a pill.

Also applies to: 136-141, 165-183


22-24: Type import alignment

Good to cast perk as HistoryEntryPerk. Ensure TransactionDetails.extraDataForDrawer?.perk is typed accordingly.

src/services/points.ts (1)

15-21: Authorization header properly redacted The sanitizeHeaders utility in src/utils/sentry.utils.ts masks any authorization header value as [REDACTED].

Comment on lines +62 to 70
if (isInvitesError || isTierInfoError) {
console.error('Error loading points data:', invitesError ?? tierInfoError)

if (isLoading) {
return <PeanutLoading coverFullScreen />
return (
<div className="mx-auto mt-6 w-full space-y-3 md:max-w-2xl">
<EmptyState icon="alert" title="Error loading points!" description="Please contact Support." />
</div>
)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Handle tier fetch failures (and missing data) as errors.

Prevents loader lock when getTierInfo fails or returns no data.

-    if (isInvitesError || isTierInfoError) {
+    if (isInvitesError || isTierInfoError || !tierInfo?.data || tierInfo?.success === false) {
         console.error('Error loading points data:', invitesError ?? tierInfoError)
 
         return (
             <div className="mx-auto mt-6 w-full space-y-3 md:max-w-2xl">
                 <EmptyState icon="alert" title="Error loading points!" description="Please contact Support." />
             </div>
         )
     }
📝 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
if (isInvitesError || isTierInfoError) {
console.error('Error loading points data:', invitesError ?? tierInfoError)
if (isLoading) {
return <PeanutLoading coverFullScreen />
return (
<div className="mx-auto mt-6 w-full space-y-3 md:max-w-2xl">
<EmptyState icon="alert" title="Error loading points!" description="Please contact Support." />
</div>
)
}
if (isInvitesError || isTierInfoError || !tierInfo?.data || tierInfo?.success === false) {
console.error('Error loading points data:', invitesError ?? tierInfoError)
return (
<div className="mx-auto mt-6 w-full space-y-3 md:max-w-2xl">
<EmptyState icon="alert" title="Error loading points!" description="Please contact Support." />
</div>
)
}
🤖 Prompt for AI Agents
In src/app/(mobile-ui)/points/page.tsx around lines 62 to 70, the current
conditional only treats isInvitesError or isTierInfoError as an error case—if
getTierInfo fails silently or returns no data the loader can lock; update the
conditional to also consider missing/invalid tierInfo (e.g., !tierInfo or
missing required fields) as an error, log both invitesError and
tierInfo/tierInfoError details, and return the same EmptyState error UI so the
component exits early instead of hanging in a loading state.

Comment on lines +41 to +56
const { data: pointsData, isLoading: isPointsDataLoading } = useQuery({
queryKey,
queryFn: () =>
pointsApi.calculatePoints({
actionType: PointsAction.P2P_SEND_LINK,
usdAmount: Number(
formatTokenAmount(
Number(formatUnits(claimLinkData.amount, claimLinkData.tokenDecimals)) * (tokenPrice ?? 0)
)
),
otherUserId: claimLinkData?.senderAddress,
}),
// Fetch only for logged in users.
enabled: !!authUser?.user.userId,
refetchOnWindowFocus: false,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: usdAmount calculation can become NaN due to formatted string

formatTokenAmount returns a localized string (e.g., "1,234.56"). Wrapping it in Number(...) yields NaN. Compute numerically from formatUnits and tokenPrice without formatting. Also, include dependencies in the queryKey to prevent stale results.

-    const { data: pointsData, isLoading: isPointsDataLoading } = useQuery({
-        queryKey,
-        queryFn: () =>
-            pointsApi.calculatePoints({
-                actionType: PointsAction.P2P_SEND_LINK,
-                usdAmount: Number(
-                    formatTokenAmount(
-                        Number(formatUnits(claimLinkData.amount, claimLinkData.tokenDecimals)) * (tokenPrice ?? 0)
-                    )
-                ),
-                otherUserId: claimLinkData?.senderAddress,
-            }),
+    const { data: pointsData, isLoading: isPointsDataLoading } = useQuery({
+        queryKey,
+        queryFn: () => {
+            const usdAmount =
+                Number(formatUnits(claimLinkData.amount, claimLinkData.tokenDecimals)) * (tokenPrice ?? 0)
+            return pointsApi.calculatePoints({
+                actionType: PointsAction.P2P_SEND_LINK,
+                usdAmount,
+                otherUserId: claimLinkData?.senderAddress,
+            })
+        },
         // Fetch only for logged in users.
         enabled: !!authUser?.user.userId,
         refetchOnWindowFocus: false,
     })

Also consider updating the key to reflect inputs (prevents stale cache):

- const queryKey = useMemo(() => ['calculate-points', 'claim-link', claimLinkData.link], [claimLinkData.link])
+ const queryKey = useMemo(
+   () => [
+     'calculate-points',
+     'claim-link',
+     claimLinkData.link,
+     tokenPrice ?? 0,
+     claimLinkData.tokenDecimals,
+     String(claimLinkData.amount),
+   ],
+   [claimLinkData.link, tokenPrice, claimLinkData.tokenDecimals, claimLinkData.amount]
+ )
🤖 Prompt for AI Agents
In src/components/Claim/Link/Onchain/Success.view.tsx around lines 41 to 56, the
usdAmount is computed by converting a formatted (localized) string to Number
which can produce NaN; also the queryKey is missing dynamic inputs causing stale
cache. Fix by computing usdAmount purely numerically (use formatUnits result *
(tokenPrice ?? 0) and ensure it's converted to a number without
localization/formatting), include relevant dependencies (e.g.,
claimLinkData.amount, claimLinkData.tokenDecimals, tokenPrice,
authUser?.user.userId) in the queryKey so the query invalidates/refreshes when
inputs change, and keep refetchOnWindowFocus behavior as needed.

import { StatusPillType } from '../Global/StatusPill'
import type { Address } from 'viem'
import { PEANUT_WALLET_CHAIN } from '@/constants'
import { HistoryEntryPerk } from '@/services/services.types'
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Type-safety for perk and points; remove casts and sanitize value

Avoid (entry as any) and explicit casts. Extend HistoryEntry types to include points?: number and extraData.perk?: HistoryEntryPerk, then drop the casts here. As a stopgap, guard points assignment.

Apply this minimal diff here:

-            perk: entry.extraData?.perk as HistoryEntryPerk | undefined,
+            perk: entry.extraData?.perk,
-        points: (entry as any).points,
+        points: typeof (entry as any).points === 'number' ? (entry as any).points : undefined,

And please update the HistoryEntry type accordingly in src/hooks/useTransactionHistory to include:

  • points?: number
  • extraData?: { ...; perk?: HistoryEntryPerk }

Run this script to locate and update the HistoryEntry definition:

Also applies to: 70-70, 484-485, 492-492


🏁 Script executed:

#!/bin/bash
# Find HistoryEntry definition(s) and extraData shape
rg -n -C3 -t ts -t tsx '\b(export\s+)?(interface|type)\s+HistoryEntry\b'
rg -n -C3 -t ts -t tsx '\bextraData\??\s*:\s*{'

Length of output: 135


Let's rerun with corrected patterns to locate the types:


🏁 Script executed:

#!/bin/bash
# Find HistoryEntry definition(s)
rg -nP 'export\s+(?:interface|type)\s+HistoryEntry\b' -g '*.ts' -g '*.tsx'
# Find extraData property shapes
rg -nP '\bextraData\s*:\s*{' -g '*.ts' -g '*.tsx'
# Find points declarations
rg -nP '\bpoints\??\s*:' -g '*.ts' -g '*.tsx'

Length of output: 1489


🏁 Script executed:

#!/bin/bash
# Inspect HistoryEntry type in utils
sed -n '80,140p' src/utils/history.utils.ts
sed -n '260,320p' src/utils/history.utils.ts
# Locate services.types file
rg -l 'services.types' -g '*.ts'
# Inspect HistoryEntryPerk in services.types
rg -nP 'HistoryEntryPerk' -g 'src/services/services.types.ts' -C3

Length of output: 3128


Type-safety: remove casts and extend HistoryEntry

  • In src/components/TransactionDetails/transactionTransformer.ts replace:
    -            perk: entry.extraData?.perk as HistoryEntryPerk,
    +            perk: entry.extraData?.perk,
    -        points: (entry as any).points,
    +        points: typeof entry.points === 'number' ? entry.points : undefined,
  • In src/utils/history.utils.ts update HistoryEntry:
    -  extraData?: Record<string, any>
    +  extraData?: {
    +    perk?: HistoryEntryPerk
    +    [key: string]: any
    +  }
    +  points?: number
📝 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
import { HistoryEntryPerk } from '@/services/services.types'
// src/components/TransactionDetails/transactionTransformer.ts
import { HistoryEntryPerk } from '@/services/services.types'
// …other code…
return {
// …other fields…
perk: entry.extraData?.perk,
points: typeof entry.points === 'number' ? entry.points : undefined,
// …other fields…
}
Suggested change
import { HistoryEntryPerk } from '@/services/services.types'
// src/utils/history.utils.ts
export interface HistoryEntry {
// …existing properties…
extraData?: {
perk?: HistoryEntryPerk
[key: string]: any
}
points?: number
// …other properties…
}
🤖 Prompt for AI Agents
In src/components/TransactionDetails/transactionTransformer.ts around line 16
and in src/utils/history.utils.ts update the HistoryEntry type: remove any
runtime casts to HistoryEntryPerk in transactionTransformer and instead extend
the HistoryEntry type in history.utils to include the Perk-specific fields used
by the transformer (e.g., id, perkId, perkName or whatever properties the
transformer expects). Change imports so transactionTransformer imports the new
extended HistoryEntry (or a union type) instead of casting, update function
signatures and variable types to use the extended type, and ensure all usages
align with the expanded type definition to restore full TypeScript type-safety.

Comment on lines +47 to +49
} catch (e) {
console.log(e)
throw new Error('Failed to fetch invites')
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use console.error and preserve original error details.

The error logging should use console.error instead of console.log for proper error categorization. Additionally, the generic error message loses the original error context, making debugging harder.

Apply this diff to improve error handling:

-        } catch (e) {
-            console.log(e)
-            throw new Error('Failed to fetch invites')
+        } catch (e) {
+            console.error('Failed to fetch invites:', e)
+            throw e instanceof Error ? e : new Error('Failed to fetch invites')
📝 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
} catch (e) {
console.log(e)
throw new Error('Failed to fetch invites')
} catch (e) {
console.error('Failed to fetch invites:', e)
throw e instanceof Error ? e : new Error('Failed to fetch invites')
🤖 Prompt for AI Agents
In src/services/invites.ts around lines 47 to 49, the catch block currently logs
with console.log and throws a generic Error losing the original context; replace
console.log with console.error and preserve the original error when rethrowing
(for example by passing the caught error as the cause or appending its message)
so logs capture the full stack and the thrown Error retains the original error
details for debugging.

Comment on lines +9 to +13
const jwtToken = Cookies.get('jwt-token')
if (!jwtToken) {
console.error('getTierInfo: No JWT token found')
return { success: false, data: null }
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Throw on missing JWT to surface errors (avoid silent failure).

Returning { success: false } hides the failure and causes indefinite spinners upstream. Throw instead.

-            if (!jwtToken) {
-                console.error('getTierInfo: No JWT token found')
-                return { success: false, data: null }
-            }
+            if (!jwtToken) {
+                console.error('getTierInfo: No JWT token found')
+                throw new Error('No JWT token found')
+            }
📝 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 jwtToken = Cookies.get('jwt-token')
if (!jwtToken) {
console.error('getTierInfo: No JWT token found')
return { success: false, data: null }
}
const jwtToken = Cookies.get('jwt-token')
if (!jwtToken) {
console.error('getTierInfo: No JWT token found')
throw new Error('No JWT token found')
}
🤖 Prompt for AI Agents
In src/services/points.ts around lines 9 to 13, the current code silently
returns { success: false, data: null } when the JWT cookie is missing; change
this to throw a clear Error (e.g., "getTierInfo: Missing JWT token") instead so
callers receive an exception and upstream spinners/errors surface; ensure the
thrown Error is descriptive and remove the early return path so callers handle
or propagate the exception.

Comment on lines +22 to +25
if (!response.ok) {
console.error('getTierInfo: API request failed', response.status, response.statusText)
return { success: false, data: null }
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Treat non-OK responses as errors (aligns with invitesApi/calculatePoints).

Throw on non-OK to let react-query set isError and render the error UI.

-            if (!response.ok) {
-                console.error('getTierInfo: API request failed', response.status, response.statusText)
-                return { success: false, data: null }
-            }
+            if (!response.ok) {
+                console.error('getTierInfo: API request failed', response.status, response.statusText)
+                throw new Error(`getTierInfo failed with status ${response.status}`)
+            }
📝 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
if (!response.ok) {
console.error('getTierInfo: API request failed', response.status, response.statusText)
return { success: false, data: null }
}
if (!response.ok) {
console.error('getTierInfo: API request failed', response.status, response.statusText)
throw new Error(`getTierInfo failed with status ${response.status}`)
}
🤖 Prompt for AI Agents
In src/services/points.ts around lines 22 to 25, the handler currently logs and
returns { success: false, data: null } for non-OK fetch responses; instead,
throw an Error when response.ok is false so react-query sets isError and the
error UI renders. Replace the console.error + return with throwing a descriptive
Error (include response.status and response.statusText or the response
body/error details if available) so callers receive a rejected promise and can
handle it via react-query's error flow.

Comment on lines +29 to +33
} catch (error) {
console.error('getTierInfo: Unexpected error', error)
return { success: false, data: null }
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Don’t swallow errors; rethrow to fail the query.

Let react-query handle the error state instead of returning { success: false }.

-        } catch (error) {
-            console.error('getTierInfo: Unexpected error', error)
-            return { success: false, data: null }
-        }
+        } catch (error) {
+            console.error('getTierInfo: Unexpected error', error)
+            throw error instanceof Error ? error : new Error('getTierInfo failed')
+        }
📝 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
} catch (error) {
console.error('getTierInfo: Unexpected error', error)
return { success: false, data: null }
}
},
} catch (error) {
console.error('getTierInfo: Unexpected error', error)
throw error instanceof Error ? error : new Error('getTierInfo failed')
}
},
🤖 Prompt for AI Agents
In src/services/points.ts around lines 29 to 33, the catch block logs the error
then swallows it by returning { success: false, data: null }; instead, remove
the swallow and rethrow the caught error so upstream (react-query) can handle
the failure state — you can still log the error if desired, but after logging do
throw error (or rethrow) rather than returning a success=false payload.

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

♻️ Duplicate comments (3)
src/app/(mobile-ui)/points/page.tsx (3)

64-66: Indefinite loading state (previously flagged).

This issue was already identified in a previous review: gating on !tierInfo?.data can cause indefinite loading if the API returns success without data. Please refer to the earlier comment suggesting to move this check to the error condition instead.


68-76: Incomplete error handling (previously flagged).

This issue was already identified: error condition should also check for missing or invalid tierInfo?.data. Please refer to the earlier comment with the suggested diff.


102-103: Division by zero (previously flagged).

The progress calculation can divide by zero when nextTierThreshold is 0. This was addressed in a previous review with a suggested fix to clamp the value.

🧹 Nitpick comments (2)
src/components/TransactionDetails/TransactionDetailsReceipt.tsx (1)

166-167: Minor redundancy in points visibility check.

The logic correctly controls row visibility. However, line 167 has a slight redundancy: transaction.points && transaction.points > 0. Since transaction.points being truthy already implies it's non-zero for numbers, the explicit > 0 check is technically unnecessary. That said, it adds clarity and is defensively safe.

If you prefer more concise code, you could simplify to:

-points: !!(transaction.points && transaction.points > 0),
+points: !!transaction.points,

But the current explicit form is perfectly fine and may be clearer for future maintainers.

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

147-147: Simplify redundant checks.

The condition invites && invites?.invitees && invites.invitees.length > 0 contains redundant checks. If invites?.invitees exists, then invites must exist.

Apply this diff to simplify:

-                {invites && invites?.invitees && invites.invitees.length > 0 && (
+                {invites?.invitees?.length > 0 && (
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 17c389d and 81ec4d3.

📒 Files selected for processing (2)
  • src/app/(mobile-ui)/points/page.tsx (4 hunks)
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-08T17:13:13.140Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1299
File: src/app/(mobile-ui)/points/page.tsx:41-51
Timestamp: 2025-10-08T17:13:13.140Z
Learning: In `src/app/(mobile-ui)/points/page.tsx`, the icon name "invite-heart" is intentionally used (not "inviter-heart") when displaying who invited the current user, as this is a deliberate design choice despite semantic differences with UserHeader usage.

Applied to files:

  • src/app/(mobile-ui)/points/page.tsx
🧬 Code graph analysis (2)
src/components/TransactionDetails/TransactionDetailsReceipt.tsx (1)
src/components/Payment/PaymentInfoRow.tsx (1)
  • PaymentInfoRow (17-83)
src/app/(mobile-ui)/points/page.tsx (10)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/services/invites.ts (1)
  • invitesApi (7-89)
src/services/points.ts (1)
  • pointsApi (6-86)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/components/Global/EmptyStates/EmptyState.tsx (1)
  • EmptyState (13-28)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (206-215)
src/utils/general.utils.ts (2)
  • generateInvitesShareText (1332-1334)
  • getInitialsFromName (1272-1279)
src/services/services.types.ts (1)
  • PointsInvite (421-433)
src/components/Global/Card/index.tsx (1)
  • getCardPosition (14-19)
src/components/UserHeader/index.tsx (1)
  • VerifiedUserLabel (45-109)
🔇 Additional comments (8)
src/components/TransactionDetails/TransactionDetailsReceipt.tsx (3)

19-19: LGTM!

The new imports support the Points earned feature correctly. Image from Next.js provides optimized rendering, and STAR_STRAIGHT_ICON is appropriately used in the points display row.

Also applies to: 24-24


259-262: Improved error handling for CoinGecko API.

The addition of the !res.ok check and descriptive error message significantly improves error handling. Previously, only JSON parsing errors were caught; now HTTP errors (4xx, 5xx) are also properly detected and logged with context.

Also applies to: 270-270


859-870: Points earned row implementation looks good.

The new Points row follows the established pattern correctly:

  • Consistent conditional rendering (checks both rowVisibilityConfig.points and transaction.points, matching the pattern used for other rows like txId at line 462)
  • Proper use of PaymentInfoRow component
  • Correct border management via shouldHideBorder
  • Appropriate star icon rendering with fixed dimensions
src/app/(mobile-ui)/points/page.tsx (5)

30-33: LGTM: Clean tier badge helper.

The fallback to TIER_0_BADGE handles unexpected tier values safely.


34-54: LGTM: Well-structured queries.

Both queries are properly gated by userId and track separate loading/error states, following React Query best practices.


83-119: Well-structured tier display.

The tier card implementation is clean with good UI details: proper singular/plural handling for points, progressive progress bar, and clear tier badges.


120-137: LGTM: Clear conditional messaging.

The logic correctly displays either the inviter information or a generic info message. The use of invite-heart icon is consistent with the design pattern noted in previous reviews.

Based on learnings


163-199: LGTM: Well-implemented invites list.

The invites rendering is solid:

  • Correct 20% points calculation at Line 167
  • Safe fallback from fullName to username at Line 178
  • Proper use of VerifiedUserLabel component
  • Good UX with clickable cards navigating to user profiles

)}
{invites?.length === 0 && (

{invites?.invitees?.length === 0 && (
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle undefined invitees array.

The condition invites?.invitees?.length === 0 won't show the empty state if invitees is undefined or null (expression evaluates to undefined === 0). While the API should always return an array, defensive handling prevents edge cases.

Apply this diff to handle both cases:

-                {invites?.invitees?.length === 0 && (
+                {(!invites?.invitees || invites.invitees.length === 0) && (
📝 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
{invites?.invitees?.length === 0 && (
{(!invites?.invitees || invites.invitees.length === 0) && (
🤖 Prompt for AI Agents
In src/app/(mobile-ui)/points/page.tsx around line 204, the empty-state
condition uses invites?.invitees?.length === 0 which evaluates to false when
invitees is undefined; change the check to use a null-coalescing or explicit
undefined check so undefined/null counts as empty (for example use
(invites?.invitees?.length ?? 0) === 0 or !invites?.invitees ||
invites.invitees.length === 0) so the empty state renders when invitees is
missing or an empty array.

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: 6

🧹 Nitpick comments (8)
instrumentation.ts (1)

6-7: Prefer @ts-expect-error over @ts-ignore.

Using @ts-expect-error is safer than @ts-ignore because it will fail if the error no longer exists, preventing outdated suppressions from lingering in the codebase.

Apply this diff:

-        // @ts-ignore - polyfill for SSR
+        // @ts-expect-error - polyfill for SSR
         globalThis.crypto = nodeCrypto.webcrypto
src/utils/crypto-polyfill.ts (1)

9-9: Prefer @ts-expect-error over @ts-ignore.

Using @ts-expect-error is safer than @ts-ignore because it will fail if the error no longer exists, preventing outdated suppressions from lingering in the codebase.

Apply this diff:

-    // @ts-ignore - Adding polyfill for crypto.randomUUID
+    // @ts-expect-error - Adding polyfill for crypto.randomUUID
     crypto.randomUUID = function randomUUID(): string {
src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx (1)

213-213: Clarify the reason for line-through (accessibility).

When hasPerk is true, the amount is struck through but without an explanation for assistive tech. Add an aria-label or aria-describedby to communicate “Sponsored by Peanut” or similar.

Example:

- <h1 className={`text-3xl font-extrabold md:text-4xl ${status === 'cancelled' || hasPerk ? 'line-through' : ''} ${status === 'cancelled' ? 'text-grey-1' : ''}`}>
+ <h1
+   aria-label={hasPerk ? 'Original amount (sponsored by Peanut)' : undefined}
+   className={`text-3xl font-extrabold md:text-4xl ${status === 'cancelled' || hasPerk ? 'line-through' : ''} ${status === 'cancelled' ? 'text-grey-1' : ''}`}
+ >
src/app/(mobile-ui)/layout.tsx (1)

24-24: Public /dev route now allowed — verify intent and simplify check.

Confirm that Dev Tools under /dev are safe to expose unauthenticated. Consider simpler, less error‑prone prefix checks:

-const publicPathRegex = /^\/(request\/pay|claim|pay\/.+$|support|invite|dev)/
+const publicPrefixes = ['/request/pay', '/claim', '/pay/', '/support', '/invite', '/dev']

Then replace the isPublicPath calc:

// replace regex test with:
const isPublicPath = publicPrefixes.some((p) => pathName.startsWith(p))
src/components/TransactionDetails/TransactionDetailsReceipt.tsx (2)

363-379: Perk banner a11y (optional).

Consider adding role="status" or aria-live="polite" if this banner appears dynamically after state changes so screen readers announce it.


877-888: Format points for readability.

Large point values should be locale-formatted.

-    <span>{transaction.points}</span>
+    <span>{Number(transaction.points).toLocaleString()}</span>
src/styles/globals.css (1)

273-373: Respect reduced motion preference (accessibility).

Wrap shake animations with a reduced-motion fallback to avoid discomfort.

+@media (prefers-reduced-motion: reduce) {
+  .perk-shake-weak,
+  .perk-shake-medium,
+  .perk-shake-strong,
+  .perk-shake-intense {
+    animation: none !important;
+  }
+}
+
.perk-shake-weak {
-    animation: perkShakeWeak 0.15s ease-in-out infinite;
+    animation: perkShakeWeak 0.15s ease-in-out infinite;
+    will-change: transform;
     transform-origin: center center;
}
...
.perk-shake-intense {
-    animation: perkShakeIntense 0.08s ease-in-out infinite;
+    animation: perkShakeIntense 0.08s ease-in-out infinite;
+    will-change: transform;
     transform-origin: center center;
}
src/app/(mobile-ui)/dev/shake-test/page.tsx (1)

13-16: Timer typing, cleanup, and pointer cancel.

  • Use ReturnType to avoid NodeJS.Timeout mismatch in browser.
  • Clear timers on unmount; add onPointerCancel to handle interruptions.
-    const [holdTimer, setHoldTimer] = useState<NodeJS.Timeout | null>(null)
-    const [progressInterval, setProgressInterval] = useState<NodeJS.Timeout | null>(null)
+    const [holdTimer, setHoldTimer] = useState<ReturnType<typeof setTimeout> | null>(null)
+    const [progressInterval, setProgressInterval] = useState<ReturnType<typeof setInterval> | null>(null)

Add onPointerCancel:

 <Button
   onPointerDown={startHold}
   onPointerUp={cancelHold}
   onPointerLeave={cancelHold}
+  onPointerCancel={cancelHold}
   shadowSize="4"
   className="relative overflow-hidden"
 >

Add unmount cleanup:

import { useEffect } from 'react'
// ...
useEffect(() => {
  return () => {
    if (holdTimer) clearTimeout(holdTimer)
    if (progressInterval) clearInterval(progressInterval)
    if ('vibrate' in navigator) navigator.vibrate(0)
  }
}, [holdTimer, progressInterval])

Also applies to: 96-115, 193-197

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 81ec4d3 and 687f16d.

📒 Files selected for processing (17)
  • instrumentation.ts (1 hunks)
  • src/app/(mobile-ui)/dev/README.md (1 hunks)
  • src/app/(mobile-ui)/dev/page.tsx (1 hunks)
  • src/app/(mobile-ui)/dev/shake-test/page.tsx (1 hunks)
  • src/app/(mobile-ui)/layout.tsx (1 hunks)
  • src/app/(mobile-ui)/qr-pay/page.tsx (8 hunks)
  • src/components/Global/Modal/index.tsx (1 hunks)
  • src/components/Global/Select/index.tsx (2 hunks)
  • src/components/LandingPage/yourMoney.tsx (1 hunks)
  • src/components/TransactionDetails/TransactionCard.tsx (4 hunks)
  • src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx (3 hunks)
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx (6 hunks)
  • src/config/wagmi.config.tsx (2 hunks)
  • src/interfaces/interfaces.ts (2 hunks)
  • src/services/manteca.ts (2 hunks)
  • src/styles/globals.css (1 hunks)
  • src/utils/crypto-polyfill.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/app/(mobile-ui)/dev/README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/Global/Modal/index.tsx
  • src/services/manteca.ts
  • src/components/TransactionDetails/TransactionCard.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
src/app/(mobile-ui)/dev/page.tsx (1)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (206-215)
src/interfaces/interfaces.ts (1)
src/services/services.types.ts (1)
  • Account (228-233)
src/app/(mobile-ui)/qr-pay/page.tsx (3)
src/components/Global/DirectSendQR/utils.ts (1)
  • isPaymentProcessorQR (108-115)
src/utils/confetti.ts (1)
  • shootDoubleStarConfetti (49-67)
src/utils/general.utils.ts (1)
  • formatNumberForDisplay (334-380)
src/app/(mobile-ui)/dev/shake-test/page.tsx (2)
src/utils/confetti.ts (1)
  • shootDoubleStarConfetti (49-67)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/components/TransactionDetails/TransactionDetailsReceipt.tsx (1)
src/components/Payment/PaymentInfoRow.tsx (1)
  • PaymentInfoRow (17-83)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (16)
src/config/wagmi.config.tsx (2)

2-2: LGTM!

The polyfill import is correctly positioned at the top to ensure crypto.randomUUID is available before DaimoPayProvider initialization.


59-59: LGTM!

The analytics flag change is clear and intentional based on the comment.

instrumentation.ts (1)

3-11: LGTM!

The SSR polyfill implementation is correct and properly scoped to the Node.js runtime. It ensures crypto.randomUUID is available for DaimoPayProvider in server-side rendering contexts.

src/utils/crypto-polyfill.ts (2)

10-29: LGTM!

The UUID v4 implementation correctly follows RFC 4122, uses secure random generation via crypto.getRandomValues, and properly sets the version and variant bits.


6-6: LGTM!

The runtime checks are appropriate for browser-side polyfilling, ensuring the polyfill only runs when needed.

src/components/LandingPage/yourMoney.tsx (1)

34-34: LGTM! Clean styling update for flexible button sizing.

The change from fixed dimensions to auto-sizing with explicit padding is a good approach that makes the button more adaptable to content changes while maintaining consistent spacing. The added background, text, and hover styling are appropriate.

Verify that the auto-sizing works well across different viewport sizes and doesn't cause any layout shifts, especially during page load or when the button is rendered.

src/components/Global/Select/index.tsx (1)

12-26: LGTM! Type refinement improves flexibility.

Expanding the value prop type to include undefined aligns with React conventions where undefined is preferred for unset optional props. This change is backwards compatible and provides better type safety.

src/interfaces/interfaces.ts (2)

319-319: LGTM! Clean addition to the interface.

The invitedBy field is well-documented with a clear comment and appropriate nullable type. This aligns with the new Points/Invites system introduced in the PR.


314-314: Verify that totalPoints is mapped from stats.pointsV2.totalPoints.

  • Confirm the backend/API populates this field for backward compatibility.
  • Add a deprecation plan to remove totalPoints in a future release.
src/app/(mobile-ui)/dev/page.tsx (1)

8-67: Dev tools index looks good.

Clean, data-driven, accessible links. No issues.

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

260-263: Good resiliency: response.ok check before parsing.

The added CoinGecko error path avoids cryptic JSON parse failures. LGTM.

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

321-381: LGTM! Well-implemented hold-to-claim interaction.

The progressive hold mechanic with escalating shake intensity and haptic feedback provides excellent user feedback. The implementation correctly:

  • Updates progress every 50ms for smooth animation
  • Triggers haptic feedback only when intensity changes to avoid over-vibration
  • Stores timers in state for proper cleanup
  • Checks for Vibration API availability before use

255-268: LGTM! Proper cleanup of hold interaction.

The cancelHold function correctly clears all timers, resets state, and stops vibration when the user releases early. The vibration stop (navigator.vibrate(0)) is particularly good UX, preventing lingering haptic feedback.


520-577: LGTM! Well-structured conditional UI for perk states.

The progressive UI states (payment info → eligibility prompt → success banner) provide clear user guidance. The dynamic messaging on lines 571-573 appropriately distinguishes between full sponsorship (100% discount) and partial discounts, enhancing clarity.


581-647: LGTM! Well-implemented button states and interactions.

The button logic correctly handles the perk claim flow:

  • Hold-to-claim button with visual progress feedback when eligible
  • Fallback to standard navigation buttons otherwise
  • Proper pointer event handling (onPointerDown/Up/Leave) for cross-platform compatibility
  • Disabled state during API call prevents duplicate submissions

The progress fill animation (lines 592-598) provides clear visual feedback during the hold interaction.


84-89: LGTM! Proper timer cleanup in reset.

Timer cleanup is correctly integrated into the reset flow, working in conjunction with the unmount cleanup effect (lines 92-97) to prevent memory leaks.

Comment on lines +168 to +169
// Fetch payment lock immediately on load (parallel with KYC check for faster UX)
// KYC blocking still happens via shouldBlockPay check at line 310
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Correct the line reference in comment.

The comment states "KYC blocking still happens via shouldBlockPay check at line 310", but line 310 is within the claimPerk function and unrelated to KYC blocking. The actual KYC gate check occurs at line 442 where shouldBlockPay determines whether to render the payment UI or KYC modals.

-    // Fetch payment lock immediately on load (parallel with KYC check for faster UX)
-    // KYC blocking still happens via shouldBlockPay check at line 310
+    // Fetch payment lock immediately on load (parallel with KYC check for faster UX)
+    // KYC blocking still happens via shouldBlockPay check at line 442
📝 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
// Fetch payment lock immediately on load (parallel with KYC check for faster UX)
// KYC blocking still happens via shouldBlockPay check at line 310
// Fetch payment lock immediately on load (parallel with KYC check for faster UX)
// KYC blocking still happens via shouldBlockPay check at line 442
🤖 Prompt for AI Agents
In src/app/(mobile-ui)/qr-pay/page.tsx around lines 168-169, update the inline
comment that currently says "KYC blocking still happens via shouldBlockPay check
at line 310" to point to the correct line where the KYC gate lives (line 442) —
replace 310 with 442 and keep the rest of the comment unchanged so it correctly
documents that shouldBlockPay at line 442 controls rendering of payment UI vs
KYC modals.

Comment on lines +300 to +309
setQrPayment({
...qrPayment,
perk: {
eligible: true,
discountPercentage: result.perk.discountPercentage,
claimed: true,
amountSponsored: result.perk.amountSponsored,
txHash: result.perk.txHash,
},
})
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Use functional setState to avoid stale closure.

The setQrPayment call uses qrPayment from the closure rather than the current state. If qrPayment changes between when claimPerk is created and when this update runs (e.g., due to concurrent updates or rapid user actions), stale data could be used.

Apply this diff to use the functional form of setState:

-                setQrPayment({
-                    ...qrPayment,
+                setQrPayment((prev) => ({
+                    ...prev,
                     perk: {
                         eligible: true,
                         discountPercentage: result.perk.discountPercentage,
                         claimed: true,
                         amountSponsored: result.perk.amountSponsored,
                         txHash: result.perk.txHash,
                     },
-                })
+                }))
📝 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
setQrPayment({
...qrPayment,
perk: {
eligible: true,
discountPercentage: result.perk.discountPercentage,
claimed: true,
amountSponsored: result.perk.amountSponsored,
txHash: result.perk.txHash,
},
})
setQrPayment((prev) => ({
...prev,
perk: {
eligible: true,
discountPercentage: result.perk.discountPercentage,
claimed: true,
amountSponsored: result.perk.amountSponsored,
txHash: result.perk.txHash,
},
}))
🤖 Prompt for AI Agents
In src/app/(mobile-ui)/qr-pay/page.tsx around lines 300 to 309, the setQrPayment
call reads qrPayment from a potentially stale closure; change it to the
functional form setQrPayment(prev => ({ ...prev, perk: { eligible: true,
discountPercentage: result.perk.discountPercentage, claimed: true,
amountSponsored: result.perk.amountSponsored, txHash: result.perk.txHash } }))
so the updater receives the latest state and merges the new perk fields safely.

<div className={`relative ${className}`}>
{label && <div className="mb-3 text-xs font-bold">{label}</div>}
<Listbox value={value} onChange={onChange}>
<Listbox value={value || undefined} onChange={onChange}>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify Headless UI Listbox compatibility with null.

The defensive || undefined conversion ensures that null is always converted to undefined before passing to Listbox. This is a safe pattern, but it would be good to verify whether Headless UI's Listbox specifically requires undefined over null for the unselected state.


🌐 Web query:

Does Headless UI Listbox component require undefined instead of null for unselected value state?

💡 Result:

Short answer: Yes — use null for a controlled Listbox when there's no selection (not undefined).

Why: Headless UI stopped accepting undefined as the controlled value (it triggered React's uncontrolled→controlled warning). The project recommends initializing controlled Listbox/Combobox values to null when you want “no selection”; type defs were updated to allow null and the Combobox adds a nullable prop for explicitly allowing empty values. [1][2][3][4]

Sources:

  • Headless UI Listbox docs. [1]
  • GitHub issue: "Error when value starts as undefined on a controlled ListBox" (recommendation: use useState(null)). [2]
  • GitHub PR/issues adding null to type definitions. [3]
  • Combobox nullable prop discussion (notes that undefined doesn't work; use null + nullable). [4]

Use null for unselected Listbox value. Headless UI Listbox requires a controlled value to be null when no option is selected; replace value={value || undefined} with value={value ?? null}.

🤖 Prompt for AI Agents
In src/components/Global/Select/index.tsx around line 47, the Listbox value is
set to value || undefined which yields undefined when unselected, but Headless
UI requires a controlled Listbox to use null for "no selection"; change the prop
to pass value ?? null instead (ensure any related prop/types accept null as a
valid value) so the Listbox receives null when nothing is selected.

Comment on lines +166 to 168
peanutFee: !!(transaction.extraDataForDrawer?.perk?.claimed && transaction.status !== 'pending'),
points: !!(transaction.points && transaction.points > 0),
comment: !!transaction.memo?.trim(),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Tighten visibility: show perks/points only on completed transactions.

Currently:

  • peanutFee shows when perk is claimed and status !== 'pending' (includes cancelled/failed).
  • points show when > 0 regardless of status.

This can mislead. Limit both to completed:

- peanutfFee: !!(transaction.extraDataForDrawer?.perk?.claimed && transaction.status !== 'pending'),
- points: !!(transaction.points && transaction.points > 0),
+ peanutFee: !!(transaction.extraDataForDrawer?.perk?.claimed && transaction.status === 'completed'),
+ points: !!(transaction.points && transaction.points > 0 && transaction.status === 'completed'),

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

🤖 Prompt for AI Agents
In src/components/TransactionDetails/TransactionDetailsReceipt.tsx around lines
166 to 168, tighten visibility logic so perks/points only display for completed
transactions: change the peanutFee condition to require
transaction.extraDataForDrawer?.perk?.claimed && transaction.status ===
'completed' and change the points condition to require transaction.points &&
transaction.points > 0 && transaction.status === 'completed' (leave comment/memo
logic unchanged).

points: number
referrerAddress: string | null
referredUsersPoints: number
// OLD Points V1 fields removed - use pointsV2 from stats instead
Copy link
Contributor

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

Clarify the migration path and document removed fields.

The comment mentions using pointsV2 from stats but doesn't specify:

  • Which fields were removed (helpful for maintainers reviewing history)
  • The complete path to access pointsV2 (e.g., is it IUserProfile.stats.pointsV2?)
  • Whether stats is a new property on Account or accessed elsewhere

Consider expanding the comment:

-    // OLD Points V1 fields removed - use pointsV2 from stats instead
+    // Removed Points V1 fields (points, referrerAddress, referredUsersPoints).
+    // For points data, use IUserProfile.stats.pointsV2 instead.

Additionally, verify that the stats.pointsV2 structure is properly defined:


🏁 Script executed:

#!/bin/bash
# Description: Verify pointsV2 interface exists and check for any remaining references to removed Account fields

# Search for pointsV2 type/interface definition
echo "=== Searching for pointsV2 definition ==="
rg -n --type=ts -A 5 'pointsV2.*[:{]'

# Check for usage of removed Account fields that might cause runtime errors
echo "=== Checking for removed Account field usage ==="
rg -n --type=ts -C 3 '\baccount\.(points|referrerAddress|referredUsersPoints)\b'

Length of output: 90


🏁 Script executed:

#!/bin/bash
echo "=== Searching for 'stats' in interfaces.ts ==="
rg -n 'stats' -C2 src/interfaces/interfaces.ts

echo "=== Searching for any Stats interface/type definition ==="
rg -n 'interface Stats' -C2

echo "=== Re-checking for pointsV2 references across codebase ==="
rg -n 'pointsV2' -C2

Length of output: 1796


Define missing stats property and pointsV2 types

  • IUserProfile comments reference stats.pointsV2, but no stats property or Stats interface exists.
  • Add a stats: Stats field (and/or on Account if needed) and define:
    interface Stats {
      pointsV2: {
        totalPoints: number;
        inviteCount: number;
        // …other V2 fields
      };
    }
  • Update comments to list removed V1 fields (points, referrerAddress, referredUsersPoints) and point to stats.pointsV2.
🤖 Prompt for AI Agents
In src/interfaces/interfaces.ts around line 289, the IUserProfile references
stats.pointsV2 but there is no stats property or Stats interface defined; add a
Stats interface and a stats: Stats field (also add to Account if Account
contains profile-like data) that defines pointsV2 with at least totalPoints:
number and inviteCount: number (and placeholder comments for other V2 fields),
remove/mark old V1 fields (points, referrerAddress, referredUsersPoints) in
comments and update the IUserProfile comment to point consumers to
stats.pointsV2 instead of the removed V1 fields.

Comment on lines +308 to +309
// OLD Points V1 fields removed - use pointsV2 in stats instead
// Points V2: Use stats.pointsV2.totalPoints, pointsV2.inviteCount, etc.
Copy link
Contributor

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

Document removed fields for maintainer clarity.

The comments provide good migration guidance mentioning pointsV2.totalPoints and inviteCount, but don't list which fields were removed. Based on the AI summary, several fields were removed (points, transactions, referralsPointsTxs, totalReferralConnections, referredUsers, totalReferralPoints).

Consider adding a comment listing the removed fields:

-    // OLD Points V1 fields removed - use pointsV2 in stats instead
-    // Points V2: Use stats.pointsV2.totalPoints, pointsV2.inviteCount, etc.
+    // Removed Points V1 fields: points, transactions, referralsPointsTxs, 
+    // totalReferralConnections, referredUsers, totalReferralPoints
+    // Points V2: Use stats.pointsV2.totalPoints, stats.pointsV2.inviteCount, etc.

Verify that stats is properly defined on IUserProfile:


🏁 Script executed:

#!/bin/bash
# Description: Check if stats property exists on IUserProfile and verify pointsV2 structure

# Search for IUserProfile stats property definition
echo "=== Searching for IUserProfile.stats definition ==="
rg -n --type=ts -B 5 -A 10 'interface IUserProfile' | rg -A 15 'stats'

# Check for any code still accessing removed IUserProfile fields
echo "=== Checking for removed IUserProfile field usage ==="
rg -n --type=ts -C 3 'userProfile\.(points|transactions|referralsPointsTxs|totalReferralConnections|referredUsers|totalReferralPoints)\b'

Length of output: 877



🏁 Script executed:

#!/bin/bash
echo "=== Searching for 'stats' property across interfaces ==="
rg -n --type=ts 'stats\s*:' -C3
echo "=== Searching for 'pointsV2' definition or usage ==="
rg -n --type=ts 'pointsV2' -C3

Length of output: 2013


Fix missing stats.pointsV2 in IUserProfile and document removed fields.

The comments reference a nonexistent stats property and pointsV2. Either add a stats: { pointsV2: { totalPoints: number; inviteCount: number; … } } field to IUserProfile or update these comments to reflect the actual shape (e.g. top-level totalPoints). Also list the removed Points V1 fields (points, transactions, referralsPointsTxs, totalReferralConnections, referredUsers, totalReferralPoints).

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

♻️ Duplicate comments (1)
src/components/TransactionDetails/TransactionDetailsReceipt.tsx (1)

166-168: Gate perks & points to completed transactions only.

Still shows Peanut-sponsored fee and earned points for cancelled/failed transactions (status !== 'pending' / no status check). Only display these once the transaction is completed to avoid misleading users.

-            peanutFee: !!(transaction.extraDataForDrawer?.perk?.claimed && transaction.status !== 'pending'),
-            points: !!(transaction.points && transaction.points > 0),
+            peanutFee: !!(
+                transaction.extraDataForDrawer?.perk?.claimed && transaction.status === 'completed'
+            ),
+            points: !!(transaction.points && transaction.points > 0 && transaction.status === 'completed'),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 704e983 and eb724c4.

📒 Files selected for processing (4)
  • next.config.js (1 hunks)
  • src/app/(mobile-ui)/qr-pay/page.tsx (8 hunks)
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx (6 hunks)
  • src/middleware.ts (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • next.config.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/(mobile-ui)/qr-pay/page.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/TransactionDetails/TransactionDetailsReceipt.tsx (1)
src/components/Payment/PaymentInfoRow.tsx (1)
  • PaymentInfoRow (17-83)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (1)
src/middleware.ts (1)

91-91: Matcher configuration updated correctly.

Adding /dev/:path* to the matcher ensures the middleware runs on all dev routes, which is necessary for the blocking logic to work.

Note that this configuration means the middleware will also run for /dev routes in non-production environments (development, staging, test). Verify this aligns with your intended behavior—currently, /dev pages will be accessible in all non-production environments.

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

🧹 Nitpick comments (5)
docs/CHANGELOG.md (5)

20-22: Minor wording polish for clarity.

Adjust phrasing for readability.

- QR payment flow now fetches payment locks in parallel with KYC checks for latency reduction
- Perk claiming uses optimistic UI updates for instant feedback (claim happens in background)
+ QR payment flow now fetches payment locks in parallel with KYC checks to reduce latency
+ Perk claiming uses optimistic UI updates for instant feedback (claim happens in the background)

11-11: Tidy tier phrasing.

“N–M” reads better than “M-N Tier”.

-- **Points System V2** with tier-based progression (0-4 Tier)
+- **Points System V2** with tier-based progression (Tiers 0–4)

29-29: Clarify SSR polyfill phrasing.

Make target environment explicit.

-- `crypto.randomUUID` polyfill for older Node.js environments in SSR
+- `crypto.randomUUID` polyfill for older Node.js versions in SSR environments

26-26: Backend fix listed in UI changelog — verify scope or link to backend PR.

If the BigInt fix shipped in a different repo, either remove it here or add a cross‑repo link; otherwise clarify the UI‑side impact (e.g., formatting/parsing).

Would you like me to add a cross-repo link if you provide the backend PR/commit?


8-9: Optional: add link references and release scaffolding.

To follow Keep a Changelog fully, add link refs at the bottom (e.g., [Unreleased]) and include version/date headers when cutting a release.

I can draft the link references once you confirm the previous tag (e.g., vX.Y.Z).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between eb724c4 and 0a9beb0.

📒 Files selected for processing (1)
  • docs/CHANGELOG.md (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (1)
docs/CHANGELOG.md (1)

10-18: Solid “Added” section.

Clear, user-facing bullets. Nice job aligning with Keep a Changelog.

Comment on lines +23 to +26
- Removed Points V1 legacy fields from `Account` and `IUserProfile` interfaces

### Fixed
- BigInt type handling in points balance calculations (backend)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Promote legacy field removal to a “Removed” section and mark as BREAKING.

Removing Points V1 fields is a breaking change for integrators. Move it out of “Changed”, add a “Removed” section, and note migration impact.

 ## [Unreleased]

 ### Added
@@
 ### Changed
- QR payment flow now fetches payment locks in parallel with KYC checks for latency reduction
- Perk claiming uses optimistic UI updates for instant feedback (claim happens in background)
- Dev pages excluded from production builds for faster compile times
- Removed Points V1 legacy fields from `Account` and `IUserProfile` interfaces
+ QR payment flow now fetches payment locks in parallel with KYC checks to reduce latency
+ Perk claiming uses optimistic UI updates for instant feedback (claim happens in the background)
+ Dev pages excluded from production builds for faster compile times
+
+### Removed
+- [BREAKING] Removed Points V1 legacy fields from `Account` and `IUserProfile` interfaces.
+  - Migration: update consumers to use Points V2 types and fields (e.g., TierInfo, balances). Add a short migration note or link.

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

🤖 Prompt for AI Agents
In docs/CHANGELOG.md around lines 23 to 26, the removal of Points V1 legacy
fields is currently listed under "Changed" but is a breaking change; move that
bullet to a new "Removed" section and mark it as BREAKING (e.g., add a "Removed"
header above the item and prepend "BREAKING:" or similar to the bullet), and
update the note to mention migration impact for integrators so consumers know
they must adapt to the removal.

@Hugo0
Copy link
Contributor Author

Hugo0 commented Oct 14, 2025

@cursor review in depth

@Hugo0
Copy link
Contributor Author

Hugo0 commented Oct 14, 2025

@coderabbitai review in depth

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 14, 2025

Tip

For best results, initiate chat on the files or code changes.

@Hugo0, I'll provide a comprehensive review of this Points/Perks V2 feature. This is a substantial addition with ~40 files changed. Here's my in-depth analysis:

🏗️ Architecture & Design

Strengths:

  • Clean separation of concerns with dedicated pointsApi and updates to mantecaApi
  • Parallel fetching optimization (KYC + payment locks) is well-documented in .optimization-notes.md
  • Optimistic UI patterns for perk claiming improve UX

Concerns:

  1. API Response Handling Inconsistency: In src/services/invites.ts, you're now throwing errors on failures instead of returning empty arrays. This breaks backward compatibility. Consider:

    // Current approach throws
    if (!response.ok) throw new Error('Failed to fetch invites')
    
    // Consider returning a success wrapper instead
    return { success: false, data: null, error: 'Failed to fetch invites' }
  2. Type Safety: In src/components/TransactionDetails/transactionTransformer.ts, you're using (entry as any).points which bypasses type checking. Add proper typing to the HistoryEntry interface instead.

🔒 Security

Critical Issues:

  1. JWT Token Handling: Multiple files (points.ts, invites.ts) access js-cookie directly. If token is missing, you throw errors. Consider:

    • Centralizing auth token retrieval
    • Adding token refresh logic
    • Consistent error handling for auth failures
  2. API Error Exposure: In src/services/points.ts:

    if (!response.ok) {
        throw new Error(`Failed to fetch tier info: ${response.status}`)
    }

    Consider not exposing raw status codes to the client in production.

⚡ Performance

Good:

  • Parallel fetching in qr-pay/page.tsx reduces latency by 8-10x (excellent optimization!)
  • Pre-fetching points in claim and payment flows

Concerns:

  1. Confetti Library Bundle Size: canvas-confetti adds to bundle. Consider:

    • Lazy loading: const confetti = (await import('canvas-confetti')).default
    • Or use a lighter alternative
  2. Multiple useQuery Calls: Several components make independent calculatePoints calls. Consider:

    • Caching strategy review
    • Ensuring enabled flags prevent unnecessary fetches

🎯 State Management

Issues in src/app/(mobile-ui)/qr-pay/page.tsx:

  1. Timer Cleanup: Hold-to-claim has multiple timers that need cleanup:

    useEffect(() => {
        return () => {
            if (holdTimer) clearTimeout(holdTimer)
            if (progressInterval) clearInterval(progressInterval)
        }
    }, [holdTimer, progressInterval])

    The dependency array should be empty [] for cleanup-only effects.

  2. Race Conditions: The optimistic perkClaimed state could get out of sync if the API call fails but succeeds on retry. Consider using a state machine or more explicit status tracking:

    type PerkStatus = 'idle' | 'claiming' | 'claimed' | 'failed'

🧪 Testing Gaps

Based on the summaries, I don't see test files added for:

  • Points calculation logic
  • Perk claiming flow (especially optimistic updates)
  • Hold-to-claim interaction state machine
  • API error scenarios
  • Confetti and haptic feedback (these can be mocked)

Recommendation: Add tests for critical paths, especially the payment flow with perks.

♿ Accessibility

Good additions:

  • Screen-reader-only drawer titles throughout

Missing:

  1. Haptic Feedback Fallbacks: The dev shake test page uses navigator.vibrate without checking support:

    // Current
    navigator.vibrate([50])
    
    // Better
    if (navigator.vibrate) {
        navigator.vibrate([50])
    }
  2. Keyboard Navigation: Hold-to-claim button needs keyboard equivalent. Add onKeyDown handler:

    onKeyDown={(e) => {
        if (e.key === 'Enter' || e.key === ' ') {
            e.preventDefault()
            startHold()
        }
    }}

🐛 Potential Bugs

  1. Screen Orientation Lock: In src/components/Global/ScreenOrientationLocker.tsx, you removed Sentry error reporting. While reduced logging is good, complete silence on orientation lock failures might hide real issues on some devices.

  2. BigInt Serialization: The changelog mentions "BigInt handling in backend points balance calculations" but I don't see explicit JSON serialization handling. Ensure all BigInt values are converted to strings before JSON.stringify.

  3. Dev Pages in Production: src/middleware.ts redirects /dev paths in production. Good! But consider also:

    • Ensuring no dev imports in production bundles
    • Using process.env.NODE_ENV checks where needed

📚 Documentation

Excellent:

  • .optimization-notes.md for QR payment latency
  • docs/CHANGELOG.md with detailed changes
  • Dev page README

Missing:

  1. No inline documentation for complex state machines (hold-to-claim)
  2. No API documentation for new points endpoints
  3. No migration guide for removed Points V1 fields

🎨 Code Quality Issues

  1. Magic Numbers:

    // In qr-pay/page.tsx
    const HOLD_DURATION = 1500 // Hardcoded

    Extract to a shared constants file.

  2. Repeated Logic: getShakeClass appears in two files (qr-pay/page.tsx and dev/shake-test/page.tsx). Extract to a shared utility.

  3. Empty File: src/components/Global/ErrorSuppressor.tsx is empty. Remove or implement.

🔄 Breaking Changes

Interface Removals in src/interfaces/interfaces.ts:

  • Removed points, referrerAddress, referredUsersPoints from Account
  • Removed points, transactions, totalReferralConnections, etc. from IUserProfile

Impact: Any code using these fields will break. Ensure:

  1. Backend removed these fields
  2. Frontend migration is complete
  3. No third-party integrations rely on these

📋 Recommendations Priority

High Priority:

  1. Fix (entry as any).points type assertion
  2. Add keyboard support for hold-to-claim
  3. Add tests for critical paths
  4. Centralize JWT token handling
  5. Fix timer cleanup dependencies in qr-pay

Medium Priority:

  1. Extract shared shake logic
  2. Add haptic feedback feature detection
  3. Document removed Points V1 fields migration
  4. Consider lazy-loading canvas-confetti

Nice to Have:

  1. Add inline docs for complex state machines
  2. Extract magic numbers to constants
  3. Consider a points calculation cache strategy

🎉 Overall Assessment

This is a well-structured feature with thoughtful optimizations and good UX patterns. The main concerns are around type safety, error handling consistency, and test coverage. The architectural decisions are sound, and the performance optimizations are impressive.

Estimated Complexity: High
Risk Level: Medium (due to breaking changes and optimistic UI patterns)
Recommendation: Address high-priority items before merge, especially type safety and accessibility concerns.

Would you like me to dive deeper into any specific area?

@Hugo0
Copy link
Contributor Author

Hugo0 commented Oct 14, 2025

@BugBot review in depth

@Hugo0
Copy link
Contributor Author

Hugo0 commented Oct 14, 2025

@coderabbitai fixed, review in depth again please

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 14, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# Check the transactionTransformer.ts for the type assertion issue
echo "=== Checking transactionTransformer.ts for type safety fixes ==="
cat src/components/TransactionDetails/transactionTransformer.ts | head -100

echo -e "\n=== Checking qr-pay page for timer cleanup and keyboard support ==="
# Look at the useEffect with timer cleanup and hold-to-claim button
cat src/app/\(mobile-ui\)/qr-pay/page.tsx | grep -A 20 "useEffect.*holdTimer\|progressInterval" | head -30

echo -e "\n=== Check for keyboard support on hold button ==="
cat src/app/\(mobile-ui\)/qr-pay/page.tsx | grep -A 10 -B 2 "onPointerDown.*startHold"

echo -e "\n=== Look for new test files ==="
fd -e test.tsx -e test.ts -e spec.tsx -e spec.ts

echo -e "\n=== Check points.ts for JWT handling ==="
cat src/services/points.ts | head -80

echo -e "\n=== Check invites.ts for JWT handling ==="
cat src/services/invites.ts | head -60

Length of output: 10976


🏁 Script executed:

#!/bin/bash

echo "=== Check complete timer cleanup effect in qr-pay ==="
cat src/app/\(mobile-ui\)/qr-pay/page.tsx | grep -A 10 "// Cleanup timers on unmount"

echo -e "\n=== Check if type assertion was fixed in transactionTransformer ==="
cat src/components/TransactionDetails/transactionTransformer.ts | grep -B 5 -A 5 "points"

echo -e "\n=== Check if shake logic was extracted to shared utility ==="
fd -t f shake | grep -v node_modules | grep -v ".git"
rg -l "getShakeClass" --type ts --type tsx

echo -e "\n=== Check for canvas-confetti lazy loading ==="
rg "import.*canvas-confetti" --type ts --type tsx

echo -e "\n=== Check for any new points/perks test files ==="
rg -l "points|perk" -g "*test.ts*" -g "*spec.ts*"

echo -e "\n=== Verify haptic feedback feature detection ==="
cat src/app/\(mobile-ui\)/dev/shake-test/page.tsx | grep -B 2 -A 2 "navigator.vibrate"

Length of output: 2914


🏁 Script executed:

#!/bin/bash

echo "=== Check if getShakeClass exists in both files ==="
grep -n "getShakeClass" src/app/\(mobile-ui\)/qr-pay/page.tsx 2>/dev/null | head -5
grep -n "getShakeClass" src/app/\(mobile-ui\)/dev/shake-test/page.tsx 2>/dev/null | head -5

echo -e "\n=== Check for shake utilities file ==="
fd shake -e ts -e tsx | grep -i util

echo -e "\n=== Check canvas-confetti imports ==="
grep -r "canvas-confetti" src/ --include="*.ts" --include="*.tsx" | head -10

echo -e "\n=== Check ErrorSuppressor implementation ==="
cat src/components/Global/ErrorSuppressor.tsx

echo -e "\n=== Check if magic numbers were extracted ==="
grep -n "HOLD_DURATION\|1500" src/app/\(mobile-ui\)/qr-pay/page.tsx | head -5

echo -e "\n=== Verify all haptic vibrate calls have feature detection ==="
grep -n "navigator.vibrate" src/app/\(mobile-ui\)/qr-pay/page.tsx | head -10

Length of output: 1521


🏁 Script executed:

#!/bin/bash

echo "=== Verify perk.utils file exists and content ==="
cat src/utils/perk.utils.ts

echo -e "\n=== Check constants file for PERK_HOLD_DURATION_MS ==="
grep -n "PERK_HOLD_DURATION" src/constants/index.ts

echo -e "\n=== Double check invites error handling ==="
cat src/services/invites.ts | grep -A 15 "getInvites"

echo -e "\n=== Check qr-pay haptic calls context ==="
grep -B 3 -A 1 "navigator.vibrate" src/app/\(mobile-ui\)/qr-pay/page.tsx | head -30

Length of output: 2712


🏁 Script executed:

#!/bin/bash

echo "=== Find PERK_HOLD_DURATION_MS definition ==="
rg "PERK_HOLD_DURATION_MS" src/

echo -e "\n=== Check if there are any test files for the new features ==="
ls -la src/app/\(mobile-ui\)/points/*.test.* 2>/dev/null || echo "No test files in points directory"
ls -la src/services/*.test.* 2>/dev/null || echo "No test files in services directory"
ls -la src/utils/*.test.* 2>/dev/null || echo "Check what test files exist in utils"

echo -e "\n=== Summary: Check ErrorSuppressor status ==="
wc -l src/components/Global/ErrorSuppressor.tsx

Length of output: 1053


@Hugo0, excellent work on addressing most of the high-priority issues! Here's my comprehensive follow-up review:

✅ Fixed Issues (Well Done!)

1. Type Safety

  • The (entry as any).points type assertion is gone
  • Properly typed as points?: number in the interface

2. Timer Cleanup

  • Perfect implementation using refs with empty dependency array:
useEffect(() => {
    return () => {
        if (holdTimerRef.current) clearTimeout(holdTimerRef.current)
        if (progressIntervalRef.current) clearInterval(progressIntervalRef.current)
    }
}, [])

3. Keyboard Accessibility

  • Added proper keyboard support for hold-to-claim:
onKeyDown={(e) => {
    if ((e.key === 'Enter' || e.key === ' ') && !isClaimingPerk) {
        e.preventDefault()
        startHold()
    }
}}

4. Code Reusability

  • Extracted shared shake logic to src/utils/perk.utils.ts
  • Clean utility with proper TypeScript types

5. Magic Numbers

  • PERK_HOLD_DURATION_MS properly extracted to constants

6. JWT Handling ✓ (partially)

  • points.ts now returns success wrappers instead of throwing

7. Haptic Feedback ✓ (in qr-pay)

  • Added feature detection: if ('vibrate' in navigator)

⚠️ Remaining Issues

High Priority

1. Test Coverage
No test files added for the new features. Critical paths that need testing:

  • Points calculation logic
  • Perk claiming flow (especially optimistic updates)
  • Hold-to-claim state machine
  • API error scenarios

Recommendation: At minimum, add tests for:

// src/services/__tests__/points.test.ts
describe('pointsApi', () => {
    it('should handle missing JWT gracefully', ...)
    it('should calculate points correctly', ...)
})

// src/utils/__tests__/perk.utils.test.ts
describe('getShakeClass', () => {
    it('should return correct class for each intensity', ...)
})

2. Error Handling Inconsistency ⚠️
invites.ts still throws errors while points.ts returns success wrappers:

// invites.ts (inconsistent)
if (!jwtToken) {
    throw new Error('No JWT token found')
}

// points.ts (consistent)
if (!jwtToken) {
    return { success: false, data: null }
}

Recommendation: Make invites.ts consistent:

getInvites: async (): Promise<{ success: boolean; data: PointsInvitesResponse | null }> => {
    try {
        const jwtToken = Cookies.get('jwt-token')
        if (!jwtToken) {
            console.error('getInvites: No JWT token found')
            return { success: false, data: null }
        }
        // ... rest of logic
        return { success: true, data: invitesRes }
    } catch (error) {
        console.error('getInvites: Unexpected error', error)
        return { success: false, data: null }
    }
}

Medium Priority

3. Empty File 🤔
src/components/Global/ErrorSuppressor.tsx is empty (0 lines). Either:

  • Implement it, or
  • Remove it from the codebase

4. Canvas-Confetti Bundle Size
Still eagerly imported in confetti.ts. Consider lazy loading:

export async function shootStarConfetti(options?: ConfettiOptions) {
    if (typeof window === 'undefined') return
    
    const confetti = (await import('canvas-confetti')).default
    // ... rest of logic
}

5. Haptic Feedback Consistency
dev/shake-test/page.tsx has vibration calls without feature detection on lines where intensity changes. Should wrap all calls:

if ('vibrate' in navigator) {
    navigator.vibrate(50)
}

📊 Progress Summary

Fixed: 7/10 high-priority items (70%)
Partially Fixed: 2/10 items
Not Fixed: 3/10 items

Overall Risk Assessment: Medium → Low-Medium (improved!)

The critical type safety, accessibility, and state management issues are resolved. Main remaining concerns are test coverage and error handling consistency.

🎯 Final Recommendations

Before Merge (Required):

  1. Make invites.ts error handling consistent with points.ts
  2. Add basic test coverage for points calculation and perk claiming

After Merge (Follow-up):

  1. Implement or remove ErrorSuppressor.tsx
  2. Consider lazy-loading canvas-confetti
  3. Add comprehensive test suite
  4. Document Points V1 → V2 migration

Great progress overall! The codebase is much more robust now. Would you like me to help draft test cases for the points/perks features?


🧠 Learnings used
Learnt from: Hugo0
PR: peanutprotocol/peanut-ui#594
File: src/components/Request/Pay/Views/Initial.view.tsx:188-213
Timestamp: 2024-12-31T15:08:15.640Z
Learning: Always resolve coderabbit comments when reviewing code, since they can contain useful suggestions.

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

♻️ Duplicate comments (1)
src/app/(mobile-ui)/qr-pay/page.tsx (1)

541-553: Functional setState to avoid stale closure (already flagged).

Use updater form to merge perk data using the latest state. This was noted previously and still applies.

-                setQrPayment({
-                    ...qrPayment,
+                setQrPayment((prev) => ({
+                    ...prev,
                     perk: {
                         eligible: true,
                         discountPercentage: result.perk.discountPercentage,
                         claimed: true,
                         amountSponsored: result.perk.amountSponsored,
                         txHash: result.perk.txHash,
                     },
-                })
+                }))
🧹 Nitpick comments (8)
src/utils/perk.utils.ts (1)

13-27: Simplify with a map and enforce exhaustiveness.

Cleaner and type‑safer via a lookup. Also treats 'none' explicitly.

-export function getShakeClass(isShaking: boolean, shakeIntensity: ShakeIntensity): string {
-    if (!isShaking) return ''
-    switch (shakeIntensity) {
-        case 'weak':
-            return 'perk-shake-weak'
-        case 'medium':
-            return 'perk-shake-medium'
-        case 'strong':
-            return 'perk-shake-strong'
-        case 'intense':
-            return 'perk-shake-intense'
-        default:
-            return ''
-    }
-}
+const SHAKE_CLASS: Record<Exclude<ShakeIntensity, 'none'>, string> = {
+    weak: 'perk-shake-weak',
+    medium: 'perk-shake-medium',
+    strong: 'perk-shake-strong',
+    intense: 'perk-shake-intense',
+}
+
+export function getShakeClass(isShaking: boolean, shakeIntensity: ShakeIntensity): string {
+    if (!isShaking || shakeIntensity === 'none') return ''
+    return SHAKE_CLASS[shakeIntensity] ?? ''
+}
src/constants/general.consts.ts (1)

210-212: Durations and intervals: consider centralizing related values.

You use 1500ms and 50ms step in multiple places. Export both to avoid drift and magic numbers.

 // Perk claim UI constants
-export const PERK_HOLD_DURATION_MS = 1500 // 1.5 seconds hold duration for claiming perks
+export const PERK_HOLD_DURATION_MS = 1_500 // 1.5 seconds hold duration for claiming perks
+export const PERK_PROGRESS_INTERVAL_MS = 50 // progress update cadence (~20 FPS)
src/app/(mobile-ui)/dev/shake-test/page.tsx (3)

15-17: Use refs with environment-safe timer types; avoid state for timers.

Avoid re-renders and Node/browser type mismatch by using refs with ReturnType<typeof setTimeout>.

-    const [holdTimer, setHoldTimer] = useState<NodeJS.Timeout | null>(null)
-    const [progressInterval, setProgressInterval] = useState<NodeJS.Timeout | null>(null)
+    const holdTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
+    const progressIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null)

19-73: Add re‑entrancy guard to prevent duplicate timers.

If pointerdown fires twice, multiple timers can run. Bail if a timer exists.

-    const startHold = useCallback(() => {
+    const startHold = useCallback(() => {
+        if (holdTimerRef.current || progressIntervalRef.current) return
         setHoldProgress(0)
         setIsShaking(true)
         setShowSuccess(false)
@@
-        const interval = setInterval(() => {
+        const interval = setInterval(() => {
             // ...
         }, 50)
-
-        setProgressInterval(interval)
+        progressIntervalRef.current = interval
@@
-        const timer = setTimeout(() => {
+        const timer = setTimeout(() => {
             // ...
         }, PERK_HOLD_DURATION_MS)
-
-        setHoldTimer(timer)
+        holdTimerRef.current = timer
-    }, [])
+    }, [])

96-110: Cleanup timers on unmount.

Ensure no leaks if the component unmounts mid‑hold.

     const cancelHold = useCallback(() => {
-        if (holdTimer) clearTimeout(holdTimer)
-        if (progressInterval) clearInterval(progressInterval)
-        setHoldTimer(null)
-        setProgressInterval(null)
+        if (holdTimerRef.current) clearTimeout(holdTimerRef.current)
+        if (progressIntervalRef.current) clearInterval(progressIntervalRef.current)
+        holdTimerRef.current = null
+        progressIntervalRef.current = null
         // ...
-    }, [holdTimer, progressInterval])
+    }, [])
+
+    // On unmount: clear timers and stop vibrations
+    useEffect(() => {
+        return () => {
+            if (holdTimerRef.current) clearTimeout(holdTimerRef.current)
+            if (progressIntervalRef.current) clearInterval(progressIntervalRef.current)
+            if ('vibrate' in navigator) navigator.vibrate(0)
+        }
+    }, [])
src/app/(mobile-ui)/qr-pay/page.tsx (3)

334-341: Make the KYC comment resilient; remove fragile line numbers.

Line numbers drift. Refer to the shouldBlockPay gate semantically instead.

-    // 2. The actual payment action is blocked by shouldBlockPay (line 713 & 1109)
+    // 2. The actual payment action is blocked by shouldBlockPay (see gate below and button disabled condition)

565-625: Guard against re‑entry; don’t start multiple holds.

Avoid duplicate timers if the user re‑presses quickly or pointer events bounce. Also ignore if already claimed/claiming.

-    const startHold = useCallback(() => {
+    const startHold = useCallback(() => {
+        if (perkClaimed || isClaimingPerk) return
+        if (holdTimerRef.current || progressIntervalRef.current) return
         setHoldProgress(0)
         setIsShaking(true)
@@
-        const interval = setInterval(() => {
+        const interval = setInterval(() => {
             // ...
-        }, 50)
+        }, 50)
         progressIntervalRef.current = interval
@@
-        const timer = setTimeout(() => {
+        const timer = setTimeout(() => {
             claimPerk()
         }, PERK_HOLD_DURATION_MS)
         holdTimerRef.current = timer
-    }, [claimPerk])
+    }, [claimPerk, perkClaimed, isClaimingPerk])

803-866: A11y: good keyboard support; consider reduced-motion users.

Nice keyboard handling. Consider disabling shake/confetti if user prefers reduced motion.

Example check:

const prefersReducedMotion = typeof window !== 'undefined' &&
  window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
// Then skip shake/vibrate/confetti when true.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a9beb0 and 97f62f6.

📒 Files selected for processing (6)
  • src/app/(mobile-ui)/dev/shake-test/page.tsx (1 hunks)
  • src/app/(mobile-ui)/qr-pay/page.tsx (10 hunks)
  • src/components/TransactionDetails/transactionTransformer.ts (4 hunks)
  • src/constants/general.consts.ts (1 hunks)
  • src/utils/history.utils.ts (1 hunks)
  • src/utils/perk.utils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/TransactionDetails/transactionTransformer.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/app/(mobile-ui)/qr-pay/page.tsx (5)
src/utils/perk.utils.ts (2)
  • ShakeIntensity (5-5)
  • getShakeClass (13-27)
src/components/Global/DirectSendQR/utils.ts (1)
  • isPaymentProcessorQR (131-138)
src/services/manteca.ts (1)
  • mantecaApi (102-290)
src/utils/confetti.ts (1)
  • shootDoubleStarConfetti (49-67)
src/constants/general.consts.ts (1)
  • PERK_HOLD_DURATION_MS (212-212)
src/app/(mobile-ui)/dev/shake-test/page.tsx (4)
src/utils/perk.utils.ts (2)
  • ShakeIntensity (5-5)
  • getShakeClass (13-27)
src/constants/general.consts.ts (1)
  • PERK_HOLD_DURATION_MS (212-212)
src/utils/confetti.ts (1)
  • shootDoubleStarConfetti (49-67)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (1)
src/utils/history.utils.ts (1)

129-131: completeHistoryEntry preserves points; verify backend returns it. Spreading entry retains the new field.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Hugo Montenegro <h@hugomontenegro.com>
}, [kycGateState, simpleFiPayment, simpleFiQrData, paymentProcessor, setLoadingState])

// fetch payment lock only when gating allows proceeding and we don't yet have a lock (Manteca)
// Fetch Manteca payment lock immediately on QR scan (Manteca only)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

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

♻️ Duplicate comments (3)
src/app/(mobile-ui)/points/page.tsx (3)

64-66: Indefinite loading persists when tierInfo.success is false.

This issue was previously raised but remains unfixed. When tierInfo returns { success: false, data: null }, the loading spinner displays indefinitely because !tierInfo?.data is true even though loading has completed.

Apply this diff to fix the indefinite loading:

-    if (isLoading || isTierInfoLoading || !tierInfo?.data) {
+    if (isLoading || isTierInfoLoading) {
         return <PeanutLoading />
     }

The error handling block below (lines 68-76) should catch missing data cases.


68-76: Handle tierInfo fetch failures and missing data as errors.

This issue was previously raised but remains partially unfixed. The error check only handles query errors (isInvitesError || isTierInfoError), but doesn't catch cases where getTierInfo returns { success: false, data: null } or when tierInfo.data is missing/invalid.

Apply this diff to handle all error cases:

-    if (isInvitesError || isTierInfoError) {
+    if (isInvitesError || isTierInfoError || !tierInfo?.data || tierInfo?.success === false) {
         console.error('Error loading points data:', invitesError ?? tierInfoError)
 
         return (

212-212: Handle undefined invitees array in empty state check.

This issue was previously raised but remains unfixed. The condition invites?.invitees?.length === 0 won't trigger when invitees is undefined or null (evaluates to undefined === 0), preventing the empty state from displaying.

Apply this diff to handle both cases:

-                {invites?.invitees?.length === 0 && (
+                {(!invites?.invitees || invites.invitees.length === 0) && (
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ef0a79 and b1f6889.

📒 Files selected for processing (1)
  • src/app/(mobile-ui)/points/page.tsx (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-08T17:13:13.155Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1299
File: src/app/(mobile-ui)/points/page.tsx:41-51
Timestamp: 2025-10-08T17:13:13.155Z
Learning: In `src/app/(mobile-ui)/points/page.tsx`, the icon name "invite-heart" is intentionally used (not "inviter-heart") when displaying who invited the current user, as this is a deliberate design choice despite semantic differences with UserHeader usage.

Applied to files:

  • src/app/(mobile-ui)/points/page.tsx
🧬 Code graph analysis (1)
src/app/(mobile-ui)/points/page.tsx (9)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/services/invites.ts (1)
  • invitesApi (7-89)
src/services/points.ts (1)
  • pointsApi (6-86)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/components/Global/EmptyStates/EmptyState.tsx (1)
  • EmptyState (13-28)
src/utils/general.utils.ts (2)
  • generateInvitesShareText (1332-1334)
  • getInitialsFromName (1272-1279)
src/services/services.types.ts (1)
  • PointsInvite (421-433)
src/components/Global/Card/index.tsx (1)
  • getCardPosition (14-19)
src/components/UserHeader/index.tsx (1)
  • VerifiedUserLabel (45-109)
🪛 Biome (2.1.2)
src/app/(mobile-ui)/points/page.tsx

[error] 102-102: Did you mean to use a :? An = can only follow a property name when the containing object literal is part of a destructuring pattern.

(parse)


[error] 102-102: Expected a property, a shorthand property, a getter, a setter, or a method but instead found '{'.

Expected a property, a shorthand property, a getter, a setter, or a method here.

(parse)


[error] 103-103: expected , but instead found width

Remove width

(parse)


[error] 113-113: expected } but instead found <

Remove <

(parse)


[error] 90-90: Expected corresponding JSX closing tag for 'div'.

Opening tag

closing tag

(parse)


[error] 83-83: Expected corresponding JSX closing tag for 'Card'.

Opening tag

closing tag

(parse)


[error] 82-82: Expected corresponding JSX closing tag for 'section'.

Opening tag

closing tag

(parse)

🪛 GitHub Actions: Tests
src/app/(mobile-ui)/points/page.tsx

[error] 102-105: Prettier check failed due to a syntax error: Property assignment expected. Command: 'pnpm prettier --check .'

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview

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

♻️ Duplicate comments (2)
src/app/(mobile-ui)/points/page.tsx (2)

64-76: Fix unreachable error handling causing indefinite loading.

The current logic prevents error handling from executing when the API returns { success: false, data: null }:

  1. Lines 64-66: Loading check includes !tierInfo?.data, which is true even after API errors
  2. Lines 68-76: Error handling never executes because the component returns early at line 65

When getTierInfo fails, isLoading and isTierInfoLoading become false, but tierInfo?.data remains null, causing the loader to display indefinitely instead of showing the error state.

Apply this diff to fix the logic:

-    if (isLoading || isTierInfoLoading || !tierInfo?.data) {
+    if (isLoading || isTierInfoLoading) {
         return <PeanutLoading />
     }

-    if (isInvitesError || isTierInfoError) {
+    if (isInvitesError || isTierInfoError || !tierInfo?.data || tierInfo?.success === false) {
         console.error('Error loading points data:', invitesError ?? tierInfoError)

         return (

214-214: Handle undefined invitees array.

The condition invites?.invitees?.length === 0 won't show the empty state if invitees is undefined or null (expression evaluates to undefined === 0 which is false). While the API should always return an array, defensive handling prevents edge cases.

Apply this diff to handle both cases:

-                {invites?.invitees?.length === 0 && (
+                {(!invites?.invitees || invites.invitees.length === 0) && (
🧹 Nitpick comments (1)
src/app/(mobile-ui)/points/page.tsx (1)

177-177: Extract magic number 0.2 to a named constant.

The 20% referral rate is hardcoded. If this rate changes, the value must be updated in multiple locations (also referenced in the text at line 138).

Consider extracting to a constant:

+const REFERRAL_POINTS_PERCENTAGE = 0.2
+
 const PointsPage = () => {
     // ... existing code ...
 
-    const pointsEarned = Math.floor(invite.totalPoints * 0.2)
+    const pointsEarned = Math.floor(invite.totalPoints * REFERRAL_POINTS_PERCENTAGE)

Alternatively, if invite.contributedPoints from the API represents the actual points earned from this invitee, consider using that field directly instead of calculating client-side.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b1f6889 and 02d0e4b.

📒 Files selected for processing (1)
  • src/app/(mobile-ui)/points/page.tsx (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-08T17:13:13.155Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1299
File: src/app/(mobile-ui)/points/page.tsx:41-51
Timestamp: 2025-10-08T17:13:13.155Z
Learning: In `src/app/(mobile-ui)/points/page.tsx`, the icon name "invite-heart" is intentionally used (not "inviter-heart") when displaying who invited the current user, as this is a deliberate design choice despite semantic differences with UserHeader usage.

Applied to files:

  • src/app/(mobile-ui)/points/page.tsx
🧬 Code graph analysis (1)
src/app/(mobile-ui)/points/page.tsx (9)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/services/invites.ts (1)
  • invitesApi (7-89)
src/services/points.ts (1)
  • pointsApi (6-86)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/components/Global/EmptyStates/EmptyState.tsx (1)
  • EmptyState (13-28)
src/utils/general.utils.ts (2)
  • generateInvitesShareText (1332-1334)
  • getInitialsFromName (1272-1279)
src/services/services.types.ts (1)
  • PointsInvite (421-433)
src/components/Global/Card/index.tsx (1)
  • getCardPosition (14-19)
src/components/UserHeader/index.tsx (1)
  • VerifiedUserLabel (45-109)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (1)
src/app/(mobile-ui)/points/page.tsx (1)

101-113: LGTM! Progress bar calculation properly guards edge cases.

The progress width calculation correctly handles:

  • Divide-by-zero: checks nextTierThreshold > 0 before division
  • Overflow: clamps ratio to max 1 with Math.min
  • Visual progression: applies 0.6 power for smooth UX curve

The duplicate style={{ syntax error mentioned in previous reviews has been resolved.

Copy link
Contributor

@jjramirezn jjramirezn left a comment

Choose a reason for hiding this comment

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

Great PR, some comments, one blocking issue but still, approving please solve
the blocking before merging


## Random

- never open SVG files, it crashes you. Only read jpeg, png, gif, or webp.
Copy link
Contributor

Choose a reason for hiding this comment

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

<3

Copy link
Contributor Author

Choose a reason for hiding this comment

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

!

@@ -1,5 +1,15 @@
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
// Polyfill crypto.randomUUID for Node.js SSR (required by DaimoPayProvider)
Copy link
Contributor

Choose a reason for hiding this comment

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

question: what does this have to do with points and perks?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i was getting errors, this solved it

no idea why this isnt an issue in other branches as well

@@ -0,0 +1 @@

No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

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

question: what is this file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

supposed to be for dev stuff. useless rn tbh

<Card className="space-y-2 bg-blue-50 p-4">
<h3 className="font-bold text-blue-900">ℹ️ Info</h3>
<ul className="space-y-1 text-sm text-blue-800">
<li>• These tools are publicly accessible (no login required)</li>
Copy link
Contributor

Choose a reason for hiding this comment

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

thought: we have to be careful what we put in here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes!

const { isLoading, loadingState, setLoadingState } = useContext(loadingStateContext)
const { shouldBlockPay, kycGateState } = useQrKycGate()
const queryClient = useQueryClient()
const [isShaking, setIsShaking] = useState(false)
Copy link
Contributor

Choose a reason for hiding this comment

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

praise: love all the shaking functionality

this is perfect for a component (not saying we should do it now)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

<3

Comment on lines +14 to 26
// Check if orientation API is available
if (!screen.orientation || !(screen.orientation as any).lock) {
return
}

try {
await (screen.orientation as any).lock('portrait-primary')
} catch (error) {
// Only log to console, don't report to Sentry as this is expected behavior
// on desktop browsers and environments that don't support orientation lock
console.debug('Screen orientation lock not available on this device/browser:', error)
// Don't capture this as it's expected behavior in many environments
}
Copy link
Contributor

Choose a reason for hiding this comment

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

praise: PRAISE PRAISE PRAISE 1 MILLION PEANUT POINTS

Copy link
Contributor Author

Choose a reason for hiding this comment

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

<3

@Hugo0 Hugo0 merged commit b007bb0 into peanut-wallet-dev Oct 14, 2025
3 of 4 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Dec 11, 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.

3 participants