Conversation
…w for better cache management
[TASK-15066] feat: implement UI changes for Points
…peanut-ui into feat/points-tier
…ality. Added tier badge display and improved progress bar in PointsPage. Implemented perk claiming feature in QRPayPage with associated animations and UI updates. Updated transaction details to reflect perk information. Adjusted styles for new animations and added explicit imports for assets.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (2 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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 analyticssrc/components/Global/ErrorSuppressor.tsx (1)
1-1: Remove unused ErrorSuppressor componentThe file is empty and has no imports or usages—delete
src/components/Global/ErrorSuppressor.tsxto 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
fetchWithSentryfor 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
qrPaymentIdparameter (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 shakeAdd 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 confettiSkip 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 loadConsider 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 checksUse 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've earned {points} {points === 1 ? 'point' : 'points'}! + You've earned {Number(points).toLocaleString()} {points === 1 ? 'point' : 'points'}! </p> </div> )}
25-29: Import consistency for STAR_STRAIGHT_ICONOther 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 cachingusdAmount 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 claimUse 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 effectPrevent 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_ICONElsewhere 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.contributedPointsAlso 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
⛔ Files ignored due to path filters (5)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlsrc/assets/badges/tier0.svgis excluded by!**/*.svgsrc/assets/badges/tier1.svgis excluded by!**/*.svgsrc/assets/badges/tier2.svgis excluded by!**/*.svgsrc/assets/badges/tier3.svgis 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.tsxsrc/app/[...recipient]/client.tsxsrc/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.tsxsrc/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 likemax-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-xlto!text-lgshould align with the design system. Thedark:text-blackclass 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.mdis 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-confettiand 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
windowandscreenAPI 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.lockavailability before use prevents errors in browsers that don't support this API.
19-26: LGTM: Appropriate error handling for orientation lock.Switching from
captureExceptiontoconsole.debugis 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
perkfield 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 DrawerTitleScreen-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 integrationAdding '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 shapeRe-exports look fine. Ensure your build config supports
defaultexports for.svg(URL vs ReactComponent) consistently across the app; add a*.svgmodule declaration if needed.If not present, consider a
global.d.tsentry like:
declare module '*.svg' { const src: string; export default src }src/components/TransactionDetails/TransactionDetailsReceipt.tsx (2)
156-158: Points row LGTM; ensure type coverageThe 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 fetchExplicit 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 alignmentGood to cast perk as HistoryEntryPerk. Ensure TransactionDetails.extraDataForDrawer?.perk is typed accordingly.
src/services/points.ts (1)
15-21: Authorization header properly redacted ThesanitizeHeadersutility in src/utils/sentry.utils.ts masks anyauthorizationheader value as[REDACTED].
| 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> | ||
| ) | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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, | ||
| }) |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
🛠️ 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?: numberextraData?: { ...; 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' -C3Length 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.
| 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… | |
| } |
| 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.
| } catch (e) { | ||
| console.log(e) | ||
| throw new Error('Failed to fetch invites') |
There was a problem hiding this comment.
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.
| } 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.
| const jwtToken = Cookies.get('jwt-token') | ||
| if (!jwtToken) { | ||
| console.error('getTierInfo: No JWT token found') | ||
| return { success: false, data: null } | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| if (!response.ok) { | ||
| console.error('getTierInfo: API request failed', response.status, response.statusText) | ||
| return { success: false, data: null } | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| } catch (error) { | ||
| console.error('getTierInfo: Unexpected error', error) | ||
| return { success: false, data: null } | ||
| } | ||
| }, |
There was a problem hiding this comment.
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.
| } 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.
There was a problem hiding this comment.
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?.datacan 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
nextTierThresholdis 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. Sincetransaction.pointsbeing truthy already implies it's non-zero for numbers, the explicit> 0check 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 > 0contains redundant checks. Ifinvites?.inviteesexists, theninvitesmust 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
📒 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.
Imagefrom Next.js provides optimized rendering, andSTAR_STRAIGHT_ICONis 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.okcheck 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.pointsandtransaction.points, matching the pattern used for other rows liketxIdat line 462)- Proper use of
PaymentInfoRowcomponent- 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_BADGEhandles unexpected tier values safely.
34-54: LGTM: Well-structured queries.Both queries are properly gated by
userIdand 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-hearticon 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
fullNametousernameat Line 178- Proper use of
VerifiedUserLabelcomponent- Good UX with clickable cards navigating to user profiles
| )} | ||
| {invites?.length === 0 && ( | ||
|
|
||
| {invites?.invitees?.length === 0 && ( |
There was a problem hiding this comment.
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.
| {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.
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (8)
instrumentation.ts (1)
6-7: Prefer@ts-expect-errorover@ts-ignore.Using
@ts-expect-erroris safer than@ts-ignorebecause 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.webcryptosrc/utils/crypto-polyfill.ts (1)
9-9: Prefer@ts-expect-errorover@ts-ignore.Using
@ts-expect-erroris safer than@ts-ignorebecause 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
📒 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.randomUUIDis 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.randomUUIDis 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
valueprop type to includeundefinedaligns with React conventions whereundefinedis 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
invitedByfield 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 thattotalPointsis mapped fromstats.pointsV2.totalPoints.
- Confirm the backend/API populates this field for backward compatibility.
- Add a deprecation plan to remove
totalPointsin 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
cancelHoldfunction 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.
src/app/(mobile-ui)/qr-pay/page.tsx
Outdated
| // Fetch payment lock immediately on load (parallel with KYC check for faster UX) | ||
| // KYC blocking still happens via shouldBlockPay check at line 310 |
There was a problem hiding this comment.
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.
| // 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.
| setQrPayment({ | ||
| ...qrPayment, | ||
| perk: { | ||
| eligible: true, | ||
| discountPercentage: result.perk.discountPercentage, | ||
| claimed: true, | ||
| amountSponsored: result.perk.amountSponsored, | ||
| txHash: result.perk.txHash, | ||
| }, | ||
| }) |
There was a problem hiding this comment.
🛠️ 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.
| 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}> |
There was a problem hiding this comment.
🧩 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
undefinedon 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.
| peanutFee: !!(transaction.extraDataForDrawer?.perk?.claimed && transaction.status !== 'pending'), | ||
| points: !!(transaction.points && transaction.points > 0), | ||
| comment: !!transaction.memo?.trim(), |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
🧩 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 itIUserProfile.stats.pointsV2?) - Whether
statsis 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' -C2Length of output: 1796
Define missing stats property and pointsV2 types
IUserProfilecomments referencestats.pointsV2, but nostatsproperty orStatsinterface exists.- Add a
stats: Statsfield (and/or onAccountif 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.
| // OLD Points V1 fields removed - use pointsV2 in stats instead | ||
| // Points V2: Use stats.pointsV2.totalPoints, pointsV2.inviteCount, etc. |
There was a problem hiding this comment.
🧩 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' -C3Length 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).
There was a problem hiding this comment.
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
📒 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
/devroutes in non-production environments (development, staging, test). Verify this aligns with your intended behavior—currently,/devpages will be accessible in all non-production environments.
There was a problem hiding this comment.
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
📒 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.
| - Removed Points V1 legacy fields from `Account` and `IUserProfile` interfaces | ||
|
|
||
| ### Fixed | ||
| - BigInt type handling in points balance calculations (backend) |
There was a problem hiding this comment.
🛠️ 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.
|
@cursor review in depth |
|
@coderabbitai review in depth |
|
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 & DesignStrengths:
Concerns:
🔒 SecurityCritical Issues:
⚡ PerformanceGood:
Concerns:
🎯 State ManagementIssues in
🧪 Testing GapsBased on the summaries, I don't see test files added for:
Recommendation: Add tests for critical paths, especially the payment flow with perks. ♿ AccessibilityGood additions:
Missing:
🐛 Potential Bugs
📚 DocumentationExcellent:
Missing:
🎨 Code Quality Issues
🔄 Breaking ChangesInterface Removals in
Impact: Any code using these fields will break. Ensure:
📋 Recommendations PriorityHigh Priority:
Medium Priority:
Nice to Have:
🎉 Overall AssessmentThis 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 Would you like me to dive deeper into any specific area? |
|
@BugBot review in depth |
|
@coderabbitai fixed, review in depth again please |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 10976 🏁 Script executed: Length of output: 2914 🏁 Script executed: Length of output: 1521 🏁 Script executed: Length of output: 2712 🏁 Script executed: 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 ✓
2. Timer Cleanup ✓
useEffect(() => {
return () => {
if (holdTimerRef.current) clearTimeout(holdTimerRef.current)
if (progressIntervalRef.current) clearInterval(progressIntervalRef.current)
}
}, [])3. Keyboard Accessibility ✓
onKeyDown={(e) => {
if ((e.key === 'Enter' || e.key === ' ') && !isClaimingPerk) {
e.preventDefault()
startHold()
}
}}4. Code Reusability ✓
5. Magic Numbers ✓
6. JWT Handling ✓ (partially)
7. Haptic Feedback ✓ (in qr-pay)
|
There was a problem hiding this comment.
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
shouldBlockPaygate 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
📒 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 preservespoints; verify backend returns it. Spreadingentryretains 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) |
There was a problem hiding this comment.
@jjramirezn need input here - thoughts?
Also see this https://discord.com/channels/972435984954302464/1427711276909465630/1427711409797468170
There was a problem hiding this comment.
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
tierInforeturns{ success: false, data: null }, the loading spinner displays indefinitely because!tierInfo?.dataistrueeven 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 wheregetTierInforeturns{ success: false, data: null }or whentierInfo.datais 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 === 0won't trigger wheninviteesisundefinedornull(evaluates toundefined === 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
📒 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
There was a problem hiding this comment.
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 }:
- Lines 64-66: Loading check includes
!tierInfo?.data, which is true even after API errors- Lines 68-76: Error handling never executes because the component returns early at line 65
When
getTierInfofails,isLoadingandisTierInfoLoadingbecome false, buttierInfo?.dataremains 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 === 0won't show the empty state ifinviteesisundefinedornull(expression evaluates toundefined === 0which isfalse). 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.contributedPointsfrom 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
📒 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 > 0before 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.
jjramirezn
left a comment
There was a problem hiding this comment.
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. |
| @@ -1,5 +1,15 @@ | |||
| export async function register() { | |||
| if (process.env.NEXT_RUNTIME === 'nodejs') { | |||
| // Polyfill crypto.randomUUID for Node.js SSR (required by DaimoPayProvider) | |||
There was a problem hiding this comment.
question: what does this have to do with points and perks?
There was a problem hiding this comment.
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 |
|||
There was a problem hiding this comment.
question: what is this file?
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
thought: we have to be careful what we put in here.
| const { isLoading, loadingState, setLoadingState } = useContext(loadingStateContext) | ||
| const { shouldBlockPay, kycGateState } = useQrKycGate() | ||
| const queryClient = useQueryClient() | ||
| const [isShaking, setIsShaking] = useState(false) |
There was a problem hiding this comment.
praise: love all the shaking functionality
this is perfect for a component (not saying we should do it now)
| // 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 | ||
| } |
There was a problem hiding this comment.
praise: PRAISE PRAISE PRAISE 1 MILLION PEANUT POINTS
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