Skip to content

fix: fixathon bug squash#1776

Merged
kushagrasarathe merged 9 commits intodevfrom
fixathon/squash
Mar 20, 2026
Merged

fix: fixathon bug squash#1776
kushagrasarathe merged 9 commits intodevfrom
fixathon/squash

Conversation

@kushagrasarathe
Copy link
Contributor

@kushagrasarathe kushagrasarathe commented Mar 20, 2026

Summary

Bug squash PR — multiple independent fixes across the app.

Fixes

  • Guest invite on request links — non-logged-in users clicking "Join Peanut" on a request link (e.g. peanut.me/prado/5USDC) now get the sender's invite code and redirect to /invite instead of getting stuck on signup with no code. Same pattern as claim links.
  • External wallet CTA loading state — clicking "exchange or wallet" no longer shows a spinner on the "Join Peanut" button; it's disabled-only while the charge is being created.
  • "You're withdrawing" copy — changed to "you're sending" in the withdraw confirmation flow and action details card.
  • Refunded status UI — refunded transactions now show green status color and strikethrough on amounts, matching the design for cancelled/reversed states.
  • Slider thumb text — text label on the amount slider is now visible above the thumb during drag instead of being hidden behind it.
  • Claim perk button text — fixed text visibility on the QR pay "claim perk" button using a dual-layer clip-path for the black-to-white transition on hold. Also reduced icon size for narrow devices.
  • Support chat drawer — replaced vaul drawer with an always-mounted panel so the iframe no longer unmounts on close, eliminating the 4-5s reload delay. Added touch drag-to-dismiss on handle bar.

Enhancements

  • Exchange rate widget — added ARS/BRL to popular currencies and a swap toggle to flip the conversion direction.

Chores

  • Removed unused imports and classes across several files.

@vercel
Copy link

vercel bot commented Mar 20, 2026

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

Project Deployment Actions Updated (UTC)
peanut-wallet Ready Ready Preview, Comment Mar 20, 2026 3:06pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ecc986cf-566c-44fb-96d1-57ef30723d46

📥 Commits

Reviewing files that changed from the base of the PR and between 5bfd9e7 and cc230e5.

📒 Files selected for processing (2)
  • src/app/(mobile-ui)/qr-pay/page.tsx
  • src/components/TransactionDetails/TransactionCard.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/TransactionDetails/TransactionCard.tsx
  • src/app/(mobile-ui)/qr-pay/page.tsx

Walkthrough

Removed unused device hook; updated UI copy for withdrawal/send flows; adjusted refunded status styling and amount presentation; refined QR Pay visuals and hold-to-claim clipping; added swap state/button and URL-sync guards in ExchangeRateWidget; replaced SupportDrawer with a custom bottom sheet and drag/escape handling; expanded popular currencies; added invite flow wiring for payment CTAs.

Changes

Cohort / File(s) Summary
Home Page Cleanup
src/app/(mobile-ui)/home/page.tsx
Removed unused useDeviceType import and deviceType usage.
Withdraw / Peanut Action Copy
src/app/(mobile-ui)/withdraw/manteca/page.tsx, src/components/Global/PeanutActionDetailsCard/index.tsx
Changed label from “You’re withdrawing” to “You’re sending”; removed unused imports in manteca page.
Refund / Status Styling
src/components/Global/Badges/StatusBadge.tsx, src/components/Global/StatusPill/index.tsx, src/components/TransactionDetails/TransactionCard.tsx
Switched refunded styling to success palette and added conditional strikethrough/grey styles for refunded amounts.
Support Drawer → Bottom Sheet
src/components/Global/SupportDrawer/index.tsx
Replaced Drawer components with always-mounted custom bottom sheet: fixed backdrop/panel, Crisp readiness gating, drag-to-dismiss, Escape/backdrop close, iframe remains mounted.
Exchange Rate Swap
src/components/Global/ExchangeRateWidget/index.tsx
Added isSwapping state, swapCurrencies callback, skip-debounce ref, swap button, and unified showLoading to include swap activity; adjusted URL sync behavior.
QR Pay / Hold-to-Claim UI
src/app/(mobile-ui)/qr-pay/page.tsx
Adjusted Perk Success Banner spacing/icon sizing; replaced single claim label with layered labels clipped by holdProgress; moved paymentMethodName into useMemo.
Points Page Minor
src/app/(mobile-ui)/points/page.tsx
Removed text-primary-1 class from the suffix span returned by shortenPoints.
Slider Overlay
src/components/Slider/index.tsx
Replaced explicit positioning with absolute inset-0 z-10 pointer-events-none overlay for full-track coverage without intercepting pointer events.
Icons / Popular Currencies
src/components/Global/Icons/Icon.tsx, src/components/LandingPage/CurrencySelect.tsx
Removed unused CallSplitRounded import; added ARS and BRL to popularCurrencies.
Contribute Pot / Invite Flow
src/features/payments/flows/contribute-pot/components/RequestPotActionList.tsx, src/features/payments/flows/contribute-pot/views/ContributePotInputView.tsx, src/features/payments/shared/components/SendWithPeanutCta.tsx
Added optional recipientUsername and isExternalWalletLoading props; added external-wallet loading flag; when unauthenticated and recipientUsername present, dispatch invite code/type and redirect to /invite; wired CTA props and disabled state accordingly.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • jjramirezn
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'fix: fixathon bug squash' is vague and generic, using the term 'bug squash' without conveying what specific bugs or changes are addressed. Consider a more descriptive title that highlights the primary fix or use a format like 'fix: multiple bug fixes including guest invites, status colors, and support drawer' to better convey the scope.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is well-structured, detailed, and clearly relates to the changeset by explaining multiple independent fixes, enhancements, and chores with concrete examples.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fixathon/squash
📝 Coding Plan
  • Generate coding plan for human review comments

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/components/Global/SupportDrawer/index.tsx (3)

