SP 112 Prod release - Lockup#1318
Conversation
…r missing inviter username
…invite acceptance state in PaymentForm
…d-accounts refactor: dont shorten saved account titles
[TASK-15701] Fix: guest invite link flow
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Other AI code review bot(s) detectedCodeRabbit 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. WalkthroughAdds 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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 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.
shortenStringLongis 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/setupso it directs users to the setup page’s waitlist step.src/components/Profile/index.tsx (1)
74-79: Click opens modal but href navigates immediatelyWith 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 earlyreturnleaves the UI stuck in loading ifrecipient.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
peanutWalletAddressis 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
usernamerelies onflowand potentially undefinedrequestLinkData?.recipienteven whenflow === 'claim'. When we hit the invite banner ({isInviteLink && !userHasAppAccess && username && ...}) on the request flow with missingrequestLinkData,usernamefalls back toclaimLinkData?.sender?.username, which is undefined, and the banner disappears even though request invite data exists. Please branch the derivation to matchflowinstead of coalescing:- const username = claimLinkData?.sender?.username ?? requestLinkData?.recipient?.identifier + const username = + flow === 'claim' + ? claimLinkData?.sender?.username + : requestLinkData?.recipient?.identifierThis way request invites always show the inviter text.
247-304: Modal losses when reopening a methodWhen the confirm invite modal’s “Lose invite” branch calls
handleMethodClick(selectedMethod)we never resetshowInviteModalbefore navigation. IfhandleMethodClickshort-circuits (e.g. because of min-amount check), the modal remains open butselectedMethodbecomes 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 liketooltip: 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
#FF5656appears 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
iconanddescriptionprops with only thetitlediffering. 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 ifAPI_KEYis 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_KEYAlso applies to: 13-16
src/components/Global/GuestLoginCta/index.tsx (1)
46-52: PreferSentry.captureMessagefor string messages.Line 51 calls
Sentry.captureExceptionwith a string. While this works, it's more idiomatic to useSentry.captureMessagefor string messages or wrap the string in anErrorobject.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
currentColorfor 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:
- Store the acceptance result in state and display a toast/notification after registration completes
- Add the result to the registration flow's return value so calling code can handle it
- 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 passingonClick,onClose, andisPermissionDeniedto NotificationBanner. While theuseBannershook 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
setIsLoadingstate 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 theModalcomponent uses avisibleprop. For consistency across the codebase (e.g.,ActionModalusesvisible), consider renaming tovisibleorisVisible.-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 requestPrevents 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 missingEmpty 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) : nullThen conditionally render invite content when inviteData exists.
21-23: Unreachable “You’re already verified” modalIt’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 tabOpening /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 componentThe 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: setIsLoadingUse setIsLoading for consistency with React conventions.
Also applies to: 74-83
src/app/(mobile-ui)/layout.tsx (1)
24-25: Tighten public path regexConsider 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 nameisLoading 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 effectThere 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 depsAdd
fetchUserto 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_LINKis 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 absentPrevent submit if
inviteTypeis undefined.- disabled={!isValid || isChanging || isLoading} + disabled={!isValid || isChanging || isLoading || !inviteType}
20-24: Nit: setter naming consistencyPrefer
setIsLoading/setIsLoggingOutfor 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 indexIf 'signup' isn’t in steps,
findIndexreturns -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
snoozeReminderBannerandclosePermissionModalboth callsetTimeoutwithout guarding against component unmount or multiple outstanding timers whenevaluateVisibilityre-schedules quickly (e.g. after permissionChange). We already clearbannerTimerRef.currentat cleanup, but we should also clear before scheduling insideevaluateVisibilityto avoid multiple timers. Right now ifevaluateVisibilityruns twice withbannerShowAt > 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
⛔ Files ignored due to path filters (7)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlpublic/arrows/top-right-arrow-2.svgis excluded by!**/*.svgsrc/assets/icons/fbmessenger.svgis excluded by!**/*.svgsrc/assets/icons/imessage.svgis excluded by!**/*.svgsrc/assets/icons/starStraight.svgis excluded by!**/*.svgsrc/assets/icons/whatsapp.svgis excluded by!**/*.svgsrc/assets/illustrations/buttery_smooth_global_money.svgis 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.tsxsrc/app/(mobile-ui)/points/page.tsxsrc/components/Home/InvitesIcon.tsxsrc/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.tsxsrc/app/(mobile-ui)/layout.tsxsrc/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)
| <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 ? ( |
There was a problem hiding this comment.
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.
| useEffect(() => { | ||
| // Re-fetch user to get the latest invitees list for showing heart Icon | ||
| fetchUser() | ||
| }, []) |
There was a problem hiding this comment.
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.
| 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.
| // reset payment state on unmount | ||
| useEffect(() => { | ||
| return () => { | ||
| dispatch(paymentActions.resetPaymentState()) | ||
| } | ||
| }, []) |
There was a problem hiding this comment.
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.
| // reset payment state on unmount | |
| useEffect(() => { | |
| return () => { | |
| dispatch(paymentActions.resetPaymentState()) | |
| } | |
| }, []) |
| // 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.
| state.inviteCode = '' | ||
| }, |
There was a problem hiding this comment.
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.
| 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.
| 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 | ||
| } | ||
| }, |
There was a problem hiding this comment.
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.
| 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.
| 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() | ||
| }, |
There was a problem hiding this comment.
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.
| 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.
| 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() | ||
| }, |
There was a problem hiding this comment.
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.
| 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.
| export const generateInviteCodeLink = (username: string) => { | ||
| const inviteCode = `${username.toUpperCase()}INVITESYOU` | ||
| const inviteLink = `${consts.BASE_URL}/invite?code=${inviteCode}` | ||
| return { inviteLink, inviteCode } | ||
| } |
There was a problem hiding this comment.
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.
| 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.
Feat/resolve merge conflict
Signed-off-by: Hugo Montenegro <hugo@peanut.to>
feat: comment out notification bell icon
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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
| const { showPermissionModal } = useNotifications() | ||
| const { balance, address, isFetchingBalance } = useWallet() |
There was a problem hiding this comment.
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.
Update OG modal copy
There was a problem hiding this comment.
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.usernameisundefinedor 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:
updateUserByIdandfetchUsercan fail, but errors are not handled. IfupdateUserByIdfails, the modal will close but thehasSeenEarlyUserModalflag 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
📒 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
| <a | ||
| className="text-sm text-grey-1 underline" | ||
| href="https://docs.peanut.me/og-and-invites" | ||
| target="_blank" | ||
| > |
There was a problem hiding this comment.
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.
| <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.
No description provided.