Skip to content

SP 112 Prod release - Lockup#1318

Merged
jjramirezn merged 177 commits intopeanut-walletfrom
peanut-wallet-dev
Oct 13, 2025
Merged

SP 112 Prod release - Lockup#1318
jjramirezn merged 177 commits intopeanut-walletfrom
peanut-wallet-dev

Conversation

@kushagrasarathe
Copy link
Contributor

No description provided.

@vercel
Copy link

vercel bot commented Oct 13, 2025

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

Project Deployment Preview Comments Updated (UTC)
peanut-ui (peanut-wallet-staging) Ready Ready Preview Comment Oct 13, 2025 0:00am
peanut-wallet Ready Ready Preview Comment Oct 13, 2025 0:00am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 13, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Adds OneSignal-based notifications, a full invite/waitlist feature (validation, accept, pages, OG image), setup flow changes (collect-email, Suspense wrappers, safer redirects), many UI/text/icon updates, new hooks/services (useNotifications, invitesApi), Redux invite state, and assorted component API/prop changes.

Changes

Cohort / File(s) Summary
Notifications (OneSignal & UI)
src/hooks/useNotifications.ts, src/components/Notifications/SetupNotificationsModal.tsx, src/components/Notifications/NotificationBanner.tsx, src/components/Notifications/NotificationNavigation.tsx, src/app/(mobile-ui)/notifications/page.tsx, src/services/notifications.ts, public/onesignal/OneSignalSDKWorker.js, .env.example, package.json
Add OneSignal init/hook, permission modal/banner, unread-badge nav, notifications list page (infinite scroll/mark-read), service API/types, service worker, env vars, and react-onesignal dependency.
Invites & Waitlist (API, pages, Redux, types)
src/services/invites.ts, src/app/actions/invites.ts, src/app/invite/page.tsx, src/components/Invites/*, src/redux/slices/setup-slice.ts, src/redux/types/setup.types.ts, src/services/services.types.ts, src/interfaces/interfaces.ts, src/hooks/useZeroDev.ts, src/hooks/useLogin.tsx
New invites service and server action, invite validation/accept flows, invite/waitlist pages/layouts, Redux invite state and types, user model fields, cookie-based invite handling during registration/login.
Setup flow & safer redirects
src/app/(setup)/layout.tsx, src/app/(setup)/setup/page.tsx, src/components/Setup/Setup.consts.tsx, src/components/Setup/Setup.types.ts, src/components/Setup/Views/*, src/components/Setup/Views/index.ts
Move setup content into Suspense-wrapped internals, add JoinWaitlist and CollectEmail steps, integrate invite cookie/store into step decision, and tighten redirect sanitization/handling.
Invite UX in payment/claim flows
src/components/Claim/Link/Initial.view.tsx, src/components/Payment/PaymentForm/index.tsx, src/components/Common/ActionList.tsx, src/components/Common/ActionListDaimoPayButton.tsx, src/components/Global/ConfirmInviteModal/index.tsx, src/app/[...recipient]/client.tsx
Pre-accept-invite logic, isInviteLink prop propagation, ConfirmInviteModal and modal gating, programmatic DaimoPay coordination, accept-invite integration into payment flows, and unmount payment-state reset.
Profile / VerifiedUserLabel / invite UI
src/components/UserHeader/index.tsx, src/components/Profile/index.tsx, src/components/Profile/components/*, src/components/Global/EarlyUserModal/index.tsx, src/components/Global/NoMoreJailModal/index.tsx
Pass username into VerifiedUserLabel, add invite/share modals, EarlyUser and NoMoreJail modals, and profile invite UI/actions.
Home banners, header, history, icons
src/hooks/useBanners.tsx, src/components/Home/HomeBanners/*, src/components/Home/HomeHistory.tsx, src/components/Home/InvitesIcon.tsx, src/app/(mobile-ui)/home/page.tsx
Add notification-banner hook integration (onClick/onClose/isPermissionDenied), BannerCard onClick passthrough, refine history empty states, add invites icon, and integrate notification/invite modal flows into Home header.
Landing content & assets
src/app/page.tsx, src/components/LandingPage/{hero,dropLink,sendInSeconds,RegulatedRails,securityBuiltIn,yourMoney}.tsx, src/assets/icons/index.ts, src/components/Global/ShareButton/index.tsx
Update CTAs/headings/copy, add animated messaging icons, export new icon assets, and adjust ShareButton layout/imports.
Global components & icons
src/components/Global/ActionModal/index.tsx, src/components/Global/CopyToClipboard/index.tsx, src/components/0_Bruddle/Toast.tsx, src/components/Global/Icons/{Icon.tsx,bell.tsx,trophy.tsx,invite-heart.tsx,lock.tsx}, src/assets/icons/index.ts
Add modal content/button children, copy-as-button mode, increase toast z-index, add new SVG icons and update IconName mapping, export icons.
OG image / InviteCardOG
src/app/api/og/route.tsx, src/components/og/InviteCardOG.tsx
Centralize arrow assets, add invite-specific OG rendering and InviteCardOG component for dynamic OG images.
Points & Invites UX
src/app/(mobile-ui)/points/page.tsx
Rework Points page to fetch/display invites, show invite code/link with copy/share and list invited users.
Search UI removal
src/components/SearchUsers/index.tsx
Remove the portal-based SearchUsers component and its UI.
Utilities & infra
src/utils/general.utils.ts, src/utils/sentry.utils.ts, src/utils/history.utils.ts, tailwind.config.js
Add redirect sanitization/getValidRedirectUrl, invite share/link generators, fetchWithSentry timeout via AbortController (uses NEW env var), expand receipt URL logic, and new Tailwind keyframes/color/animation.
Notifications types & page
src/services/notifications.ts, src/components/Notifications/*, src/app/(mobile-ui)/notifications/page.tsx
Add InAppItem/ListResponse types and notificationsApi (list/unread/markRead) plus client notifications page with infinite scroll and optimistic mark-read.
Misc API/prop changes & refactors
multiple files (e.g., src/context/authContext.tsx, src/components/*, src/redux/*, src/hooks/*, src/utils/*)
Expose invitedUsernamesSet, add fetchUser to useAuth consumers, propagate username to VerifiedUserLabel usages, remove SavedAccountsView truncation, add/remove small props and text updates across many components.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title “SP 112 Prod release - Lockup” is ambiguous and doesn’t convey the primary scopes of these extensive changes, sounding like an internal sprint release note rather than a concise summary of the main functionality. It fails to meaningfully describe any specific feature or component added or updated in this pull request. Please revise the title to a clear, concise sentence that highlights the core functionality introduced, such as “Integrate OneSignal notifications and invite flows with waitlist pages.”
Description Check ⚠️ Warning The pull request lacks any description of the changes, offering no context or summary to reviewers. Please include a concise description of the changes and their purpose to help reviewers understand this release.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch peanut-wallet-dev

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

❤️ Share

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

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

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 25

Caution

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

⚠️ Outside diff range comments (7)
src/components/Common/SavedAccountsView.tsx (1)

3-3: Remove unused import.

shortenStringLong is imported but no longer used after removing the truncation logic.

Apply this diff:

-import { shortenStringLong, formatIban } from '@/utils/general.utils'
+import { formatIban } from '@/utils/general.utils'
src/components/LandingPage/sendInSeconds.tsx (1)

233-239: Update CTA link to match waitlist flow
Button text reads “JOIN WAITLIST” but still links to /send. Change the anchor’s href to /setup so it directs users to the setup page’s waitlist step.

src/components/Profile/index.tsx (1)

74-79: Click opens modal but href navigates immediately

With href set, the router may navigate before the modal shows.

Apply one of:

  • Remove href and only navigate from modal CTA, or
  • Prevent default inside onClick (if ProfileMenuItem forwards the event).

Example (removing href):

-                        <ProfileMenuItem
+                        <ProfileMenuItem
                             icon="shield"
                             label="Identity Verification"
-                            href="/profile/identity-verification"
-                            onClick={() => {
-                                setShowInitiateKycModal(true)
-                            }}
+                            onClick={() => setShowInitiiateKycModal(true)}
                             position="middle"

Or ensure onClick receives event and calls e.preventDefault().

onClick={(e) => { e.preventDefault(); setShowInitiateKycModal(true) }}
src/components/Claim/Link/Initial.view.tsx (1)

161-168: Fix loading-state leak on early return

setLoadingState('Loading') is set, then an early return leaves the UI stuck in loading if recipient.address === ''.

             setLoadingState('Loading')
@@
-            if (recipient.address === '') return
+            if (recipient.address === '') {
+                setLoadingState('Idle')
+                return
+            }
src/components/Common/ActionListDaimoPayButton.tsx (1)

102-134: Add missing dependency to useCallback

peanutWalletAddress is captured but not listed; include to avoid stale address.

-        [chargeDetails, completeDaimoPayment, dispatch]
+        [chargeDetails, completeDaimoPayment, dispatch, peanutWalletAddress]
src/components/Common/ActionList.tsx (2)

213-236: Don’t compute invite username after render guard

username relies on flow and potentially undefined requestLinkData?.recipient even when flow === 'claim'. When we hit the invite banner ({isInviteLink && !userHasAppAccess && username && ...}) on the request flow with missing requestLinkData, username falls back to claimLinkData?.sender?.username, which is undefined, and the banner disappears even though request invite data exists. Please branch the derivation to match flow instead of coalescing:

-    const username = claimLinkData?.sender?.username ?? requestLinkData?.recipient?.identifier
+    const username =
+        flow === 'claim'
+            ? claimLinkData?.sender?.username
+            : requestLinkData?.recipient?.identifier

This way request invites always show the inviter text.


247-304: Modal losses when reopening a method

When the confirm invite modal’s “Lose invite” branch calls handleMethodClick(selectedMethod) we never reset showInviteModal before navigation. If handleMethodClick short-circuits (e.g. because of min-amount check), the modal remains open but selectedMethod becomes null, breaking the UI. Please hide the modal before invoking the handler:

-                handleLoseInvite={() => {
-                    if (selectedMethod) {
-                        handleMethodClick(selectedMethod)
-                        setShowInviteModal(false)
-                        setSelectedMethod(null)
-                    }
-                }}
+                handleLoseInvite={() => {
+                    if (!selectedMethod) return
+                    setShowInviteModal(false)
+                    const methodToResume = selectedMethod
+                    setSelectedMethod(null)
+                    handleMethodClick(methodToResume)
+                }}

Prevents stale state when the handler exits early.

🧹 Nitpick comments (33)
src/components/0_Bruddle/Toast.tsx (1)

98-98: Consider using a documented z-index scale instead of an extreme value.

While z-[99999] ensures toasts appear above other overlays, such a high value can lead to maintainability issues and z-index escalation. Consider defining a z-index scale in your design system (e.g., using CSS custom properties or a constants file) with semantic tiers like tooltip: 9000, modal: 8000, toast: 7000, etc.

Example approach using Tailwind configuration:

// tailwind.config.js or constants file
export const zIndex = {
  base: 1,
  dropdown: 1000,
  sticky: 1020,
  fixed: 1030,
  modalBackdrop: 1040,
  modal: 1050,
  popover: 1060,
  toast: 1070,
  tooltip: 1080,
}

Then update the className:

-<div className={`fixed z-[99999] flex flex-col gap-2 ${getPositionClasses('bottom-right')}`}>
+<div className={`fixed z-[1070] flex flex-col gap-2 ${getPositionClasses('bottom-right')}`}>
tailwind.config.js (1)

91-91: Verify the color naming and value.

The color value #FF5656 appears to be a bright red rather than orange. Consider verifying this is the intended color or renaming to better reflect its actual appearance (e.g., red.2).

src/components/Home/HomeHistory.tsx (1)

215-231: Optional: Consider extracting common EmptyState props.

Both empty state branches use identical icon and description props with only the title differing. While this duplication is minor, extracting the common props could improve maintainability if these values need to change in the future.

Example refactor:

const emptyStateProps = {
    icon: 'txn-off' as const,
    description: 'Start by sending or requesting money',
}

// Then use:
<EmptyState {...emptyStateProps} title="No activity yet!" />
// and
<EmptyState {...emptyStateProps} title="No transactions yet!" />
src/app/actions/invites.ts (1)

6-6: Minor inconsistency in API_KEY handling.

Line 6 uses the ! assertion (API_KEY!), but line 13 checks if API_KEY is falsy. While this works in practice (the runtime check catches missing values), consider removing the ! assertion for consistency:

-const API_KEY = process.env.PEANUT_API_KEY!
+const API_KEY = process.env.PEANUT_API_KEY

Also applies to: 13-16

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

46-52: Prefer Sentry.captureMessage for string messages.

Line 51 calls Sentry.captureException with a string. While this works, it's more idiomatic to use Sentry.captureMessage for string messages or wrap the string in an Error object.

Apply this diff:

                 } else {
                     // If redirect_uri was invalid, stay on current page
-                    Sentry.captureException(`Invalid redirect URL ${redirect_uri}`)
+                    Sentry.captureMessage(`Invalid redirect URL: ${redirect_uri}`)
                 }
src/components/Home/InvitesIcon.tsx (1)

5-5: Consider a more descriptive alt text.

The alt text "star" doesn't clearly convey the semantic meaning of this icon in the invites context. Consider using something more descriptive like "invites" or "new invite notification" for better accessibility.

Apply this diff:

-    return <Image className="animate-pulsate-slow" src={STAR_STRAIGHT_ICON} alt="star" width={20} height={20} />
+    return <Image className="animate-pulsate-slow" src={STAR_STRAIGHT_ICON} alt="invites" width={20} height={20} />
src/components/Global/Icons/invite-heart.tsx (1)

5-30: Consider theming implications of hardcoded colors.

Unlike other icon components (lock, trophy) which use currentColor for theming flexibility, this icon has hardcoded color values (black, #FF90E8, #FFC400). While this may be intentional for branding consistency, it limits the icon's adaptability to different themes or contexts.

If theming flexibility is desired, consider accepting color props:

-export const InviteHeartIcon: FC<SVGProps<SVGSVGElement>> = (props) => {
+interface InviteHeartIconProps extends SVGProps<SVGSVGElement> {
+    primaryColor?: string
+    accentColor?: string
+    highlightColor?: string
+}
+
+export const InviteHeartIcon: FC<InviteHeartIconProps> = ({ 
+    primaryColor = 'black', 
+    accentColor = '#FF90E8', 
+    highlightColor = '#FFC400',
+    ...props 
+}) => {
     return (
         <svg width="66" height="58" viewBox="0 0 66 58" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
             <path
                 d="M30.7753 57.48C30.5053 57.48 30.2353 57.42 29.9853 57.31C18.7553 52.36 9.4753 43.5 3.8553 32.38C0.3853 25.51 -0.464699 19.45 1.3153 14.34C3.2553 8.78 8.5953 4.62001 14.5853 3.97001C15.1453 3.91001 15.7253 3.88 16.2953 3.88C21.3553 3.88 26.4953 6.27 30.0353 10.29C30.2853 10.58 30.5353 10.87 30.7753 11.18C31.0153 10.88 31.2653 10.58 31.5153 10.29C35.0553 6.28 40.1853 3.88 45.2553 3.88C45.8253 3.88 46.4053 3.91001 46.9653 3.97001C52.9553 4.61001 58.2853 8.78 60.2353 14.34C62.0153 19.44 61.1653 25.51 57.6953 32.38C52.0753 43.5 42.7953 52.35 31.5653 57.31C31.3153 57.42 31.0453 57.48 30.7753 57.48Z"
-                fill="black"
+                fill={primaryColor}
             />
-            <!-- Apply similar changes to other path fills -->
         </svg>
     )
 }

Based on learnings

src/components/Global/Icons/lock.tsx (1)

5-17: Minor redundancy: duplicate fill attribute.

The fill="currentColor" is specified both on the <svg> element (line 9) and on the <path> element (line 15). The path inherits the fill from the svg element, making the second declaration redundant.

Apply this diff to remove the redundant fill:

         <svg
             width="16"
             height="20"
             viewBox="0 0 16 20"
             fill="currentColor"
             xmlns="http://www.w3.org/2000/svg"
             {...props}
         >
             <path
                 d="M13.4286 6.83333H12.5239V5.02381C12.5239 2.52667 10.4972 0.5 8.00006 0.5C5.50292 0.5 3.47625 2.52667 3.47625 5.02381V6.83333H2.57149C1.57625 6.83333 0.761963 7.64762 0.761963 8.64286V17.6905C0.761963 18.6857 1.57625 19.5 2.57149 19.5H13.4286C14.4239 19.5 15.2382 18.6857 15.2382 17.6905V8.64286C15.2382 7.64762 14.4239 6.83333 13.4286 6.83333ZM5.28577 5.02381C5.28577 3.5219 6.49815 2.30952 8.00006 2.30952C9.50196 2.30952 10.7143 3.5219 10.7143 5.02381V6.83333H5.28577V5.02381ZM13.4286 17.6905H2.57149V8.64286H13.4286V17.6905ZM8.00006 14.9762C8.9953 14.9762 9.80958 14.1619 9.80958 13.1667C9.80958 12.1714 8.9953 11.3571 8.00006 11.3571C7.00482 11.3571 6.19053 12.1714 6.19053 13.1667C6.19053 14.1619 7.00482 14.9762 8.00006 14.9762Z"
-                fill="currentColor"
             />
         </svg>
src/hooks/useZeroDev.ts (1)

65-75: Consider surfacing invite acceptance failures to the user.

The current implementation logs errors to the console but doesn't inform the user if invite acceptance fails. While registration continues successfully, users might expect confirmation that their invite was accepted or at least a warning if it failed.

Consider one of the following approaches:

  1. Store the acceptance result in state and display a toast/notification after registration completes
  2. Add the result to the registration flow's return value so calling code can handle it
  3. At minimum, use Sentry to capture the error for monitoring:
                 } catch (e) {
                     console.error('Error accepting invite', e)
+                    captureException(e) // Already imported at line 13
                 }
src/components/Home/HomeBanners/index.tsx (1)

19-29: Non-null assertions assume notification-banner structure.

Lines 23-25 use non-null assertions (!) when passing onClick, onClose, and isPermissionDenied to NotificationBanner. While the useBanners hook currently guarantees these properties exist for notification-banner (as seen in the relevant code snippet), this creates tight coupling and could break if the banner structure changes.

Consider defensive checks or TypeScript type guards:

             if (banner.id === 'notification-banner') {
+                if (!banner.onClick || !banner.onClose || banner.isPermissionDenied === undefined) {
+                    console.warn('notification-banner missing required props')
+                    return null
+                }
                 return (
                     <div key={banner.id} className="embla__slide">
                         <NotificationBanner
-                            onClick={banner.onClick!}
-                            onClose={banner.onClose!}
-                            isPermissionDenied={banner.isPermissionDenied!}
+                            onClick={banner.onClick}
+                            onClose={banner.onClose}
+                            isPermissionDenied={banner.isPermissionDenied}
                         />
                     </div>
                 )
             }

Alternatively, refine the Banner type in the hook to have a discriminated union where notification-banner has required properties.

src/components/Notifications/NotificationNavigation.tsx (1)

11-11: Remove unused loading state.

The setIsLoading state setter on line 11 is declared but never used to display a loading indicator in the UI. If loading feedback isn't needed, remove the state to reduce complexity.

Apply this diff to remove the unused state:

     const [notificationCount, setNotificationCount] = useState<number>(0)
-    const [, setIsLoading] = useState<boolean>(false)

And update the fetch function:

         const fetchNotificationCount = async () => {
-            setIsLoading(true)
             try {
                 const { count } = await notificationsApi.unreadCount()
                 setNotificationCount(count)
             } catch (error) {
                 console.error(error)
-            } finally {
-                setIsLoading(false)
             }
         }
src/components/Global/NoMoreJailModal/index.tsx (1)

10-10: Consider renaming for consistency.

The state variable is named isOpen, but the Modal component uses a visible prop. For consistency across the codebase (e.g., ActionModal uses visible), consider renaming to visible or isVisible.

-const [isOpen, setisOpen] = useState(false)
+const [visible, setVisible] = useState(false)

 const onClose = () => {
-    setisOpen(false)
+    setVisible(false)
     sessionStorage.removeItem('showNoMoreJailModal')
 }

 // ... in useEffect
 if (showNoMoreJailModal === 'true') {
-    setisOpen(true)
+    setVisible(true)
 }

 return (
     <Modal
         hideOverlay
-        visible={isOpen}
+        visible={visible}
src/components/UserHeader/index.tsx (1)

37-38: Harden invite checks and avoid duplicate Tooltip ids

  • Guard invitedUsernamesSet access and make ids unique to prevent collisions when multiple labels render.
  • Optional: improve keyboard a11y by making Tooltip trigger focusable (tabIndex=0) in Tooltip component.

Apply:

-    const { invitedUsernamesSet, user } = useAuth()
+    const { invitedUsernamesSet, user } = useAuth()

-    // O(1) lookup in pre-computed Set
-    const isInvitedByLoggedInUser = invitedUsernamesSet.has(username)
+    // O(1) lookup in pre-computed Set (guard against undefined)
+    const isInvitedByLoggedInUser = invitedUsernamesSet?.has(username) === true
-            {badge && (
-                <Tooltip id="verified-user-label" content={tooltipContent} position="top">
+            {badge && (
+                <Tooltip id={`verified-user-label-${username}`} content={tooltipContent} position="top">
                 {badge}
                 </Tooltip>
             )}
-            {(isInvitedByLoggedInUser || isInviter) && (
-                <Tooltip
-                    id={isInviter ? 'inviter-user' : 'invited-by-user'}
+            {(isInvitedByLoggedInUser || isInviter) && (
+                <Tooltip
+                    id={isInviter ? `inviter-user-${username}` : `invited-by-user-${username}`}
                     content={isInviter ? 'You were invited by this user.' : "You've invited this user."}
                     position="top"
                 >
                     <Icon name="invite-heart" size={iconSize} />
                 </Tooltip>
             )}
  • Confirm invitedUsernamesSet is always a Set at runtime in authContext.
  • Verify the Icon mapping includes "invite-heart" (intentional naming). Based on learnings.

Also applies to: 62-87, 98-106

src/components/Notifications/SetupNotificationsModal.tsx (1)

14-33: Good flow; consider disabling CTA during async request

Prevents double-clicks while awaiting browser dialog.

Example:

+    const [pending, setPending] = useState(false)
     const handleAllowClick = async (e?: React.MouseEvent) => {
         e?.preventDefault()
         e?.stopPropagation()
         try {
+            setPending(true)
             await requestPermission()
             hidePermissionModalImmediate()
             await afterPermissionAttempt()
         } catch (error) {
             console.error('Error requesting permission:', error)
             hidePermissionModalImmediate()
-        }
+        } finally { setPending(false) }
     }
...
                 ctas={[
                     {
                         text: 'Enable notifications',
-                        onClick: handleAllowClick,
+                        onClick: pending ? undefined : handleAllowClick,
+                        disabled: pending,

Also applies to: 45-69

src/components/Profile/index.tsx (2)

34-35: Guard invite link/code when username is missing

Empty username yields odd codes. Consider disabling modal/share until username is present.

Example:

-    const inviteData = generateInviteCodeLink(user?.user.username ?? '')
+    const uname = user?.user.username
+    const inviteData = uname ? generateInviteCodeLink(uname) : null

Then conditionally render invite content when inviteData exists.


21-23: Unreachable “You’re already verified” modal

It’s never opened in this component. Remove or wire a trigger, e.g., show when user taps Identity Verification and is already verified.

Also applies to: 134-148

src/components/LandingPage/dropLink.tsx (3)

21-22: Copy nit: missing “is”

Consider “Paying is as easy as a text.” for correct grammar.


113-120: Use Next.js Link for internal nav and avoid new tab

Opening /setup in a new tab can drop SPA context and feels odd for internal nav. Prefer Link and same-tab navigation.

Apply this diff:

-                    <a href="/setup" target="_blank" rel="noopener noreferrer">
-                        <Button
+                    <Link href="/setup">
+                        <Button
                             shadowSize="4"
                             className="mt-8 hidden w-58 bg-white px-7 pb-11 pt-4 text-base font-extrabold hover:bg-white/90 md:inline-block md:w-72 md:px-10 md:text-lg"
                         >
                             JOIN WAITLIST
                         </Button>
-                    </a>
+                    </Link>

And add import at top:

+import Link from 'next/link'

24-106: Reduce duplication by extracting a FloatingIcon component

The four near-identical animated blocks (mobile and desktop) can be a small reusable component taking src/alt/positions/animation props to simplify and reduce render cost from recreating inline objects per render.

Also applies to: 123-197

src/components/Setup/Views/CollectEmail.tsx (1)

16-17: Naming consistency: setIsLoading

Use setIsLoading for consistency with React conventions.

Also applies to: 74-83

src/app/(mobile-ui)/layout.tsx (1)

24-25: Tighten public path regex

Consider anchoring each alternative and removing inner $ for clarity and to avoid unintended matches.

Example:

const publicPathRegex = /^(?:\/request\/pay|\/claim(?:\/.*)?|\/pay\/.+|\/support|\/invite(?:\/.*)?)$/
src/components/Setup/Views/JoinWaitlist.tsx (1)

21-22: Clarify “loading” state name

isLoading here represents input validation in progress; consider renaming to isValidating for clarity.

Also applies to: 29-45

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

146-191: Remove duplicate balance warning effect

There are two identical effects; keep one and delete the duplicate to avoid redundant state toggles.

Apply this diff:

-    // effect for showing balance warning modal
-    useEffect(() => {
-        if (isFetchingBalance || balance === undefined || !user) return
-
-        if (typeof window !== 'undefined') {
-            const hasSeenBalanceWarning = getFromLocalStorage(`${user!.user.userId}-hasSeenBalanceWarning`)
-            const balanceInUsd = Number(formatUnits(balance, PEANUT_WALLET_TOKEN_DECIMALS))
-
-            // show if:
-            // 1. balance is above the threshold
-            // 2. user hasn't seen this warning in the current session
-            // 3. no other modals are currently active
-            if (
-                balanceInUsd > BALANCE_WARNING_THRESHOLD &&
-                !hasSeenBalanceWarning &&
-                !showIOSPWAInstallModal &&
-                !showAddMoneyPromptModal
-            ) {
-                setShowBalanceWarningModal(true)
-            }
-        }
-    }, [balance, isFetchingBalance, showIOSPWAInstallModal, showAddMoneyPromptModal, user])
src/components/og/InviteCardOG.tsx (1)

24-25: Clamp scribble width to avoid overflow on very long usernames.

Current estimate can exceed the content width for long names.

Apply:

-    const scribbleWidth = usernamePxWidth(username)
+    const scribbleMaxWidth = 1200 - 2 * 48 // card width - horizontal padding
+    const scribbleWidth = Math.min(scribbleMaxWidth, usernamePxWidth(username))
src/components/Invites/InvitesPage.tsx (1)

73-76: Consolidate conflicting Tailwind classes for clarity.

twMerge resolves justify-between vs justify-center, but it’s hard to read. Merge into one final intent.

src/components/Setup/Views/SetupPasskey.tsx (1)

38-46: Consider clearing inviteCode cookie after consuming it.

To avoid stale codes affecting later sessions and reduce retention, remove the cookie once used (post‑account add).

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

75-76: Hide decorative animation from assistive tech.

Use empty alt and aria-hidden for the background animation.

Apply:

-                        <Image src={chillPeanutAnim.src} alt="Peanut Man" className="object-contain" fill />
+                        <Image src={chillPeanutAnim.src} alt="" aria-hidden className="object-contain" fill />

Based on learnings

src/components/Claim/Link/Initial.view.tsx (1)

265-283: Complete useCallback deps

Add fetchUser to avoid stale closure if context updates.

         ],
-        ]
+        fetchUser]
src/components/Payment/PaymentForm/index.tsx (1)

322-346: Normalize invite code casing and fetch user in background

  • Use uppercase to match other flows and avoid server mismatch.
  • Background refresh avoids blocking UX (fetchUser doesn’t need to be awaited). Based on learnings.
-            const inviteCode = `${recipient?.identifier}INVITESYOU`
+            const inviteCode = `${String(recipient?.identifier).toUpperCase()}INVITESYOU`
@@
-            await fetchUser()
+            // refresh in background; page navigation doesn't depend on fresh user state
+            void fetchUser()

Also, please confirm that EInviteType.PAYMENT_LINK is the correct type for this payment-triggered acceptance (vs. a user invite type).

src/components/Invites/JoinWaitlistPage.tsx (2)

116-124: Guard action when inviteType is absent

Prevent submit if inviteType is undefined.

-                                disabled={!isValid || isChanging || isLoading}
+                                disabled={!isValid || isChanging || isLoading || !inviteType}

20-24: Nit: setter naming consistency

Prefer setIsLoading / setIsLoggingOut for readability.

-    const [isLoading, setisLoading] = useState(false)
+    const [isLoading, setIsLoading] = useState(false)
@@
-    const [isLoggingOut, setisLoggingOut] = useState(false)
+    const [isLoggingOut, setIsLoggingOut] = useState(false)

Also applies to: 54-55

src/app/(setup)/setup/page.tsx (1)

121-127: Guard against missing signup step index

If 'signup' isn’t in steps, findIndex returns -1; ensure a safe fallback.

-                const signupScreenIndex = steps.findIndex((s: ISetupStep) => s.screenId === 'signup')
-                dispatch(setupActions.setStep(signupScreenIndex + 1))
+                const signupScreenIndex = steps.findIndex((s: ISetupStep) => s.screenId === 'signup')
+                dispatch(setupActions.setStep(signupScreenIndex > -1 ? signupScreenIndex + 1 : 1))
src/hooks/useNotifications.ts (1)

347-395: Avoid duplicate banner timers

snoozeReminderBanner and closePermissionModal both call setTimeout without guarding against component unmount or multiple outstanding timers when evaluateVisibility re-schedules quickly (e.g. after permissionChange). We already clear bannerTimerRef.current at cleanup, but we should also clear before scheduling inside evaluateVisibility to avoid multiple timers. Right now if evaluateVisibility runs twice with bannerShowAt > now, we stack timers, leading to repeated execution and flicker. Consider extracting a helper that clears existing timeout before setting a new one.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 255dcdd and 51000ef.

⛔ Files ignored due to path filters (7)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • public/arrows/top-right-arrow-2.svg is excluded by !**/*.svg
  • src/assets/icons/fbmessenger.svg is excluded by !**/*.svg
  • src/assets/icons/imessage.svg is excluded by !**/*.svg
  • src/assets/icons/starStraight.svg is excluded by !**/*.svg
  • src/assets/icons/whatsapp.svg is excluded by !**/*.svg
  • src/assets/illustrations/buttery_smooth_global_money.svg is excluded by !**/*.svg
📒 Files selected for processing (92)
  • .env.example (1 hunks)
  • package.json (1 hunks)
  • public/onesignal/OneSignalSDKWorker.js (1 hunks)
  • src/app/(mobile-ui)/home/page.tsx (5 hunks)
  • src/app/(mobile-ui)/layout.tsx (3 hunks)
  • src/app/(mobile-ui)/notifications/page.tsx (1 hunks)
  • src/app/(mobile-ui)/points/page.tsx (1 hunks)
  • src/app/(setup)/layout.tsx (2 hunks)
  • src/app/(setup)/setup/page.tsx (3 hunks)
  • src/app/[...recipient]/client.tsx (2 hunks)
  • src/app/[...recipient]/payment-layout-wrapper.tsx (2 hunks)
  • src/app/actions/invites.ts (1 hunks)
  • src/app/api/og/route.tsx (2 hunks)
  • src/app/invite/page.tsx (1 hunks)
  • src/app/page.tsx (3 hunks)
  • src/assets/icons/index.ts (1 hunks)
  • src/components/0_Bruddle/Toast.tsx (1 hunks)
  • src/components/Claim/Link/Initial.view.tsx (4 hunks)
  • src/components/Common/ActionList.tsx (7 hunks)
  • src/components/Common/ActionListDaimoPayButton.tsx (4 hunks)
  • src/components/Common/SavedAccountsView.tsx (1 hunks)
  • src/components/Global/ActionModal/index.tsx (6 hunks)
  • src/components/Global/Banner/MaintenanceBanner.tsx (1 hunks)
  • src/components/Global/Banner/index.tsx (2 hunks)
  • src/components/Global/ConfirmInviteModal/index.tsx (1 hunks)
  • src/components/Global/CopyToClipboard/index.tsx (1 hunks)
  • src/components/Global/DirectSendQR/index.tsx (2 hunks)
  • src/components/Global/EarlyUserModal/index.tsx (1 hunks)
  • src/components/Global/GuestLoginCta/index.tsx (1 hunks)
  • src/components/Global/Icons/Icon.tsx (5 hunks)
  • src/components/Global/Icons/bell.tsx (1 hunks)
  • src/components/Global/Icons/invite-heart.tsx (1 hunks)
  • src/components/Global/Icons/lock.tsx (1 hunks)
  • src/components/Global/Icons/trophy.tsx (1 hunks)
  • src/components/Global/NoMoreJailModal/index.tsx (1 hunks)
  • src/components/Global/ShareButton/index.tsx (2 hunks)
  • src/components/Global/UnderMaintenance/index.tsx (0 hunks)
  • src/components/Global/WalletNavigation/index.tsx (2 hunks)
  • src/components/Home/HomeBanners/BannerCard.tsx (1 hunks)
  • src/components/Home/HomeBanners/index.tsx (1 hunks)
  • src/components/Home/HomeHistory.tsx (1 hunks)
  • src/components/Home/InvitesIcon.tsx (1 hunks)
  • src/components/Invites/InvitesPage.tsx (1 hunks)
  • src/components/Invites/InvitesPageLayout.tsx (1 hunks)
  • src/components/Invites/JoinWaitlistPage.tsx (1 hunks)
  • src/components/LandingPage/RegulatedRails.tsx (2 hunks)
  • src/components/LandingPage/dropLink.tsx (2 hunks)
  • src/components/LandingPage/hero.tsx (1 hunks)
  • src/components/LandingPage/securityBuiltIn.tsx (1 hunks)
  • src/components/LandingPage/sendInSeconds.tsx (1 hunks)
  • src/components/LandingPage/yourMoney.tsx (2 hunks)
  • src/components/Notifications/NotificationBanner.tsx (1 hunks)
  • src/components/Notifications/NotificationNavigation.tsx (1 hunks)
  • src/components/Notifications/SetupNotificationsModal.tsx (1 hunks)
  • src/components/Payment/PaymentForm/index.tsx (8 hunks)
  • src/components/Profile/components/ProfileHeader.tsx (1 hunks)
  • src/components/Profile/components/PublicProfile.tsx (5 hunks)
  • src/components/Profile/index.tsx (5 hunks)
  • src/components/SearchUsers/SearchResults.tsx (2 hunks)
  • src/components/SearchUsers/index.tsx (0 hunks)
  • src/components/Setup/Setup.consts.tsx (3 hunks)
  • src/components/Setup/Setup.types.ts (2 hunks)
  • src/components/Setup/Views/CollectEmail.tsx (1 hunks)
  • src/components/Setup/Views/JoinWaitlist.tsx (1 hunks)
  • src/components/Setup/Views/SetupPasskey.tsx (3 hunks)
  • src/components/Setup/Views/Welcome.tsx (1 hunks)
  • src/components/Setup/Views/index.ts (1 hunks)
  • src/components/TransactionDetails/TransactionCard.tsx (1 hunks)
  • src/components/TransactionDetails/TransactionDetailsHeaderCard.tsx (1 hunks)
  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx (4 hunks)
  • src/components/User/UserCard.tsx (1 hunks)
  • src/components/UserHeader/index.tsx (6 hunks)
  • src/components/index.ts (0 hunks)
  • src/components/og/InviteCardOG.tsx (1 hunks)
  • src/config/routesUnderMaintenance.ts (0 hunks)
  • src/config/underMaintenance.config.ts (1 hunks)
  • src/context/authContext.tsx (4 hunks)
  • src/hooks/useBanners.tsx (2 hunks)
  • src/hooks/useLogin.tsx (1 hunks)
  • src/hooks/useNotifications.ts (1 hunks)
  • src/hooks/useZeroDev.ts (3 hunks)
  • src/interfaces/interfaces.ts (3 hunks)
  • src/middleware.ts (1 hunks)
  • src/redux/slices/setup-slice.ts (4 hunks)
  • src/redux/types/setup.types.ts (2 hunks)
  • src/services/invites.ts (1 hunks)
  • src/services/notifications.ts (1 hunks)
  • src/services/services.types.ts (2 hunks)
  • src/utils/general.utils.ts (2 hunks)
  • src/utils/history.utils.ts (1 hunks)
  • src/utils/sentry.utils.ts (4 hunks)
  • tailwind.config.js (3 hunks)
💤 Files with no reviewable changes (4)
  • src/components/index.ts
  • src/config/routesUnderMaintenance.ts
  • src/components/SearchUsers/index.tsx
  • src/components/Global/UnderMaintenance/index.tsx
🧰 Additional context used
🧠 Learnings (7)
📚 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/components/Global/Icons/invite-heart.tsx
  • src/app/(mobile-ui)/points/page.tsx
  • src/components/Home/InvitesIcon.tsx
  • src/components/UserHeader/index.tsx
📚 Learning: 2025-08-19T09:08:16.945Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1107
File: src/components/LandingPage/hero.tsx:160-160
Timestamp: 2025-08-19T09:08:16.945Z
Learning: In the Hero component at src/components/LandingPage/hero.tsx, the team prefers to keep the main heading as h2 (not h1) and does not want a heading prop parameter - the heading content should remain hardcoded in the component.

Applied to files:

  • src/components/LandingPage/hero.tsx
📚 Learning: 2025-09-29T18:34:33.596Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1251
File: src/components/Invites/JoinWaitlistPage.tsx:41-55
Timestamp: 2025-09-29T18:34:33.596Z
Learning: In the JoinWaitlistPage component, after successfully accepting an invite via invitesApi.acceptInvite(), calling fetchUser() is sufficient to update the user state and automatically display the app. No manual navigation to /home or other pages is required since the user is already on the home page and the app will be displayed once user.hasAppAccess is updated.

Applied to files:

  • src/components/Invites/JoinWaitlistPage.tsx
  • src/app/(mobile-ui)/layout.tsx
  • src/components/Claim/Link/Initial.view.tsx
📚 Learning: 2025-07-24T10:57:15.315Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1000
File: src/components/og/ProfileCardOG.tsx:0-0
Timestamp: 2025-07-24T10:57:15.315Z
Learning: In `src/components/og/ProfileCardOG.tsx`, the scribble image should have an empty alt attribute (alt="") to prevent layout issues if the image fails to load. Since it's a decorative element positioned absolutely over the username text, showing alt text would interfere with the layout and username display.

Applied to files:

  • src/components/og/InviteCardOG.tsx
📚 Learning: 2025-08-26T15:25:53.328Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1132
File: src/app/[...recipient]/client.tsx:394-397
Timestamp: 2025-08-26T15:25:53.328Z
Learning: In `src/components/Common/ActionListDaimoPayButton.tsx`, the `handleCompleteDaimoPayment` function should not display error messages to users when DB update fails because the Daimo payment itself has succeeded - showing errors would be confusing since the payment was successful.

Applied to files:

  • src/components/Common/ActionListDaimoPayButton.tsx
📚 Learning: 2025-09-25T11:18:10.633Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1251
File: src/app/invite/page.tsx:44-53
Timestamp: 2025-09-25T11:18:10.633Z
Learning: The setup page (/setup) contains the waitlist functionality in the welcome step using the JoinWaitlist component, so redirecting users to /setup for joining the waitlist is correct.

Applied to files:

  • src/components/Setup/Views/JoinWaitlist.tsx
📚 Learning: 2025-06-18T19:56:55.443Z
Learnt from: jjramirezn
PR: peanutprotocol/peanut-ui#919
File: src/components/Withdraw/views/Initial.withdraw.view.tsx:87-87
Timestamp: 2025-06-18T19:56:55.443Z
Learning: In withdraw flows for Peanut Wallet, the PeanutActionDetailsCard should always display "USDC" as the token symbol because it shows the amount being withdrawn from the Peanut Wallet (which holds USDC), regardless of the destination token/chain selected by the user. The TokenSelector is used for choosing the withdrawal destination, not the source display.

Applied to files:

  • src/components/TransactionDetails/TransactionDetailsReceipt.tsx
🧬 Code graph analysis (51)
src/components/Notifications/NotificationNavigation.tsx (2)
src/services/notifications.ts (1)
  • notificationsApi (20-69)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (206-215)
src/components/Global/DirectSendQR/index.tsx (1)
src/components/Global/Icons/Icon.tsx (1)
  • IconName (68-132)
src/components/Global/ConfirmInviteModal/index.tsx (1)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/components/Global/EarlyUserModal/index.tsx (3)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/utils/general.utils.ts (2)
  • generateInviteCodeLink (1336-1340)
  • generateInvitesShareText (1332-1334)
src/app/actions/users.ts (1)
  • updateUserById (12-35)
src/components/Global/Icons/Icon.tsx (4)
src/components/Global/Icons/bell.tsx (1)
  • BellIcon (3-10)
src/components/Global/Icons/trophy.tsx (1)
  • TrophyIcon (3-12)
src/components/Global/Icons/invite-heart.tsx (1)
  • InviteHeartIcon (3-32)
src/components/Global/Icons/lock.tsx (1)
  • LockIcon (3-19)
src/app/[...recipient]/payment-layout-wrapper.tsx (1)
src/components/Global/Banner/index.tsx (1)
  • Banner (11-25)
src/components/Profile/components/PublicProfile.tsx (2)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (206-215)
src/services/notifications.ts (1)
src/constants/general.consts.ts (1)
  • PEANUT_API_URL (43-47)
src/app/actions/invites.ts (2)
src/constants/general.consts.ts (1)
  • PEANUT_API_URL (43-47)
src/utils/sentry.utils.ts (1)
  • fetchWithSentry (36-150)
src/components/Home/HomeBanners/BannerCard.tsx (1)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (206-215)
src/components/Global/NoMoreJailModal/index.tsx (1)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/hooks/useZeroDev.ts (3)
src/redux/hooks.ts (1)
  • useSetupStore (9-9)
src/utils/general.utils.ts (2)
  • getFromCookie (160-186)
  • removeFromCookie (188-198)
src/services/invites.ts (1)
  • invitesApi (7-83)
src/hooks/useBanners.tsx (2)
src/hooks/useNotifications.ts (1)
  • useNotifications (22-416)
src/hooks/useKycStatus.tsx (1)
  • useKycStatus (12-30)
src/components/Invites/JoinWaitlistPage.tsx (6)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/redux/hooks.ts (1)
  • useSetupStore (9-9)
src/services/invites.ts (1)
  • invitesApi (7-83)
src/app/actions/invites.ts (1)
  • validateInviteCode (8-43)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/components/Common/SavedAccountsView.tsx (1)
src/utils/general.utils.ts (1)
  • formatIban (962-969)
src/components/Global/WalletNavigation/index.tsx (1)
src/components/Global/DirectSendQR/index.tsx (1)
  • DirectSendQr (172-480)
src/app/(mobile-ui)/home/page.tsx (3)
src/hooks/useNotifications.ts (1)
  • useNotifications (22-416)
src/components/Notifications/NotificationNavigation.tsx (1)
  • NotificationNavigation (9-47)
src/components/Notifications/SetupNotificationsModal.tsx (1)
  • SetupNotificationsModal (5-71)
src/components/Home/HomeHistory.tsx (1)
src/components/Global/EmptyStates/EmptyState.tsx (1)
  • EmptyState (13-28)
src/context/authContext.tsx (1)
src/hooks/query/user.ts (1)
  • useUserQuery (10-51)
src/services/services.types.ts (1)
src/utils/bridge-accounts.utils.ts (1)
  • BridgeKycStatus (34-34)
src/components/Global/Banner/MaintenanceBanner.tsx (1)
src/components/Global/Banner/index.tsx (1)
  • MaintenanceBanner (47-47)
src/app/(mobile-ui)/layout.tsx (2)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/components/Global/Banner/index.tsx (1)
  • Banner (11-25)
src/app/(mobile-ui)/points/page.tsx (8)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/services/invites.ts (1)
  • invitesApi (7-83)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (206-215)
src/utils/general.utils.ts (1)
  • generateInvitesShareText (1332-1334)
src/services/services.types.ts (1)
  • Invite (399-408)
src/components/Global/Card/index.tsx (1)
  • getCardPosition (14-19)
src/components/UserHeader/index.tsx (1)
  • VerifiedUserLabel (45-109)
src/app/[...recipient]/client.tsx (1)
src/redux/slices/payment-slice.ts (1)
  • paymentActions (77-77)
src/components/Common/ActionListDaimoPayButton.tsx (3)
src/components/Global/DaimoPayButton/index.tsx (1)
  • DaimoPayButton (51-200)
src/redux/slices/payment-slice.ts (1)
  • paymentActions (77-77)
src/components/SearchUsers/SearchResultCard.tsx (1)
  • SearchResultCard (19-70)
src/app/invite/page.tsx (3)
src/app/actions/invites.ts (1)
  • validateInviteCode (8-43)
src/lib/hosting/get-origin.ts (1)
  • getOrigin (3-16)
src/components/Invites/InvitesPage.tsx (1)
  • InvitesPage (101-107)
src/app/api/og/route.tsx (1)
src/components/og/InviteCardOG.tsx (1)
  • InviteCardOG (6-142)
src/components/Setup/Views/JoinWaitlist.tsx (6)
src/components/0_Bruddle/Toast.tsx (1)
  • useToast (111-117)
src/hooks/useSetupFlow.ts (1)
  • useSetupFlow (6-68)
src/redux/hooks.ts (1)
  • useAppDispatch (5-5)
src/hooks/useLogin.tsx (1)
  • useLogin (23-52)
src/services/invites.ts (1)
  • invitesApi (7-83)
src/redux/slices/setup-slice.ts (1)
  • setupActions (65-65)
src/components/Common/ActionList.tsx (4)
src/constants/actionlist.consts.ts (1)
  • PaymentMethod (5-11)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/redux/hooks.ts (1)
  • useAppDispatch (5-5)
src/redux/slices/setup-slice.ts (1)
  • setupActions (65-65)
src/components/TransactionDetails/TransactionDetailsReceipt.tsx (2)
src/constants/zerodev.consts.ts (2)
  • PEANUT_WALLET_TOKEN_SYMBOL (21-21)
  • PEANUT_WALLET_CHAIN (18-18)
src/components/Payment/PaymentInfoRow.tsx (1)
  • PaymentInfoRow (17-83)
src/components/Setup/Views/Welcome.tsx (1)
src/utils/general.utils.ts (1)
  • sanitizeRedirectURL (1217-1238)
src/components/Invites/InvitesPage.tsx (6)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/redux/hooks.ts (1)
  • useAppDispatch (5-5)
src/hooks/useLogin.tsx (1)
  • useLogin (23-52)
src/utils/general.utils.ts (1)
  • saveToCookie (136-158)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/components/Notifications/SetupNotificationsModal.tsx (1)
src/hooks/useNotifications.ts (1)
  • useNotifications (22-416)
src/components/Home/HomeBanners/index.tsx (2)
src/hooks/useBanners.tsx (1)
  • useBanners (21-76)
src/components/Global/Icons/Icon.tsx (1)
  • IconName (68-132)
src/components/Global/CopyToClipboard/index.tsx (1)
src/components/0_Bruddle/Button.tsx (2)
  • ButtonSize (17-17)
  • Button (76-267)
src/components/Setup/Views/CollectEmail.tsx (3)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/app/actions/users.ts (1)
  • updateUserById (12-35)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/hooks/useLogin.tsx (3)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/hooks/useZeroDev.ts (1)
  • useZeroDev (36-187)
src/utils/general.utils.ts (2)
  • getFromLocalStorage (112-134)
  • getValidRedirectUrl (1342-1358)
src/components/UserHeader/index.tsx (3)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/components/Tooltip/index.tsx (1)
  • Tooltip (18-106)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (206-215)
src/app/(setup)/layout.tsx (2)
src/components/Global/Banner/index.tsx (1)
  • Banner (11-25)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/app/page.tsx (2)
src/components/LandingPage/dropLink.tsx (1)
  • DropLink (12-222)
src/components/LandingPage/noFees.tsx (1)
  • NoFees (12-135)
src/components/Global/Banner/index.tsx (1)
src/components/Global/Banner/MaintenanceBanner.tsx (1)
  • MaintenanceBanner (3-5)
src/hooks/useNotifications.ts (2)
src/redux/hooks.ts (1)
  • useUserStore (13-13)
src/utils/general.utils.ts (2)
  • getFromLocalStorage (112-134)
  • saveToLocalStorage (94-110)
src/components/Claim/Link/Initial.view.tsx (3)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/services/invites.ts (1)
  • invitesApi (7-83)
src/components/Common/ActionList.tsx (1)
  • ActionList (51-311)
src/app/(mobile-ui)/notifications/page.tsx (6)
src/services/notifications.ts (2)
  • InAppItem (4-16)
  • notificationsApi (20-69)
src/utils/dateGrouping.utils.ts (3)
  • getDateGroup (69-93)
  • getDateGroupKey (128-148)
  • formatGroupHeaderDate (102-120)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/components/Global/EmptyStates/EmptyState.tsx (1)
  • EmptyState (13-28)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/components/Global/Card/index.tsx (1)
  • CardPosition (4-4)
src/components/Notifications/NotificationBanner.tsx (1)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (206-215)
src/components/Payment/PaymentForm/index.tsx (3)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/utils/general.utils.ts (1)
  • formatCurrency (382-384)
src/services/invites.ts (1)
  • invitesApi (7-83)
src/components/Setup/Views/SetupPasskey.tsx (3)
src/redux/hooks.ts (2)
  • useAppDispatch (5-5)
  • useSetupStore (9-9)
src/hooks/useSetupFlow.ts (1)
  • useSetupFlow (6-68)
src/utils/general.utils.ts (2)
  • getFromCookie (160-186)
  • getValidRedirectUrl (1342-1358)
src/components/Profile/index.tsx (2)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/utils/general.utils.ts (2)
  • generateInviteCodeLink (1336-1340)
  • generateInvitesShareText (1332-1334)
src/components/LandingPage/dropLink.tsx (1)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/services/invites.ts (4)
src/utils/sentry.utils.ts (1)
  • fetchWithSentry (36-150)
src/constants/general.consts.ts (1)
  • PEANUT_API_URL (43-47)
src/services/services.types.ts (1)
  • Invite (399-408)
src/app/actions/invites.ts (1)
  • validateInviteCode (8-43)
src/app/(setup)/setup/page.tsx (4)
src/redux/hooks.ts (1)
  • useSetupStore (9-9)
src/utils/general.utils.ts (1)
  • getFromCookie (160-186)
src/components/Setup/Setup.types.ts (1)
  • ISetupStep (41-53)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
🪛 Biome (2.1.2)
src/hooks/useNotifications.ts

[error] 113-113: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 351-351: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 374-374: This hook is being called conditionally, but all hooks must be called in the exact same order in every component render.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)


[error] 385-385: This hook is being called from a nested function, but all hooks must be called unconditionally from the top-level component.

For React to preserve state between calls, hooks needs to be called unconditionally and always in the same order.
See https://reactjs.org/docs/hooks-rules.html#only-call-hooks-at-the-top-level

(lint/correctness/useHookAtTopLevel)

🪛 dotenv-linter (3.3.0)
.env.example

[warning] 57-57: [UnorderedKey] The NEXT_PUBLIC_FETCH_TIMEOUT_MS key should go before the NEXT_PUBLIC_VAPID_PUBLIC_KEY key

(UnorderedKey)


[warning] 62-62: [UnorderedKey] The NEXT_PUBLIC_ONESIGNAL_WEBHOOK key should go before the NEXT_PUBLIC_SAFARI_WEB_ID key

(UnorderedKey)

Comment on lines +167 to +194
<Link
href={href ?? ''}
className="flex w-full items-center gap-3"
data-notification-id={notif.id}
onClick={() => handleNotificationClick(notif.id)}
>
<Image
src={notif.iconUrl ?? PEANUTMAN_LOGO}
alt="icon"
width={32}
height={32}
className="size-8 min-w-8 self-center"
/>

<div className="flex min-w-0 flex-col">
<div className="flex items-center gap-2">
<div className="line-clamp-2 font-semibold">
{notif.title}
</div>
</div>
{notif.body ? (
<div className="line-clamp-2 text-sm text-gray-600">
{notif.body}
</div>
) : null}
</div>
</Link>
{!notif.state.readAt ? (
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against empty Link href to avoid runtime/navigation issues.

Render a button/plain container when there’s no deeplink.

Apply:

-            <Link
-                href={href ?? ''}
-                className="flex w-full items-center gap-3"
-                data-notification-id={notif.id}
-                onClick={() => handleNotificationClick(notif.id)}
-            >
+            {href ? (
+                <Link
+                    href={href}
+                    className="flex w-full items-center gap-3"
+                    data-notification-id={notif.id}
+                    onClick={() => handleNotificationClick(notif.id)}
+                >
+                    {/* content */}
+                    <Image
+                        src={notif.iconUrl ?? PEANUTMAN_LOGO}
+                        alt="notification icon"
+                        width={32}
+                        height={32}
+                        className="size-8 min-w-8 self-center"
+                    />
+
+                    <div className="flex min-w-0 flex-col">
+                        <div className="flex items-center gap-2">
+                            <div className="line-clamp-2 font-semibold">{notif.title}</div>
+                        </div>
+                        {notif.body ? <div className="line-clamp-2 text-sm text-gray-600">{notif.body}</div> : null}
+                    </div>
+                </Link>
+            ) : (
+                <button
+                    type="button"
+                    className="flex w-full items-center gap-3 text-left"
+                    data-notification-id={notif.id}
+                    onClick={() => handleNotificationClick(notif.id)}
+                >
+                    <Image
+                        src={notif.iconUrl ?? PEANUTMAN_LOGO}
+                        alt="notification icon"
+                        width={32}
+                        height={32}
+                        className="size-8 min-w-8 self-center"
+                    />
+                    <div className="flex min-w-0 flex-col">
+                        <div className="flex items-center gap-2">
+                            <div className="line-clamp-2 font-semibold">{notif.title}</div>
+                        </div>
+                        {notif.body ? <div className="line-clamp-2 text-sm text-gray-600">{notif.body}</div> : null}
+                    </div>
+                </button>
+            )}

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

🤖 Prompt for AI Agents
In src/app/(mobile-ui)/notifications/page.tsx around lines 167 to 194, the
current Link uses href={href ?? ''} which can produce an empty href and cause
navigation/runtime issues; change the JSX so you render a Next.js Link only when
href is a non-empty string, and otherwise render a non-navigating element (e.g.,
a <div> or <button>) with the exact same classes, data-notification-id and
onClick handler so clicking still triggers handleNotificationClick; ensure the
non-link element remains keyboard accessible (add role="button" and tabIndex={0}
and handle onKeyDown for Enter/Space) and keep the Image and children
markup/styles identical.

Comment on lines +33 to +36
useEffect(() => {
// Re-fetch user to get the latest invitees list for showing heart Icon
fetchUser()
}, [])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add fetchUser to useEffect dependencies or verify stability.

Line 35 calls fetchUser() in a useEffect without including it in the dependency array. If fetchUser is not memoized or stable across renders, this could cause stale closures or unexpected behavior.

Verify whether fetchUser from useAuth is stable (memoized with useCallback). If not, apply this diff:

     useEffect(() => {
         // Re-fetch user to get the latest invitees list for showing heart Icon
         fetchUser()
-    }, [])
+    }, [fetchUser])

Alternatively, if the intent is to run this only once on mount and fetchUser stability is guaranteed by the auth context, add an ESLint disable comment with justification:

     useEffect(() => {
         // Re-fetch user to get the latest invitees list for showing heart Icon
         fetchUser()
+        // eslint-disable-next-line react-hooks/exhaustive-deps
     }, [])
📝 Committable suggestion

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

Suggested change
useEffect(() => {
// Re-fetch user to get the latest invitees list for showing heart Icon
fetchUser()
}, [])
useEffect(() => {
// Re-fetch user to get the latest invitees list for showing heart Icon
fetchUser()
}, [fetchUser])
🤖 Prompt for AI Agents
In src/app/(mobile-ui)/points/page.tsx around lines 33 to 36, useEffect calls
fetchUser() but does not include fetchUser in the dependency array; ensure
fetchUser is stable or update the code: either memoize fetchUser in the useAuth
implementation (wrap with useCallback so its identity is stable) and then add it
to the dependency array, or include fetchUser in the dependency array here so
React tracks changes, or if the intent is to run only once on mount and you can
guarantee fetchUser is stable from the auth context, add a one-line ESLint
disable comment (with a short justification) above the useEffect to suppress the
missing-deps warning.

Comment on lines +413 to +418
// reset payment state on unmount
useEffect(() => {
return () => {
dispatch(paymentActions.resetPaymentState())
}
}, [])
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Verify cleanup doesn't conflict with existing reset logic.

This cleanup effect resets payment state on every unmount, but lines 269-274 already reset payment state when chargeId is absent. This could lead to redundant resets or race conditions.

Additionally, dispatch is missing from the dependency array. While Redux dispatch functions are typically stable, it's better practice to include all dependencies or explicitly document why they're omitted.

Consider one of these approaches:

Option 1: Remove this effect if the existing reset logic (lines 269-274) is sufficient

-    // reset payment state on unmount
-    useEffect(() => {
-        return () => {
-            dispatch(paymentActions.resetPaymentState())
-        }
-    }, [])
-

Option 2: If this cleanup is necessary, add dispatch to dependencies and document why both resets are needed

     // reset payment state on unmount
     useEffect(() => {
         return () => {
             dispatch(paymentActions.resetPaymentState())
         }
-    }, [])
+    }, [dispatch])
📝 Committable suggestion

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

Suggested change
// reset payment state on unmount
useEffect(() => {
return () => {
dispatch(paymentActions.resetPaymentState())
}
}, [])
Suggested change
// reset payment state on unmount
useEffect(() => {
return () => {
dispatch(paymentActions.resetPaymentState())
}
}, [])
// reset payment state on unmount
useEffect(() => {
return () => {
dispatch(paymentActions.resetPaymentState())
}
}, [dispatch])
🤖 Prompt for AI Agents
In src/app/[...recipient]/client.tsx around lines 413-418 (and noting existing
reset logic at lines 269-274), the cleanup effect unconditionally resets payment
state on unmount and omits dispatch from the dependency array; reconcile this by
either removing the cleanup effect if the lines 269-274 reset already cover
required scenarios, or keep the cleanup but add dispatch to the dependency array
and a comment explaining why both the in-render reset and the unmount cleanup
are necessary to avoid redundant resets/race conditions; update code accordingly
and ensure the dependency list and comments clearly document the chosen
approach.

Comment on lines +32 to 33
state.inviteCode = ''
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

resetSetup doesn’t reset inviteType

Leaving inviteType unchanged can leak state between sessions.

Apply:

         state.steps = []
         state.inviteCode = ''
+        state.inviteType = EInviteType.DIRECT
📝 Committable suggestion

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

Suggested change
state.inviteCode = ''
},
state.steps = []
state.inviteCode = ''
state.inviteType = EInviteType.DIRECT
},
🤖 Prompt for AI Agents
In src/redux/slices/setup-slice.ts around lines 32-33, the resetSetup reducer
clears state.inviteCode but leaves state.inviteType intact, which can leak state
between sessions; update the reducer to also reset inviteType to its initial
value (e.g., set state.inviteType = initialState.inviteType or the default value
used when the slice is created) so the entire setup state is returned to the
initial/default state.

Comment on lines +21 to +44
async list(params: { limit?: number; cursor?: string | null; filter?: 'all' | 'unread'; category?: string } = {}) {
const { limit = 20, cursor, filter = 'all', category } = params
const search = new URLSearchParams()
search.set('limit', String(limit))
search.set('filter', filter)
if (cursor) search.set('cursor', cursor)
if (category) search.set('category', category)

const token = Cookies.get('jwt-token')
const url = `${PEANUT_API_URL}/notifications?${search.toString()}`

try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
})
if (!response.ok) throw new Error('failed to fetch notifications')
return (await response.json()) as ListResponse
} catch (e) {
throw e
}
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle missing JWT token.

If Cookies.get('jwt-token') returns undefined, the Authorization header will be 'Bearer undefined', which will cause authentication failures with potentially confusing error messages.

Apply this diff to fail fast with a clear error:

         const token = Cookies.get('jwt-token')
+        if (!token) {
+            throw new Error('Authentication required: JWT token not found')
+        }
         const url = `${PEANUT_API_URL}/notifications?${search.toString()}`
📝 Committable suggestion

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

Suggested change
async list(params: { limit?: number; cursor?: string | null; filter?: 'all' | 'unread'; category?: string } = {}) {
const { limit = 20, cursor, filter = 'all', category } = params
const search = new URLSearchParams()
search.set('limit', String(limit))
search.set('filter', filter)
if (cursor) search.set('cursor', cursor)
if (category) search.set('category', category)
const token = Cookies.get('jwt-token')
const url = `${PEANUT_API_URL}/notifications?${search.toString()}`
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
})
if (!response.ok) throw new Error('failed to fetch notifications')
return (await response.json()) as ListResponse
} catch (e) {
throw e
}
},
async list(params: { limit?: number; cursor?: string | null; filter?: 'all' | 'unread'; category?: string } = {}) {
const { limit = 20, cursor, filter = 'all', category } = params
const search = new URLSearchParams()
search.set('limit', String(limit))
search.set('filter', filter)
if (cursor) search.set('cursor', cursor)
if (category) search.set('category', category)
const token = Cookies.get('jwt-token')
if (!token) {
throw new Error('Authentication required: JWT token not found')
}
const url = `${PEANUT_API_URL}/notifications?${search.toString()}`
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
})
if (!response.ok) throw new Error('failed to fetch notifications')
return (await response.json()) as ListResponse
} catch (e) {
throw e
}
},
🤖 Prompt for AI Agents
In src/services/notifications.ts around lines 21 to 44, the code uses
Cookies.get('jwt-token') but does not handle the case where it returns
undefined, resulting in an Authorization header like "Bearer undefined"; update
the function to check the token immediately after reading it and if it's falsy
throw a clear, specific error (e.g., "Missing JWT token for notifications
request") so the function fails fast and avoids making the request with an
invalid header.

Comment on lines +46 to +55
async unreadCount(): Promise<{ count: number }> {
const response = await fetch(`${PEANUT_API_URL}/notifications/unread-count`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${Cookies.get('jwt-token')}`,
},
})
if (!response.ok) throw new Error('failed to fetch unread count')
return await response.json()
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle missing JWT token.

Same issue as in the list method: if the JWT token is missing, the Authorization header will be malformed.

Apply this diff:

     async unreadCount(): Promise<{ count: number }> {
+        const token = Cookies.get('jwt-token')
+        if (!token) {
+            throw new Error('Authentication required: JWT token not found')
+        }
         const response = await fetch(`${PEANUT_API_URL}/notifications/unread-count`, {
             headers: {
                 'Content-Type': 'application/json',
-                Authorization: `Bearer ${Cookies.get('jwt-token')}`,
+                Authorization: `Bearer ${token}`,
             },
         })
📝 Committable suggestion

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

Suggested change
async unreadCount(): Promise<{ count: number }> {
const response = await fetch(`${PEANUT_API_URL}/notifications/unread-count`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${Cookies.get('jwt-token')}`,
},
})
if (!response.ok) throw new Error('failed to fetch unread count')
return await response.json()
},
async unreadCount(): Promise<{ count: number }> {
const token = Cookies.get('jwt-token')
if (!token) {
throw new Error('Authentication required: JWT token not found')
}
const response = await fetch(`${PEANUT_API_URL}/notifications/unread-count`, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
})
if (!response.ok) throw new Error('failed to fetch unread count')
return await response.json()
},
🤖 Prompt for AI Agents
In src/services/notifications.ts around lines 46 to 55, the unreadCount function
builds an Authorization header directly from Cookies.get('jwt-token') which can
be undefined; first read the token into a variable, check if it's present, and
if not throw a clear error (e.g. "missing jwt token") or return a controlled
response; then use that validated token when constructing the fetch headers so
the Authorization header is never malformed and the function fails fast with a
descriptive error.

Comment on lines +57 to +68
async markRead(ids: string[]) {
const response = await fetch(`${PEANUT_API_URL}/notifications/mark-read`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${Cookies.get('jwt-token')}`,
},
body: JSON.stringify({ ids }),
})
if (!response.ok) throw new Error('failed to mark read')
return await response.json()
},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle missing JWT token.

Same issue as the other methods: missing token handling will result in malformed Authorization headers.

Apply this diff:

     async markRead(ids: string[]) {
+        const token = Cookies.get('jwt-token')
+        if (!token) {
+            throw new Error('Authentication required: JWT token not found')
+        }
         const response = await fetch(`${PEANUT_API_URL}/notifications/mark-read`, {
             method: 'POST',
             headers: {
                 'Content-Type': 'application/json',
-                Authorization: `Bearer ${Cookies.get('jwt-token')}`,
+                Authorization: `Bearer ${token}`,
             },
             body: JSON.stringify({ ids }),
         })
📝 Committable suggestion

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

Suggested change
async markRead(ids: string[]) {
const response = await fetch(`${PEANUT_API_URL}/notifications/mark-read`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${Cookies.get('jwt-token')}`,
},
body: JSON.stringify({ ids }),
})
if (!response.ok) throw new Error('failed to mark read')
return await response.json()
},
async markRead(ids: string[]) {
const token = Cookies.get('jwt-token')
if (!token) {
throw new Error('Authentication required: JWT token not found')
}
const response = await fetch(`${PEANUT_API_URL}/notifications/mark-read`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ ids }),
})
if (!response.ok) throw new Error('failed to mark read')
return await response.json()
},
🤖 Prompt for AI Agents
In src/services/notifications.ts around lines 57 to 68, the markRead function
builds an Authorization header directly from Cookies.get('jwt-token') which can
be undefined and produce a malformed header; first read the token into a local
variable, check whether it exists, and if missing either throw a clear error
(e.g., "missing auth token") or reject early; then use that validated token to
construct the Authorization header and proceed with the fetch, ensuring you do
not send an undefined/invalid header value.

Comment on lines +1336 to +1340
export const generateInviteCodeLink = (username: string) => {
const inviteCode = `${username.toUpperCase()}INVITESYOU`
const inviteLink = `${consts.BASE_URL}/invite?code=${inviteCode}`
return { inviteLink, inviteCode }
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

URL-encode the invite code.

If username contains special characters (e.g., &, =, ?), the generated URL will be malformed because the inviteCode is not encoded in the query parameter.

Apply this diff:

 export const generateInviteCodeLink = (username: string) => {
     const inviteCode = `${username.toUpperCase()}INVITESYOU`
-    const inviteLink = `${consts.BASE_URL}/invite?code=${inviteCode}`
+    const inviteLink = `${consts.BASE_URL}/invite?code=${encodeURIComponent(inviteCode)}`
     return { inviteLink, inviteCode }
 }
📝 Committable suggestion

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

Suggested change
export const generateInviteCodeLink = (username: string) => {
const inviteCode = `${username.toUpperCase()}INVITESYOU`
const inviteLink = `${consts.BASE_URL}/invite?code=${inviteCode}`
return { inviteLink, inviteCode }
}
export const generateInviteCodeLink = (username: string) => {
const inviteCode = `${username.toUpperCase()}INVITESYOU`
const inviteLink = `${consts.BASE_URL}/invite?code=${encodeURIComponent(inviteCode)}`
return { inviteLink, inviteCode }
}
🤖 Prompt for AI Agents
In src/utils/general.utils.ts around lines 1336 to 1340 the invite code is
interpolated directly into the URL query string causing malformed URLs when
username contains special characters; update the code to URL-encode the
inviteCode (use encodeURIComponent on the inviteCode when constructing
inviteLink) so the query parameter is safely escaped and the returned inviteLink
remains valid.

Signed-off-by: Hugo Montenegro <hugo@peanut.to>
cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c4b47eb and 21b242b.

📒 Files selected for processing (1)
  • src/app/(mobile-ui)/home/page.tsx (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/app/(mobile-ui)/home/page.tsx (2)
src/hooks/useNotifications.ts (1)
  • useNotifications (22-416)
src/components/Notifications/SetupNotificationsModal.tsx (1)
  • SetupNotificationsModal (5-71)
⏰ 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). (2)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Deploy-Preview

Comment on lines +53 to 54
const { showPermissionModal } = useNotifications()
const { balance, address, isFetchingBalance } = useWallet()
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t call useNotifications twice

useNotifications keeps its own React state. Calling it here creates a second, independent instance from the one inside SetupNotificationsModal. When the user dismisses the modal, only the modal’s instance flips showPermissionModal back to false; the Home-level instance stays true, so we keep rendering <SetupNotificationsModal /> (just invisible) and permanently suppress the add-money prompt (!showPermissionModal never becomes true again). Hoist the hook to a single owner (e.g., call it only in Home and pass the state/actions into the modal) or otherwise share the state (context/store).

🤖 Prompt for AI Agents
In src/app/(mobile-ui)/home/page.tsx around lines 53-54, you’re calling
useNotifications twice which creates two independent React states; remove the
extra hook usage and hoist the notifications state to a single owner by calling
useNotifications only in Home and passing the relevant values/actions
(showPermissionModal and any setters or handlers) as props into
SetupNotificationsModal (also update SetupNotificationsModal to accept and use
those props instead of calling useNotifications internally), or alternatively
wire the modal to a shared store/context so only one instance of the hook/state
exists.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/components/Global/EarlyUserModal/index.tsx (2)

11-11: The missing username guard issue remains unaddressed.

This issue was previously flagged: if user?.user.username is undefined or empty, the generated invite link will be invalid ('INVITESYOU'). The modal will render with a broken invite link, leading to a poor user experience.

Please refer to the previous review comment for suggested fixes.


20-24: The async error handling issue remains unaddressed.

This issue was previously flagged: updateUserById and fetchUser can fail, but errors are not handled. If updateUserById fails, the modal will close but the hasSeenEarlyUserModal flag won't be set, causing the modal to reappear unexpectedly.

Please refer to the previous review comment for suggested fixes.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21b242b and 5dc591b.

📒 Files selected for processing (1)
  • src/components/Global/EarlyUserModal/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/Global/EarlyUserModal/index.tsx (3)
src/context/authContext.tsx (1)
  • useAuth (191-197)
src/utils/general.utils.ts (2)
  • generateInviteCodeLink (1336-1340)
  • generateInvitesShareText (1332-1334)
src/app/actions/users.ts (1)
  • updateUserById (12-35)
🪛 Biome (2.1.2)
src/components/Global/EarlyUserModal/index.tsx

[error] 51-52: Avoid using target="_blank" without rel="noopener" or rel="noreferrer".

Opening external links in new tabs without rel="noopener" is a security risk. See the explanation for more details.
Safe fix: Add the rel="noopener" attribute.

(lint/security/noBlankTarget)

⏰ 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

Comment on lines +48 to +52
<a
className="text-sm text-grey-1 underline"
href="https://docs.peanut.me/og-and-invites"
target="_blank"
>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add rel="noopener" to external link for security.

The anchor tag uses target="_blank" without rel="noopener" or rel="noreferrer". This allows the opened page to access window.opener, which is a security risk.

As per static analysis hint, apply this diff to fix the security issue:

                    <a
                        className="text-sm text-grey-1 underline"
                        href="https://docs.peanut.me/og-and-invites"
                        target="_blank"
+                       rel="noopener"
                    >
                        Learn more
                    </a>
📝 Committable suggestion

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

Suggested change
<a
className="text-sm text-grey-1 underline"
href="https://docs.peanut.me/og-and-invites"
target="_blank"
>
<a
className="text-sm text-grey-1 underline"
href="https://docs.peanut.me/og-and-invites"
target="_blank"
rel="noopener"
>
Learn more
</a>
🧰 Tools
🪛 Biome (2.1.2)

[error] 51-52: Avoid using target="_blank" without rel="noopener" or rel="noreferrer".

Opening external links in new tabs without rel="noopener" is a security risk. See the explanation for more details.
Safe fix: Add the rel="noopener" attribute.

(lint/security/noBlankTarget)

🤖 Prompt for AI Agents
In src/components/Global/EarlyUserModal/index.tsx around lines 48 to 52, the
anchor uses target="_blank" without a rel attribute; update the anchor to
include rel="noopener" (or rel="noopener noreferrer") to prevent the opened page
from accessing window.opener. Modify the <a> tag to add the rel attribute
alongside the existing target attribute and keep other attributes unchanged.

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.

4 participants