84-90: Redundant translate classes - inline style overrides them.

The translate-y-0 and translate-y-full classes in the className (line 85) are overridden by the inline transform style (line 88). Consider removing the redundant Tailwind classes to avoid confusion.

♻️ Suggested cleanup
                 className={`fixed inset-x-0 bottom-0 z-[999999] flex max-h-[85vh] flex-col rounded-t-[10px] border bg-background pt-4 ${
-                    isSupportModalOpen ? 'translate-y-0' : 'pointer-events-none translate-y-full'
+                    isSupportModalOpen ? '' : 'pointer-events-none'
                 }`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Global/SupportDrawer/index.tsx` around lines 84 - 90, Remove
the redundant Tailwind translate classes from the SupportDrawer container: the
className currently toggles 'translate-y-0' and 'translate-y-full' but those are
always overridden by the inline style using transform (which reads
isSupportModalOpen and dragOffset) and transition (isDragging). Update the JSX
for the element that builds className (the container using isSupportModalOpen,
isDragging, and dragOffset) to drop 'translate-y-0' and 'translate-y-full' so
the visual behavior is controlled solely by the inline transform/transition
logic.

18-22: Consider using state instead of ref for drag tracking.

Reading dragStartY.current during render (line 22) to compute isDragging is fragile because ref mutations don't trigger re-renders. While this mostly works due to setDragOffset causing re-renders during drag, there's a subtle issue: when handleTouchEnd sets dragOffset to 0 but it was already 0 (e.g., user tapped without dragging), no re-render occurs, leaving isDragging potentially stale for the next interaction.

♻️ Suggested refactor using state
-    const dragStartY = useRef<number | null>(null)
-    const [dragOffset, setDragOffset] = useState(0)
-    const isDragging = dragStartY.current !== null
+    const dragStartY = useRef<number | null>(null)
+    const [dragOffset, setDragOffset] = useState(0)
+    const [isDragging, setIsDragging] = useState(false)

     const handleTouchStart = useCallback((e: React.TouchEvent) => {
         dragStartY.current = e.touches[0].clientY
+        setIsDragging(true)
     }, [])

     // ... handleTouchMove unchanged ...

     const handleTouchEnd = useCallback(() => {
         if (dragOffset > DISMISS_THRESHOLD) {
             setIsSupportModalOpen(false)
         }
         dragStartY.current = null
         setDragOffset(0)
+        setIsDragging(false)
-    }, [dragOffset, setIsSupportModalOpen])
+    }, [dragOffset, setIsSupportModalOpen])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Global/SupportDrawer/index.tsx` around lines 18 - 22, The
current useRef dragStartY and derived isDragging (dragStartY.current !== null)
can become stale because ref changes don't trigger renders; replace dragStartY
ref with a stateful value (e.g., const [dragStartY, setDragStartY] =
useState<number | null>(null) or add an explicit [isDragging, setIsDragging])
and update the touch handlers (handleTouchStart, handleTouchMove,
handleTouchEnd) to call setDragStartY / setIsDragging and setDragOffset via
setDragOffset so component re-renders reliably; update any references to
panelRef, dragStartY, isDragging, setDragOffset, and handleTouchEnd to use the
new state and ensure you clear the state on touch end so isDragging accurately
reflects the UI.

79-91: Add focus management to the dialog for improved accessibility.

The component has proper ARIA attributes (role="dialog", aria-modal) and Escape key handling, but lacks focus management. For better accessibility, implement:

  1. Move focus into the dialog (or to the iframe) when it opens
  2. Trap focus within the dialog while open
  3. Restore focus to the trigger element when closed

This can be done with a library like focus-trap-react or react-focus-lock, or by implementing focus management directly in a useEffect hook.

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

In `@src/components/Global/SupportDrawer/index.tsx` around lines 79 - 91, Add
focus management around the Support drawer: when isSupportModalOpen becomes
true, save document.activeElement, programmatically focus the panel (panelRef)
or its iframe (iframeRef) and enable a focus trap while open (use
focus-trap-react or implement Tab/Shift+Tab key handling on panelRef). When
closing, remove the trap and restore focus to the previously focused trigger
element. Implement this in a useEffect that watches isSupportModalOpen and
cleans up on unmount; reference panelRef, iframeRef (or create one), and
isSupportModalOpen to locate where to wire the logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/Global/SupportDrawer/index.tsx`:
- Around line 43-55: The CRISP_READY listener (inside the useEffect with
handleMessage) can leave the loader showing forever if the message never
arrives; add a timeout fallback: create a timeout (e.g., 8–15s) inside the same
useEffect that sets a new state (e.g., crispReadyTimedOut or sets isCrispReady
to false/error) to dismiss the loader or show an error, clear that timeout when
handleMessage receives 'CRISP_READY' (and setIsCrispReady(true)), and also clear
the timeout in the effect cleanup; update the UI to check crispReadyTimedOut (or
the error state) so the loading overlay doesn’t remain indefinitely.

In `@src/components/TransactionDetails/TransactionCard.tsx`:
- Around line 209-216: The component uses inconsistent color token names: inside
the span that renders displayAmount (the className on the span that combines
'font-semibold' and a refunded condition using status === 'refunded'), replace
the token 'text-grey-1' with 'text-gray-1' so it matches the other usages (e.g.,
other lines using 'text-gray-1') and keeps naming consistent across
TransactionCard.

---

Nitpick comments:
In `@src/components/Global/SupportDrawer/index.tsx`:
- Around line 84-90: Remove the redundant Tailwind translate classes from the
SupportDrawer container: the className currently toggles 'translate-y-0' and
'translate-y-full' but those are always overridden by the inline style using
transform (which reads isSupportModalOpen and dragOffset) and transition
(isDragging). Update the JSX for the element that builds className (the
container using isSupportModalOpen, isDragging, and dragOffset) to drop
'translate-y-0' and 'translate-y-full' so the visual behavior is controlled
solely by the inline transform/transition logic.
- Around line 18-22: The current useRef dragStartY and derived isDragging
(dragStartY.current !== null) can become stale because ref changes don't trigger
renders; replace dragStartY ref with a stateful value (e.g., const [dragStartY,
setDragStartY] = useState<number | null>(null) or add an explicit [isDragging,
setIsDragging]) and update the touch handlers (handleTouchStart,
handleTouchMove, handleTouchEnd) to call setDragStartY / setIsDragging and
setDragOffset via setDragOffset so component re-renders reliably; update any
references to panelRef, dragStartY, isDragging, setDragOffset, and
handleTouchEnd to use the new state and ensure you clear the state on touch end
so isDragging accurately reflects the UI.
- Around line 79-91: Add focus management around the Support drawer: when
isSupportModalOpen becomes true, save document.activeElement, programmatically
focus the panel (panelRef) or its iframe (iframeRef) and enable a focus trap
while open (use focus-trap-react or implement Tab/Shift+Tab key handling on
panelRef). When closing, remove the trap and restore focus to the previously
focused trigger element. Implement this in a useEffect that watches
isSupportModalOpen and cleans up on unmount; reference panelRef, iframeRef (or
create one), and isSupportModalOpen to locate where to wire the logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ab00c3c0-fe86-405c-967c-0b890040860a

📥 Commits

Reviewing files that changed from the base of the PR and between 420c791 and 6d74335.

📒 Files selected for processing (13)
  • src/app/(mobile-ui)/home/page.tsx
  • src/app/(mobile-ui)/points/page.tsx
  • src/app/(mobile-ui)/qr-pay/page.tsx
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx
  • src/components/Global/Badges/StatusBadge.tsx
  • src/components/Global/ExchangeRateWidget/index.tsx
  • src/components/Global/Icons/Icon.tsx
  • src/components/Global/PeanutActionDetailsCard/index.tsx
  • src/components/Global/StatusPill/index.tsx
  • src/components/Global/SupportDrawer/index.tsx
  • src/components/LandingPage/CurrencySelect.tsx
  • src/components/Slider/index.tsx
  • src/components/TransactionDetails/TransactionCard.tsx
💤 Files with no reviewable changes (2)
  • src/app/(mobile-ui)/home/page.tsx
  • src/components/Global/Icons/Icon.tsx

@kushagrasarathe
Copy link
Contributor Author

@coderabbitai review in depth.

Evaluate:

  • risk of introducing new bugs and breaking existing functionality
  • performance impact and latency
  • code quality and DRY

Also, write a very concise testing guide for QA

@coderabbitai coderabbitai bot added the enhancement New feature or request label Mar 20, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

🧩 Analysis chain

Let me gather the key file contents to perform a thorough review.


🏁 Script executed:

#!/bin/bash
echo "=== ExchangeRateWidget ==="
cat src/components/Global/ExchangeRateWidget/index.tsx

echo ""
echo "=== SupportDrawer ==="
cat src/components/Global/SupportDrawer/index.tsx

echo ""
echo "=== qr-pay page ==="
cat src/app/\(mobile-ui\)/qr-pay/page.tsx

echo ""
echo "=== TransactionCard ==="
cat src/components/TransactionDetails/TransactionCard.tsx

echo ""
echo "=== Slider ==="
cat src/components/Slider/index.tsx

echo ""
echo "=== StatusBadge ==="
cat src/components/Global/Badges/StatusBadge.tsx

echo ""
echo "=== StatusPill ==="
cat src/components/Global/StatusPill/index.tsx

Length of output: 116084


@kushagrasarathe Here is the in-depth review:


🔴 Critical Bug

qr-pay/page.tsxuseMemo called inside a conditional block (Rules of Hooks violation)

Line ~862:

if (isProviderDisabled) {
    const paymentMethodName = useMemo(() => { ... }, []) // ❌ HOOK IN CONDITIONAL
    return ( ... )
}

This is a direct violation of React's Rules of Hooks. When isProviderDisabled is true, React sees an extra useMemo call in the hook call order compared to when it's false. This will throw "Rendered more hooks than during the previous render" in development and silently corrupt hook state in production.

Fix: Hoist paymentMethodName unconditionally, before any if block:

const paymentMethodName = useMemo(() => {
    if (!isProviderDisabled) return null
    if (paymentProcessor === 'MANTECA') {
        switch (qrType) {
            case EQrType.PIX: return 'PIX'
            case EQrType.MERCADO_PAGO: return 'Mercado Pago'
            default: return 'QR'
        }
    }
    return 'SimpleFi'
}, [isProviderDisabled, paymentProcessor, qrType])

// later:
if (isProviderDisabled) {
    return ( /* use paymentMethodName */ )
}

🟠 Potential Bugs / Risk Areas

ExchangeRateWidget — Swap race condition with isSwapping

Lines ~96–103:

const swapCurrencies = useCallback(() => {
    setIsSwapping(true)
    skipNextDebounceSyncRef.current = true
    // ... updateUrlParams(...)
}, [...])

useEffect(() => {
    if (isSwapping && !isLoading) {
        setIsSwapping(false)
    }
}, [isSwapping, isLoading])

Risk: When swapCurrencies fires, setIsSwapping(true) and updateUrlParams happen in the same render cycle. The useExchangeRate hook picks up new URL params only on the next render. If the effect runs before isLoading has flipped to true (i.e., in the very first render where isSwapping becomes true but isLoading is still false from the previous query), the effect immediately clears isSwapping, causing the loading shimmer to flash for 0 frames — the swap animation is invisible.

Suggested fix: Add a small guard so the effect only runs after at least one render with isLoading === true:

const hasSeenLoadingRef = useRef(false)
// In swapCurrencies: hasSeenLoadingRef.current = false
useEffect(() => {
    if (isLoading) hasSeenLoadingRef.current = true
    if (isSwapping && hasSeenLoadingRef.current && !isLoading) {
        setIsSwapping(false)
        hasSeenLoadingRef.current = false
    }
}, [isSwapping, isLoading])

swapCurrencies — Silent no-op when destinationAmount is 0

const newAmount =
    typeof destinationAmount === 'number' && destinationAmount > 0
        ? Math.round(destinationAmount * 100) / 100
        : undefined
updateUrlParams({ from: destinationCurrency, to: sourceCurrency, amount: newAmount })

When destinationAmount is 0 or invalid, newAmount is undefined, and updateUrlParams only sets amount if params.amount !== undefined. This means the old source amount stays in the URL after the swap. The user swaps 100 USD → EUR (yielding e.g. 92 EUR) and then swaps again — but if the destination is 0, the displayed amount after swap will be stale. Add a comment or explicit 0 fallback to clarify intent.


🟡 Code Quality & DRY

StatusBadge + StatusPill — Duplicated color mapping for refunded

Both components now independently map refundedbg-success-2 text-success-4 border border-success-5. If this changes in the future, both must be updated in sync. Consider extracting a shared constant:

// shared/statusColors.ts
export const REFUNDED_STATUS_CLASSES = 'bg-success-2 text-success-4 border border-success-5'

SupportDrawerisCrispReady never resets

isCrispReady is set to true on the first CRISP_READY message and never resets. This is intentional (iframe stays always-mounted), so the loading overlay won't reappear on re-open. This is fine, but should have a comment explaining this decision to avoid future confusion:

// isCrispReady is intentionally never reset: the iframe stays mounted
// across open/close cycles, so we only need to wait for it once.

qr-pay/page.tsx — Unused TransactionReceipt and Hash import warning

Both TransactionReceipt and Hash from viem are still used in handleSimpleFiPayment (let userOpHash: Hash, let receipt: TransactionReceipt | null), so these imports are valid. No action needed — but the AI summary for manteca/page.tsx correctly noted their removal from that file.


✅ Looks Good

Change Assessment
Slider overlay fix (inset-0 z-10 pointer-events-none) Correct — label was being covered by the range fill; z-10 and inset-0 fix both stacking and sizing
TransactionCard refunded strikethrough (twMerge + line-through) Clean, correct, no regressions
StatusBadge + StatusPill refunded → success colors Consistent; refunded is semantically positive (money returned)
SupportDrawer rewrite (drag-to-dismiss, Escape, backdrop) Well-structured; Escape listener is correctly scoped to isSupportModalOpen; same-origin check on message events is correct
Hold-to-claim clipPath overlay clipPath: inset(0 ${100 - holdProgress}% 0 0) is the correct approach for a reveal effect; z-20 stacking over the black fill is correct
PeanutActionDetailsCard + manteca/page.tsx copy change Consistent "You're sending" across both
LandingPage/CurrencySelect — ARS + BRL added to popular Low risk, purely UI ordering
home/page.tsxuseDeviceType removal Safe if the deviceType variable had no downstream consumers

🧪 QA Testing Guide

1. Exchange Rate Widget — Currency Swap

  • Enter 100 USD → EUR, confirm rate shows.
  • Click the swap arrow (↔) — verify shimmer shows during recalculation, then new rate appears (100 EUR → USD).
  • Verify the amount field now shows the former destination amount (e.g., ~92).
  • Swap with destinationAmount = 0 or empty — should not crash.

2. QR Pay — Hold-to-Claim Perk

  • Trigger a Manteca QR payment that returns a perk.
  • On success screen, hold the "Claim Peanut Perk Now!" button — confirm black fill animates left-to-right and white clipped text tracks the fill boundary.
  • Release early — confirm state resets cleanly.
  • Hold to 100% — confirm confetti fires, success banner appears, and perk card switches to "Peanut got you!".

3. QR Pay — Provider Under Maintenance

  • Set MANTECA in disabledPaymentProviders config and scan a Mercado Pago QR — verify maintenance card shows without a React hook crash.

4. Support Drawer

  • Open → drag down past ~100px → confirm drawer dismisses.
  • Open → press Escape → confirm closes.
  • Open → click backdrop → confirm closes.
  • Open for the first time → confirm loading spinner shows until CRISP_READY fires → confirm spinner hides.
  • Close and reopen — confirm chat is still visible (no reload/flash).

5. Refunded Transaction Styling

  • Find a transaction with status = 'refunded'.
  • Confirm USD amount has line-through + grey text.
  • Confirm the secondary currency amount (non-USD) also has line-through.
  • Confirm StatusBadge and StatusPill both show green (success) styling for refunded.

🧠 Learnings used
Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 852
File: src/hooks/useTransactionHistory.ts:177-239
Timestamp: 2025-05-14T21:16:06.369Z
Learning: When reviewing code, pay attention to and resolve CodeRabbit comments as they can contain useful refactoring suggestions and insights.

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

Learnt from: Zishan-7
Repo: peanutprotocol/peanut-ui PR: 1094
File: src/components/AddWithdraw/DynamicBankAccountForm.tsx:279-279
Timestamp: 2025-08-14T08:02:26.705Z
Learning: For hotfixes in the peanut-ui codebase, prefer generic error messages over specific validation error details until the copy can be reviewed with the team, even when the validation functions return detailed error messages.

Learnt from: kushagrasarathe
Repo: peanutprotocol/peanut-ui PR: 0
File: :0-0
Timestamp: 2026-03-04T18:32:51.654Z
Learning: In peanutprotocol/peanut-ui, Bridge-specific add-money and withdraw flows (`src/app/(mobile-ui)/add-money/[country]/bank/page.tsx`, `src/components/AddWithdraw/AddWithdrawCountriesList.tsx`, `src/components/Claim/Link/views/BankFlowManager.view.tsx`) have been migrated (PR `#1713`) to use the unified `isUserKycApproved` from `useKycStatus()` hook instead of per-component `liveKycStatus` + WebSocket state. The migration is complete; no more direct `bridgeKycStatus === 'approved'` checks in those files.

Learnt from: Hugo0
Repo: peanutprotocol/peanut-ui PR: 1014
File: src/components/Claim/Link/Initial.view.tsx:413-413
Timestamp: 2025-07-24T13:26:10.290Z
Learning: In the peanut-ui repository, the change from `${SQUID_API_URL}/route` to `${SQUID_API_URL}/v2/route` in src/components/Claim/Link/Initial.view.tsx was a typo fix, not an API migration, as the codebase was already using Squid API v2.
✅ Actions performed

Full review triggered.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/features/payments/shared/components/SendWithPeanutCta.tsx (1)

63-74: Consider extracting invite code generation to a shared utility.

The invite code generation logic (${inviterUsername.toUpperCase()}INVITESYOU) is duplicated in RequestPotActionList.tsx (line 134). Consider extracting this to a shared utility function to ensure consistency and simplify maintenance.

♻️ Suggested refactor

Create a utility function:

// In a shared utils file
export const generateInviteCode = (username: string) => `${username.toUpperCase()}INVITESYOU`

Then use it in both components:

-const inviteCode = `${inviterUsername.toUpperCase()}INVITESYOU`
+const inviteCode = generateInviteCode(inviterUsername)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/payments/shared/components/SendWithPeanutCta.tsx` around lines
63 - 74, Extract the duplicated invite-code logic into a shared utility (e.g.,
export const generateInviteCode(username: string) =>
`${username.toUpperCase()}INVITESYOU`) and replace the inline construction in
SendWithPeanutCta (inside the block that sets inviteCode and dispatches
setupActions.setInviteType) and the duplicate in RequestPotActionList (around
line 134) to call generateInviteCode(inviterUsername) so both components use the
same helper and stay consistent.
src/features/payments/flows/contribute-pot/components/RequestPotActionList.tsx (1)

133-140: Duplicate invite code generation logic.

This invite code generation (${recipientUsername.toUpperCase()}INVITESYOU) duplicates the logic in SendWithPeanutCta.tsx (lines 67-70). As noted in the other file, consider extracting to a shared utility.

The different redirectUri values are appropriate: bank methods redirect to /add-money while the peanut CTA preserves the current location.

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

In
`@src/features/payments/flows/contribute-pot/components/RequestPotActionList.tsx`
around lines 133 - 140, The invite code string
("${recipientUsername.toUpperCase()}INVITESYOU") is duplicated between
RequestPotActionList.tsx and SendWithPeanutCta.tsx; extract this logic into a
single utility (e.g., makeInviteCode(recipientUsername) or formatInviteCode) and
replace the inline template in both components to call that exported function,
then import the util into RequestPotActionList and SendWithPeanutCta and ensure
dispatch(setupActions.setInviteCode(...)) uses the new helper; keep existing
redirectUri behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@src/features/payments/flows/contribute-pot/components/RequestPotActionList.tsx`:
- Around line 133-140: The invite code string
("${recipientUsername.toUpperCase()}INVITESYOU") is duplicated between
RequestPotActionList.tsx and SendWithPeanutCta.tsx; extract this logic into a
single utility (e.g., makeInviteCode(recipientUsername) or formatInviteCode) and
replace the inline template in both components to call that exported function,
then import the util into RequestPotActionList and SendWithPeanutCta and ensure
dispatch(setupActions.setInviteCode(...)) uses the new helper; keep existing
redirectUri behavior unchanged.

In `@src/features/payments/shared/components/SendWithPeanutCta.tsx`:
- Around line 63-74: Extract the duplicated invite-code logic into a shared
utility (e.g., export const generateInviteCode(username: string) =>
`${username.toUpperCase()}INVITESYOU`) and replace the inline construction in
SendWithPeanutCta (inside the block that sets inviteCode and dispatches
setupActions.setInviteType) and the duplicate in RequestPotActionList (around
line 134) to call generateInviteCode(inviterUsername) so both components use the
same helper and stay consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5415dad8-42cd-4d3e-83da-5a9e1e2c4bd6

📥 Commits

Reviewing files that changed from the base of the PR and between 6d74335 and 5bfd9e7.

📒 Files selected for processing (3)
  • src/features/payments/flows/contribute-pot/components/RequestPotActionList.tsx
  • src/features/payments/flows/contribute-pot/views/ContributePotInputView.tsx
  • src/features/payments/shared/components/SendWithPeanutCta.tsx

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/app/(mobile-ui)/qr-pay/page.tsx (1)

1061-1077: ⚠️ Potential issue | 🔴 Critical

Move useMemo out of the conditional branch—this violates React's Rules of Hooks.

The useMemo at line 1063 is called only when isProviderDisabled is true. If isProviderDisabled changes between renders (it depends on paymentProcessor), the hook call order changes, causing React's hook state to become corrupted.

Additionally, the empty dependency array [] is incorrect; the memoized value depends on paymentProcessor and qrType.

🔧 Proposed fix
+    const paymentMethodName = useMemo(() => {
+        if (paymentProcessor === 'MANTECA') {
+            switch (qrType) {
+                case EQrType.PIX:
+                    return 'PIX'
+                case EQrType.MERCADO_PAGO:
+                    return 'Mercado Pago'
+                case EQrType.ARGENTINA_QR3:
+                    return 'QR'
+                default:
+                    return 'QR'
+            }
+        }
+        return 'SimpleFi'
+    }, [paymentProcessor, qrType])
+
     if (isProviderDisabled) {
-        const paymentMethodName = useMemo(() => {
-            if (paymentProcessor === 'MANTECA') {
-                switch (qrType) {
-                    case EQrType.PIX:
-                        return 'PIX'
-                    case EQrType.MERCADO_PAGO:
-                        return 'Mercado Pago'
-                    case EQrType.ARGENTINA_QR3:
-                        return 'QR'
-                    default:
-                        return 'QR'
-                }
-            }
-            return 'SimpleFi'
-        }, [])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(mobile-ui)/qr-pay/page.tsx around lines 1061 - 1077, The hook call
to useMemo for computing paymentMethodName is currently inside the
isProviderDisabled conditional which breaks React's Rules of Hooks; move the
useMemo call to the component's top-level (always-call) so it runs on every
render, and update its dependency array to [paymentProcessor, qrType] so the
memo recomputes when those values change; keep using the same logic inside the
memo (checking paymentProcessor === 'MANTECA' and the EQrType switch) and then
reference the paymentMethodName variable inside the isProviderDisabled branch as
before.
♻️ Duplicate comments (1)
src/components/Global/SupportDrawer/index.tsx (1)

43-55: ⚠️ Potential issue | 🟡 Minor

Don't gate the chat on a single ready message.

isCrispReady only flips from CRISP_READY, so any proxy failure leaves the loading overlay up forever and blocks support. Please add a timeout or iframe load/error fallback so the user can recover.

Also applies to: 104-107

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

In `@src/components/Global/SupportDrawer/index.tsx` around lines 43 - 55, The
current useEffect installs handleMessage which only sets isCrispReady when
receiving 'CRISP_READY', leaving the loading overlay stuck if that message never
arrives; update the SupportDrawer logic (the useEffect, handleMessage, and any
code that reads isCrispReady) to add a fail-safe: start a timeout (e.g. 5–10s)
when the iframe is created that flips isCrispReady (or a separate
isCrispFallback flag) so the UI unblocks if 'CRISP_READY' never arrives, and
also attach iframe onLoad and onError handlers to call
setIsCrispReady/setIsCrispFallback accordingly; ensure you still remove the
message listener and clear the timeout in the cleanup to avoid leaks and keep
the existing CRISP_READY handling intact.
🧹 Nitpick comments (4)
src/components/Global/StatusPill/index.tsx (1)

16-16: LGTM! Refunded status color update is consistent.

The refunded status now uses the success palette (green), matching the identical change in StatusBadge.tsx line 35. This creates a consistent visual treatment across the UI.

As noted in the StatusBadge.tsx review, both components now duplicate the refunded → success color mapping. Consider extracting this to a shared constant to prevent future drift between the two components.

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

In `@src/components/Global/StatusPill/index.tsx` at line 16, Both components
duplicate the refunded→success color mapping; extract the shared mapping into a
single exported constant (e.g., STATUS_CLASS_MAP or REFUND_STATUS_CLASS) and
import it into StatusPill (index.tsx) and StatusBadge so both use the same
source of truth; update the mapping references (the refunded key currently in
StatusPill and the mapping in StatusBadge) to reference the new constant and
remove the duplicate literal mappings.
src/components/Global/Badges/StatusBadge.tsx (1)

34-35: Refunded status color change looks good.

The change from yellow/pending palette to green/success palette for refunded transactions is visually consistent and intuitive. However, note that StatusPill/index.tsx line 16 duplicates this exact refunded → success color mapping.

♻️ Consider extracting shared constant to avoid future drift

To keep the refunded status styling consistent between StatusBadge and StatusPill, consider extracting the color mapping to a shared constant:

// src/components/Global/statusColors.ts
export const STATUS_COLOR_MAPS = {
  refunded: 'success' as const,
  completed: 'success' as const,
  closed: 'success' as const,
  // ... other mappings
}

Then both components can reference the same source of truth.

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

In `@src/components/Global/Badges/StatusBadge.tsx` around lines 34 - 35,
StatusBadge's refunded→success color mapping is duplicated in StatusPill;
extract a single source-of-truth constant (e.g., export const STATUS_COLOR_MAPS
= { refunded: 'success', completed: 'success', closed: 'success', ... } from a
new module like statusColors.ts) and update both StatusBadge and StatusPill to
import STATUS_COLOR_MAPS and derive the CSS class string from the mapped token
instead of hardcoding 'refunded' → 'bg-success-2 ...'; ensure the mapping
contains all existing status keys and that both components use the mapping
symbol when building their className logic.
src/features/payments/shared/components/SendWithPeanutCta.tsx (1)

66-70: Consider extracting invite code generation to a shared utility.

The invite code generation pattern ${username.toUpperCase()}INVITESYOU and the Redux dispatch sequence is duplicated in RequestPotActionList.tsx (lines 133-137). Extracting this to a shared utility would reduce duplication and prevent drift.

♻️ Suggested utility extraction

Create a shared utility in src/utils/invite.utils.ts:

import { setupActions } from '@/redux/slices/setup-slice'
import { EInviteType } from '@/services/services.types'
import type { AppDispatch } from '@/redux/store'

export const generateInviteCode = (username: string): string => {
    return `${username.toUpperCase()}INVITESYOU`
}

export const dispatchInviteCode = (
    dispatch: AppDispatch,
    username: string,
    inviteType: EInviteType = EInviteType.PAYMENT_LINK
) => {
    const inviteCode = generateInviteCode(username)
    dispatch(setupActions.setInviteCode(inviteCode))
    dispatch(setupActions.setInviteType(inviteType))
    return inviteCode
}

Then use in both components:

-const inviteCode = `${inviterUsername.toUpperCase()}INVITESYOU`
-dispatch(setupActions.setInviteCode(inviteCode))
-dispatch(setupActions.setInviteType(EInviteType.PAYMENT_LINK))
+const inviteCode = dispatchInviteCode(dispatch, inviterUsername)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/payments/shared/components/SendWithPeanutCta.tsx` around lines
66 - 70, Duplicate invite-code generation and Redux dispatch logic between
SendWithPeanutCta and RequestPotActionList should be extracted into a shared
utility; create functions generateInviteCode(username: string) and
dispatchInviteCode(dispatch, username, inviteType = EInviteType.PAYMENT_LINK)
(referencing setupActions, setInviteCode, setInviteType, and EInviteType) in a
new utils file (e.g., invite.utils.ts), replace the inline code in
SendWithPeanutCta (the block using inviterUsername, setupActions and
router.push) and the corresponding block in RequestPotActionList to call
dispatchInviteCode to get the inviteCode and then call
router.push(`/invite?code=${inviteCode}&redirect_uri=${redirectUri}`).
src/components/Global/ExchangeRateWidget/index.tsx (1)

99-103: Clarify behavior when destinationAmount is invalid.

When destinationAmount is 0 or undefined, newAmount becomes undefined and the existing URL amount is silently preserved. After swap, the source input will show the old amount rather than a swapped value.

If this is intentional, consider adding a brief comment. Otherwise, consider explicit handling (e.g., default to a fallback or clear the amount param).

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

In `@src/components/Global/ExchangeRateWidget/index.tsx` around lines 99 - 103,
The swap branch currently sets newAmount to undefined when destinationAmount is
0 or missing, which silently preserves the existing URL amount; update the
ExchangeRateWidget swap logic around destinationAmount/newAmount to be explicit:
either add a short comment above this block stating that undefined intentionally
preserves the existing amount, or change the behavior to explicitly clear or set
a fallback amount when destinationAmount is invalid (e.g., call
updateUrlParams({ from: destinationCurrency, to: sourceCurrency, amount: null })
or amount: '' or amount: 0 depending on updateUrlParams semantics) so the source
input shows the expected value after swap; update the code at the newAmount
calculation and the subsequent updateUrlParams call accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/`(mobile-ui)/qr-pay/page.tsx:
- Around line 1343-1353: The overlay span that duplicates the button label (the
second <span> that has className "absolute inset-0 z-20 flex items-center
justify-center text-white transition-all duration-75" and uses style.clipPath
with holdProgress) should be hidden from assistive tech so screen readers don't
announce the label twice; update that overlay element (not the visible label
span or the label variable) to be decorative by adding an appropriate
accessibility attribute such as aria-hidden="true" (or role="presentation") to
the overlay span.

In `@src/components/Global/ExchangeRateWidget/index.tsx`:
- Around line 106-111: The current useEffect that clears isSwapping when
!isLoading can race because isLoading may still be false from a prior state;
modify the logic in the ExchangeRateWidget by adding a small local flag (e.g.,
hasObservedLoading) tracked inside the component and referenced in the existing
useEffect for isSwapping: set hasObservedLoading to true when isLoading becomes
true, and only call setIsSwapping(false) when hasObservedLoading is true and
isLoading transitions to false; update the dependencies of the useEffect to
include hasObservedLoading (and keep isSwapping/isLoading) so the effect only
clears swapping after an actual loading cycle has been observed.

In `@src/components/Global/SupportDrawer/index.tsx`:
- Around line 57-65: The sheet currently only wires an Escape key listener on
window via the useEffect referenced (isSupportModalOpen, setIsSupportModalOpen)
but does not move initial focus into the sheet, trap focus there, restore focus
on close, provide a parent close control, nor handle Escape events that occur
inside the iframe (see lines ~79-116). Fix by turning the sheet into an
accessible dialog: when isSupportModalOpen goes true, save
document.activeElement, call focus() on a designated focusable container (or
close button) inside the component, implement a focus trap (or use a library
like focus-trap or ReachDialog) to intercept Tab/Shift+Tab and keep focus inside
the sheet, restore focus to the saved element on close via
setIsSupportModalOpen, and ensure a visible close control in the parent shell.
Also handle Escape events originating inside the iframe by either mounting the
iframe inside the same dialog boundary (so host dialog handles focus) or by
adding a postMessage API from the iframe to the parent to invoke
setIsSupportModalOpen(false) (and still keep the parent Escape listener) so
presses inside the framed document close the sheet.

---

Outside diff comments:
In `@src/app/`(mobile-ui)/qr-pay/page.tsx:
- Around line 1061-1077: The hook call to useMemo for computing
paymentMethodName is currently inside the isProviderDisabled conditional which
breaks React's Rules of Hooks; move the useMemo call to the component's
top-level (always-call) so it runs on every render, and update its dependency
array to [paymentProcessor, qrType] so the memo recomputes when those values
change; keep using the same logic inside the memo (checking paymentProcessor ===
'MANTECA' and the EQrType switch) and then reference the paymentMethodName
variable inside the isProviderDisabled branch as before.

---

Duplicate comments:
In `@src/components/Global/SupportDrawer/index.tsx`:
- Around line 43-55: The current useEffect installs handleMessage which only
sets isCrispReady when receiving 'CRISP_READY', leaving the loading overlay
stuck if that message never arrives; update the SupportDrawer logic (the
useEffect, handleMessage, and any code that reads isCrispReady) to add a
fail-safe: start a timeout (e.g. 5–10s) when the iframe is created that flips
isCrispReady (or a separate isCrispFallback flag) so the UI unblocks if
'CRISP_READY' never arrives, and also attach iframe onLoad and onError handlers
to call setIsCrispReady/setIsCrispFallback accordingly; ensure you still remove
the message listener and clear the timeout in the cleanup to avoid leaks and
keep the existing CRISP_READY handling intact.

---

Nitpick comments:
In `@src/components/Global/Badges/StatusBadge.tsx`:
- Around line 34-35: StatusBadge's refunded→success color mapping is duplicated
in StatusPill; extract a single source-of-truth constant (e.g., export const
STATUS_COLOR_MAPS = { refunded: 'success', completed: 'success', closed:
'success', ... } from a new module like statusColors.ts) and update both
StatusBadge and StatusPill to import STATUS_COLOR_MAPS and derive the CSS class
string from the mapped token instead of hardcoding 'refunded' → 'bg-success-2
...'; ensure the mapping contains all existing status keys and that both
components use the mapping symbol when building their className logic.

In `@src/components/Global/ExchangeRateWidget/index.tsx`:
- Around line 99-103: The swap branch currently sets newAmount to undefined when
destinationAmount is 0 or missing, which silently preserves the existing URL
amount; update the ExchangeRateWidget swap logic around
destinationAmount/newAmount to be explicit: either add a short comment above
this block stating that undefined intentionally preserves the existing amount,
or change the behavior to explicitly clear or set a fallback amount when
destinationAmount is invalid (e.g., call updateUrlParams({ from:
destinationCurrency, to: sourceCurrency, amount: null }) or amount: '' or
amount: 0 depending on updateUrlParams semantics) so the source input shows the
expected value after swap; update the code at the newAmount calculation and the
subsequent updateUrlParams call accordingly.

In `@src/components/Global/StatusPill/index.tsx`:
- Line 16: Both components duplicate the refunded→success color mapping; extract
the shared mapping into a single exported constant (e.g., STATUS_CLASS_MAP or
REFUND_STATUS_CLASS) and import it into StatusPill (index.tsx) and StatusBadge
so both use the same source of truth; update the mapping references (the
refunded key currently in StatusPill and the mapping in StatusBadge) to
reference the new constant and remove the duplicate literal mappings.

In `@src/features/payments/shared/components/SendWithPeanutCta.tsx`:
- Around line 66-70: Duplicate invite-code generation and Redux dispatch logic
between SendWithPeanutCta and RequestPotActionList should be extracted into a
shared utility; create functions generateInviteCode(username: string) and
dispatchInviteCode(dispatch, username, inviteType = EInviteType.PAYMENT_LINK)
(referencing setupActions, setInviteCode, setInviteType, and EInviteType) in a
new utils file (e.g., invite.utils.ts), replace the inline code in
SendWithPeanutCta (the block using inviterUsername, setupActions and
router.push) and the corresponding block in RequestPotActionList to call
dispatchInviteCode to get the inviteCode and then call
router.push(`/invite?code=${inviteCode}&redirect_uri=${redirectUri}`).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b8a22bfd-6095-4543-b031-160d0a22e82e

📥 Commits

Reviewing files that changed from the base of the PR and between 420c791 and 5bfd9e7.

📒 Files selected for processing (16)
  • src/app/(mobile-ui)/home/page.tsx
  • src/app/(mobile-ui)/points/page.tsx
  • src/app/(mobile-ui)/qr-pay/page.tsx
  • src/app/(mobile-ui)/withdraw/manteca/page.tsx
  • src/components/Global/Badges/StatusBadge.tsx
  • src/components/Global/ExchangeRateWidget/index.tsx
  • src/components/Global/Icons/Icon.tsx
  • src/components/Global/PeanutActionDetailsCard/index.tsx
  • src/components/Global/StatusPill/index.tsx
  • src/components/Global/SupportDrawer/index.tsx
  • src/components/LandingPage/CurrencySelect.tsx
  • src/components/Slider/index.tsx
  • src/components/TransactionDetails/TransactionCard.tsx
  • src/features/payments/flows/contribute-pot/components/RequestPotActionList.tsx
  • src/features/payments/flows/contribute-pot/views/ContributePotInputView.tsx
  • src/features/payments/shared/components/SendWithPeanutCta.tsx
💤 Files with no reviewable changes (2)
  • src/components/Global/Icons/Icon.tsx
  • src/app/(mobile-ui)/home/page.tsx

- move paymentMethodName useMemo above the isProviderDisabled conditional
  to fix Rules of Hooks violation (hooks can't be called conditionally)
- add proper deps (paymentProcessor, qrType) to the useMemo
- fix text-grey-1 → text-gray-1 in TransactionCard for consistency
@kushagrasarathe kushagrasarathe merged commit ac1edd0 into dev Mar 20, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant