Skip to content

[TASK-14876] Feat/invites#1251

Merged
Zishan-7 merged 50 commits intopeanut-wallet-devfrom
feat/invites
Oct 6, 2025
Merged

[TASK-14876] Feat/invites#1251
Zishan-7 merged 50 commits intopeanut-wallet-devfrom
feat/invites

Conversation

@Zishan-7
Copy link
Contributor

@Zishan-7 Zishan-7 commented Sep 24, 2025

Also contributes to TASK-14878


Note

Adds invite-only gating with waitlist, a new invite claim page, revamped invites/points UI, and flow guards/modals across claim/request and setup.

  • Access & Routing:

    • Gate app by user.user.hasAppAccess; show JoinWaitlistPage when access is missing.
    • Add public route for invite and new page src/app/invite/page.tsx validating codes and onboarding via signup.
    • Home header: add InvitesIcon and link to /points.
  • Invites & Points UX:

    • Revamp src/app/(mobile-ui)/points/page.tsx: fetch invites, show invite code/link, copy/share, and invited users list.
    • New components: InvitesPageLayout, JoinWaitlistPage, ConfirmInviteModal, Home/InvitesIcon; enhance CopyToClipboard (button mode) and ShareButton layout.
    • Add assets/icons: starStraight.svg, trophy icon.
  • Payment/Claim Flows:

    • ActionList and ActionListDaimoPayButton: support isInviteLink, show invite-protection modal, and pass through to setup with prefilled invite; enforce min amounts; integrate Daimo confirm modal.
    • Wire isInviteLink from claim/request views.
  • Setup Flow:

    • Replace Welcome with invite-first JoinWaitlist step; add CollectEmail step; wrap setup/layout in Suspense.
    • Store invite in Redux (inviteCode, inviteType); accept invite during passkey registration; skip Welcome when step=signup and invite present.
  • Services & Types:

    • New invitesApi (accept, validate, list, waitlist position) and server action validateInviteCode.
    • Extend interfaces.User with hasAppAccess; add Invite/EInviteType; add generateInvitesShareText util.
  • Profile & Public Profile:

    • Add “Invite friends” modal with share/copy in profile.
    • Public profile: block Request for non-access users and provide “beg for an invite” share; add invite modal.

Written by Cursor Bugbot for commit ed5c25d. This will update automatically on new commits. Configure here.

@notion-workspace
Copy link

Implement UI Changes

@notion-workspace
Copy link

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 24, 2025

Warning

Rate limit exceeded

@Zishan-7 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 0 minutes and 52 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 6891653 and 8e3adb0.

📒 Files selected for processing (2)
  • src/app/(setup)/layout.tsx (2 hunks)
  • src/assets/icons/index.ts (1 hunks)

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 an invite-first feature set: server invite validation and services, invite landing/OG generation, invite/join-waitlist UIs and layout, setup flow wiring and Redux invite state, ActionList invite/payment modal flows, new icons/assets, and mobile layout gating by user app access.

Changes

Cohort / File(s) Summary
Server invite action & service types
src/app/actions/invites.ts, src/services/invites.ts, src/services/services.types.ts
New server action validateInviteCode; invitesApi gains getWaitlistQueuePosition, validateInviteCode, acceptInvite; adds EInviteType enum and Invite type.
Invite landing, OG & assets
src/app/invite/page.tsx, src/components/og/InviteCardOG.tsx, src/app/api/og/route.tsx
New invite page with generateMetadata, InviteCardOG component, and OG route branch for invite cards (isInvite query path).
Invites pages & layout components
src/components/Invites/InvitesPage.tsx, src/components/Invites/InvitesPageLayout.tsx, src/components/Invites/JoinWaitlistPage.tsx, src/components/Home/InvitesIcon.tsx
New InvitesPage, InvitesPageLayout, JoinWaitlistPage (client) and a mobile InvitesIcon component with animation.
Setup views, flow & wiring
src/components/Setup/Views/JoinWaitlist.tsx, src/components/Setup/Views/CollectEmail.tsx, src/components/Setup/Views/SetupPasskey.tsx, src/components/Setup/Views/index.ts, src/components/Setup/Setup.consts.tsx, src/components/Setup/Setup.types.ts
New JoinWaitlist and CollectEmail views; setup steps updated (collect-email added, join-waitlist used), passkey flow adjusted for invite handling, and CollectEmail re-exported.
Setup page & layout (Suspense)
src/app/(setup)/setup/page.tsx, src/app/(setup)/layout.tsx
Extracted content into Suspense-wrapped components, added PeanutLoading fallback, moved setup initialization into content components.
Mobile UI header, points & layout gating
src/app/(mobile-ui)/home/page.tsx, src/app/(mobile-ui)/points/page.tsx, src/app/(mobile-ui)/layout.tsx
Added InvitesIcon to mobile header; Points page converted to invites-centric sharing UI; layout publicPathRegex extended and new branch renders JoinWaitlistPage when user exists but lacks hasAppAccess.
ActionList, payments, modals & claim integration
src/components/Common/ActionList.tsx, src/components/Common/ActionListDaimoPayButton.tsx, src/components/Common/ActionList.tsx (props), src/components/Claim/Link/Initial.view.tsx, src/app/[...recipient]/client.tsx
ActionList gains isInviteLink prop and invite-banner/confirm flows; Daimo pay button refactored to support ConfirmInviteModal and stored callback invocation; isInviteLink propagated to relevant invocation sites.
Confirm & action modal changes
src/components/Global/ActionModal/index.tsx, src/components/Global/ConfirmInviteModal/index.tsx
ActionModal accepts new content prop and per-CTA children; new ConfirmInviteModal component added for "Don’t lose your invite!" flow.
Profile invite UI & sharing
src/components/Profile/index.tsx, src/components/Profile/components/PublicProfile.tsx, src/utils/general.utils.ts
Invite Friends modal and in-profile share/copy invite-code UI added; guest messaging adjusted; generateInvitesShareText util added.
Icons, assets & small UI components
src/components/Global/Icons/trophy.tsx, src/components/Global/Icons/Icon.tsx, src/assets/icons/index.ts, src/components/Global/CopyToClipboard/index.tsx, src/components/Global/ShareButton/index.tsx
Added TrophyIcon and mapped it; exported STAR_STRAIGHT_ICON; CopyToClipboard supports `type: 'button'
Invites Redux & user types
src/redux/slices/setup-slice.ts, src/redux/types/setup.types.ts, src/interfaces/interfaces.ts
Added inviteCode and inviteType to setup state and reducers; added hasAppAccess: boolean to User interface.
Auth/registration hook & context changes
src/hooks/useZeroDev.ts, src/context/authContext.tsx
During signup, call invitesApi.acceptInvite when invite present; switched auth context to use useUserQuery.isLoading for user-loading flag.
Copy/CTA wiring & miscellaneous
src/components/Common/*, src/components/Global/*, src/components/Setup/*, src/components/Profile/*, src/app/(mobile-ui)/*
Multiple UI wiring changes: CTA behaviors, modal insertions, sharing buttons, header CTAs, small text/layout adjustments, and a debug console.log(user, 'user').

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Support page feature #1195 — modifies src/app/(mobile-ui)/layout.tsx (mobile layout auth/loading gating), closely related to the layout gating and hasAppAccess changes.
  • [TASK-12124] Prod Release Sprint 95 #911 — modifies src/app/api/og/route.tsx and OG image handling, related to the invite OG branch and InviteCardOG usage.
  • Stage manteca-integration #1269 — adds/exported constants for API key handling (PEANUT_API_KEY), related to server action validateInviteCode which reads that env var.

Suggested reviewers

  • kushagrasarathe
  • jjramirezn
  • Hugo0

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title “[TASK-14876] Feat/invites” concisely indicates that this PR implements the invites feature and aligns directly with the main changeset focused on invite workflows, waitlist gating, and related UI enhancements.
Description Check ✅ Passed The description thoroughly outlines the invite-only gating, waitlist integration, new invite claim pages, updated points UI, setup flow adjustments, and service extensions, matching the detailed changeset and providing clear context for reviewers.

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.

@vercel
Copy link

vercel bot commented Sep 24, 2025

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

Project Deployment Preview Comments Updated (UTC)
peanut-wallet Ready Ready Preview Comment Oct 6, 2025 0:03am

@coderabbitai coderabbitai bot added the enhancement New feature or request label Sep 24, 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: 15

Caution

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

⚠️ Outside diff range comments (2)
src/app/[...recipient]/client.tsx (1)

567-572: Fix typo in error copy

“The payment” not “They payment”.

-    message: 'They payment you are trying to access is invalid. Please check the URL and try again.',
+    message: 'The payment you are trying to access is invalid. Please check the URL and try again.',
src/components/Common/ActionList.tsx (1)

200-203: Fix min-amount message for request flow ($1 vs $5).

The modal always says $5, but request flow threshold is $1. Make the message conditional.

-                description={'The minimum amount for a bank transaction is $5. Please try a different method.'}
+                description={`The minimum amount for a bank transaction is $${flow === 'request' ? 1 : 5}. Please try a different method.`}
🧹 Nitpick comments (18)
src/interfaces/interfaces.ts (1)

239-239: Confirm backend parity for hasAppAccess (or make it optional).

This is a breaking type change if older API payloads don’t include hasAppAccess. Either ensure the BE returns this for all users now, or make it optional temporarily.

If needed, make it optional:

-    hasAppAccess: boolean
+    hasAppAccess?: boolean
src/app/(mobile-ui)/home/page.tsx (2)

44-44: Prefer vector Icon over raster image.

You added a PNG just for a small header affordance. We already have the Icon system (and a new trophy icon). Use Icon for theming, dark mode, and consistency; drop this import.

-import starImage from '@/assets/icons/star.png'
+// Use <Icon name="trophy" /> instead of a raster image

223-228: Use Icon + add accessible label for the Points link.

Replace the raster image with the Icon component and add aria-label for better a11y. Keeps SearchUsers behavior intact.

-                        <Link href="/points">
-                            <Image src={starImage} alt="star" width={20} height={20} />
-                        </Link>
+                        <Link
+                            href="/points"
+                            aria-label="Points"
+                            className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-gray-100"
+                        >
+                            <Icon name="trophy" size={20} />
+                        </Link>
src/components/Profile/components/PublicProfile.tsx (1)

173-182: Wire up the modal CTA.

The CTA onClick is a no-op. Consider calling navigator.share (with a fallback to copy) or reusing ShareButton logic so the modal actually helps guests obtain invites.

src/hooks/useZeroDev.ts (1)

59-61: Handle invite acceptance result and optionally clear the code.

acceptInvite failures are silently ignored; this can leave users without access. Check the result, log to Sentry on failure, and consider clearing inviteCode on success.

-            if (inviteCode.trim().length > 0) {
-                await invitesApi.acceptInvite(inviteCode)
-            }
+            if (inviteCode?.trim()) {
+                const res = await invitesApi.acceptInvite(inviteCode.trim())
+                if (!res.success) {
+                    captureException(new Error(`Invite acceptance failed for code: ${inviteCode}`))
+                }
+                // Optionally clear the invite code after successful acceptance:
+                // dispatch(setupActions.setInviteCode(''))
+            }

Add import if you choose to clear the code afterward:

import { setupActions } from '@/redux/slices/setup-slice'

Also confirm a valid JWT cookie exists at registration time, since acceptInvite expects Authorization.

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

61-67: Avoid dummy href to prevent accidental navigation

ProfileMenuItem may still navigate to /dummy. Drop href (or ensure onClick prevents default inside the component).

-                    <ProfileMenuItem
+                    <ProfileMenuItem
                         icon="smile"
                         label="Invite friends to Peanut"
                         onClick={() => setIsInviteFriendsModalOpen(true)}
-                        href="/dummy" // Dummy link, wont be called
                         position="single"
                     />

169-177: Handle clipboard write Promise and errors

navigator.clipboard.writeText returns a Promise and can fail in non-secure contexts. Await and surface success/failure (toast/snackbar).

-                        onClick: () => {
-                            navigator.clipboard.writeText(inviteCode)
-                        },
+                        onClick: async () => {
+                            try {
+                                await navigator.clipboard.writeText(inviteCode)
+                                // TODO: show success toast
+                            } catch {
+                                // TODO: show error toast / fallback
+                            }
+                        },
src/app/invite/page.tsx (1)

57-57: Make the login CTA an actual link

Turn this into a Link to the correct login route to complete the flow.

What is the correct login route in this app (/setup?step=signin, /login, etc.)? Once confirmed, replace the button with a Next.js Link accordingly.

src/components/Global/CopyToClipboard/CopyToClipboardButton.tsx (1)

2-2: Harden clipboard handling and clean up timers.

  • Add try/catch for clipboard failures (permissions/insecure context).
  • Clear the timeout on unmount to avoid setState on unmounted component.

Apply these diffs:

-import React, { useState } from 'react'
+import React, { useEffect, useRef, useState } from 'react'
-    const [copied, setCopied] = useState(false)
+    const [copied, setCopied] = useState(false)
+    const timeoutRef = useRef<number | null>(null)
+
+    useEffect(() => {
+        return () => {
+            if (timeoutRef.current) {
+                clearTimeout(timeoutRef.current)
+            }
+        }
+    }, [])
-    const handleCopy = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
-        e.stopPropagation()
-        navigator.clipboard.writeText(textToCopy).then(() => {
-            setCopied(true)
-            setTimeout(() => setCopied(false), 2000)
-        })
-    }
+    const handleCopy = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
+        e.stopPropagation()
+        try {
+            await navigator.clipboard.writeText(textToCopy)
+            setCopied(true)
+            if (timeoutRef.current) clearTimeout(timeoutRef.current)
+            timeoutRef.current = window.setTimeout(() => setCopied(false), 2000)
+        } catch (err) {
+            console.error('Clipboard copy failed:', err)
+        }
+    }

Also applies to: 13-14, 15-21

src/app/actions/invites.ts (2)

18-25: Trim trailing slash from base URL to avoid double slashes.

Prevents accidental // in the request URL if env var ends with /.

Apply this diff:

-        const response = await fetchWithSentry(`${apiUrl}/invites/validate`, {
+        const response = await fetchWithSentry(`${apiUrl.replace(/\/$/, '')}/invites/validate`, {
             method: 'POST',
             headers: {
                 'Content-Type': 'application/json',
                 'api-key': API_KEY,
             },
             body: JSON.stringify({ inviteCode }),
         })

32-35: Be resilient to response shape differences (username under payload).

If the API returns { payload: { username } }, current code would miss it. Handle both.

Apply this diff:

-        const data = await response.json()
-
-        return { data: { success: true, username: data.username } }
+        const data = await response.json()
+        const username = data?.username ?? data?.payload?.username ?? ''
+        return { data: { success: true, username } }
src/components/Invites/JoinWaitlistPage.tsx (3)

31-45: Reset error before attempting to accept invite.

Prevents stale error banners from persisting across retries.

Apply this diff:

     const handleAcceptInvite = async () => {
         setisLoading(true)
         try {
+            setError('')
             const res = await invitesApi.acceptInvite(inviteCode)
             if (res.success) {
                 fetchUser()
             } else {
                 setError('Something went wrong. Please try again or contact support.')
             }
         } catch {
             setError('Something went wrong. Please try again or contact support.')
         } finally {
             setisLoading(false)
         }
     }

82-86: Clear errors while typing/changing the code.

UX: removes previous error state as the user edits the input.

Apply this diff:

                                 onUpdate={({ value, isValid, isChanging }) => {
                                     setIsValid(isValid)
                                     setIsChanging(isChanging)
                                     setInviteCode(value)
+                                    if (error) setError('')
                                 }}

18-28: Separate validation vs. submission loading states.

Using one isLoading for both validation (debounced) and acceptance can cause confusing button spinners. Consider distinct flags: isValidating and isAccepting, wiring ValidatedInput to the former and the Next button to the latter.

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

44-75: Use the safe inviteList everywhere.

Replace invites with inviteList in conditions and mapping.

Apply this diff:

-                {invites.length > 0 && (
+                {inviteList.length > 0 && (
                     <>
                         <Button shadowSize="4">Invite a friend!</Button>
                         <h2 className="!mt-8 font-bold">People you invited</h2>
                         <div>
-                            {invites.map((invite: any, i: number) => {
+                            {inviteList.map((invite: any, i: number) => {
                                 const username = invite.invitee.username
                                 const isVerified = invite.invitee.bridgeKycStatus === 'approved'
                                 return (
-                                    <Card key={invite.id} position={getCardPosition(i, invites.length)}>
+                                    <Card key={invite.id} position={getCardPosition(i, inviteList.length)}>
                                         <div className="flex items-center justify-between gap-4">
-                {invites.length === 0 && (
+                {inviteList.length === 0 && (
                     <Card className="flex flex-col items-center justify-center gap-4 py-4">

Also applies to: 77-90


18-21: Scope query by user and gate execution.

Consider including the user id in the queryKey and adding enabled: !!user?.user?.id to avoid fetching before auth is ready and to prevent stale cache across users.

src/components/Setup/Views/JoinWaitlist.tsx (2)

21-21: Typo: rename setisLoading -> setIsLoading.

Minor readability/consistency fix.

-    const [isLoading, setisLoading] = useState(false)
+    const [isLoading, setIsLoading] = useState(false)
-        setisLoading(true)
+        setIsLoading(true)
         const res = await invitesApi.validateInviteCode(inviteCode)
-        setisLoading(false)
+        setIsLoading(false)

Also applies to: 31-35


30-35: Skip network call on empty/whitespace codes.

Avoid unnecessary requests and flicker while typing.

-    const validateInviteCode = async (inviteCode: string): Promise<boolean> => {
-        setIsLoading(true)
-        const res = await invitesApi.validateInviteCode(inviteCode)
-        setIsLoading(false)
-        return res.success
-    }
+    const validateInviteCode = async (inviteCode: string): Promise<boolean> => {
+        const code = inviteCode.trim()
+        if (!code) return false
+        setIsLoading(true)
+        const res = await invitesApi.validateInviteCode(code)
+        setIsLoading(false)
+        return res.success
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da0e6d2 and 6d430da.

📒 Files selected for processing (24)
  • src/app/(mobile-ui)/home/page.tsx (2 hunks)
  • src/app/(mobile-ui)/layout.tsx (2 hunks)
  • src/app/(mobile-ui)/points/page.tsx (1 hunks)
  • src/app/(setup)/layout.tsx (1 hunks)
  • src/app/(setup)/setup/page.tsx (0 hunks)
  • src/app/[...recipient]/client.tsx (1 hunks)
  • src/app/actions/invites.ts (1 hunks)
  • src/app/invite/page.tsx (1 hunks)
  • src/components/Common/ActionList.tsx (6 hunks)
  • src/components/Global/ActionModal/index.tsx (6 hunks)
  • src/components/Global/CopyToClipboard/CopyToClipboardButton.tsx (1 hunks)
  • src/components/Global/Icons/Icon.tsx (3 hunks)
  • src/components/Global/Icons/trophy.tsx (1 hunks)
  • src/components/Invites/InvitesPageLayout.tsx (1 hunks)
  • src/components/Invites/JoinWaitlistPage.tsx (1 hunks)
  • src/components/Profile/components/PublicProfile.tsx (5 hunks)
  • src/components/Profile/index.tsx (4 hunks)
  • src/components/Setup/Setup.consts.tsx (2 hunks)
  • src/components/Setup/Views/JoinWaitlist.tsx (1 hunks)
  • src/hooks/useZeroDev.ts (3 hunks)
  • src/interfaces/interfaces.ts (1 hunks)
  • src/redux/slices/setup-slice.ts (3 hunks)
  • src/redux/types/setup.types.ts (1 hunks)
  • src/services/invites.ts (1 hunks)
💤 Files with no reviewable changes (1)
  • src/app/(setup)/setup/page.tsx
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-18T09:30:42.901Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1230
File: src/app/(mobile-ui)/withdraw/page.tsx:92-97
Timestamp: 2025-09-18T09:30:42.901Z
Learning: In src/app/(mobile-ui)/withdraw/page.tsx, the useEffect that calls setShowAllWithdrawMethods(true) when amountFromContext exists is intentionally designed to run only on component mount (empty dependency array), not when amountFromContext changes. This is the correct behavior for the withdraw flow where showing all methods should only happen on initial load when an amount is already present.

Applied to files:

  • src/app/(mobile-ui)/home/page.tsx
📚 Learning: 2025-08-12T17:44:04.268Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1089
File: src/components/LandingPage/dropLink.tsx:35-42
Timestamp: 2025-08-12T17:44:04.268Z
Learning: In the Peanut UI project, opening the `/setup` route in a new tab from landing page CTAs is intentional design behavior to keep users on the marketing page while they start the setup process.

Applied to files:

  • src/components/Common/ActionList.tsx
📚 Learning: 2025-01-13T17:36:31.764Z
Learnt from: jjramirezn
PR: peanutprotocol/peanut-ui#623
File: src/context/walletContext/zeroDevContext.context.tsx:91-93
Timestamp: 2025-01-13T17:36:31.764Z
Learning: In the peanut-ui project, the webAuthnKey stored in localStorage only contains public data and is safe to store there. This is used in the ZeroDevContext for passkey validation.

Applied to files:

  • src/hooks/useZeroDev.ts
📚 Learning: 2025-01-13T17:45:04.539Z
Learnt from: jjramirezn
PR: peanutprotocol/peanut-ui#623
File: src/context/walletContext/zeroDevContext.context.tsx:106-123
Timestamp: 2025-01-13T17:45:04.539Z
Learning: The promise chain for creating passkey validator and kernel client from webAuthnKey in ZeroDevContext is designed to be reliable and should not error, as the webAuthnKey is already validated before being stored in localStorage.

Applied to files:

  • src/hooks/useZeroDev.ts
🧬 Code graph analysis (14)
src/components/Invites/JoinWaitlistPage.tsx (4)
src/context/authContext.tsx (1)
  • useAuth (182-188)
src/app/actions/invites.ts (1)
  • validateInviteCode (7-42)
src/services/invites.ts (1)
  • invitesApi (6-52)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/components/Global/CopyToClipboard/CopyToClipboardButton.tsx (1)
src/components/0_Bruddle/Button.tsx (2)
  • ButtonSize (17-17)
  • Button (76-267)
src/components/Setup/Views/JoinWaitlist.tsx (6)
src/hooks/useZeroDev.ts (1)
  • useZeroDev (36-172)
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/services/invites.ts (1)
  • invitesApi (6-52)
src/utils/general.utils.ts (2)
  • getFromLocalStorage (126-148)
  • sanitizeRedirectURL (1220-1232)
src/components/Global/Icons/Icon.tsx (1)
src/components/Global/Icons/trophy.tsx (1)
  • TrophyIcon (3-12)
src/app/(mobile-ui)/points/page.tsx (6)
src/services/invites.ts (1)
  • invitesApi (6-52)
src/context/authContext.tsx (1)
  • useAuth (182-188)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/components/Global/Card/index.tsx (1)
  • getCardPosition (14-19)
src/components/UserHeader/index.tsx (1)
  • VerifiedUserLabel (34-81)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (198-207)
src/app/actions/invites.ts (1)
src/utils/sentry.utils.ts (1)
  • fetchWithSentry (26-104)
src/services/invites.ts (3)
src/utils/sentry.utils.ts (1)
  • fetchWithSentry (26-104)
src/constants/general.consts.ts (1)
  • PEANUT_API_URL (43-47)
src/app/actions/invites.ts (1)
  • validateInviteCode (7-42)
src/app/(setup)/layout.tsx (2)
src/redux/hooks.ts (2)
  • useAppDispatch (5-5)
  • useSetupStore (9-9)
src/components/Setup/Setup.consts.tsx (1)
  • setupSteps (9-91)
src/app/(mobile-ui)/home/page.tsx (1)
src/components/SearchUsers/index.tsx (1)
  • SearchUsers (82-121)
src/components/Profile/index.tsx (1)
src/context/authContext.tsx (1)
  • useAuth (182-188)
src/components/Common/ActionList.tsx (5)
src/services/sendLinks.ts (1)
  • ClaimLinkData (62-62)
src/lib/url-parser/types/payment.ts (1)
  • ParsedURL (7-16)
src/constants/actionlist.consts.ts (1)
  • PaymentMethod (5-11)
src/redux/hooks.ts (1)
  • useAppDispatch (5-5)
src/redux/slices/setup-slice.ts (1)
  • setupActions (60-60)
src/hooks/useZeroDev.ts (2)
src/redux/hooks.ts (1)
  • useSetupStore (9-9)
src/services/invites.ts (1)
  • invitesApi (6-52)
src/app/invite/page.tsx (4)
src/redux/hooks.ts (1)
  • useAppDispatch (5-5)
src/services/invites.ts (1)
  • invitesApi (6-52)
src/redux/slices/setup-slice.ts (1)
  • setupActions (60-60)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
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 (198-207)
🪛 Biome (2.1.2)
src/components/Invites/InvitesPageLayout.tsx

[error] 33-40: Missing key property for this element in iterable.

The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.

(lint/correctness/useJsxKeyInIterable)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (21)
src/components/Setup/Setup.consts.tsx (2)

6-8: LGTM on swapping in JoinWaitlist.

Direct import is fine; aligns the setup flow with the invite-only gating.


49-54: Invite-only welcome step looks good—verify invite code persistence.

Ensure JoinWaitlist writes the invite code into the setup store (inviteCode) so subsequent steps can consume it.

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

62-63: Trophy icon addition looks solid.

Import, union type, and mapping are consistent with existing patterns. No issues.

Also applies to: 126-127, 195-196

src/redux/types/setup.types.ts (1)

10-10: Resolved — inviteCode is initialized in the slice, has a setter, and is reset.
ISetupState includes inviteCode; setup slice initialState sets inviteCode: ''; reducers include setInviteCode and resetSetup clears inviteCode.

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

513-516: Good addition: isInviteLink wiring

Passing isInviteLink based on flow and USERNAME looks right and scopes the behavior well.

Please confirm ActionList’s prop typing includes isInviteLink?: boolean and the default behavior is unchanged when false/undefined.

src/redux/slices/setup-slice.ts (3)

13-14: State shape update looks good

inviteCode added to initial state.


30-31: Reset includes inviteCode

Resetting inviteCode to '' aligns with the rest of resetSetup.


54-56: Reducer for inviteCode

setInviteCode reducer is correct and typed.

src/components/Global/ActionModal/index.tsx (5)

11-12: CTA children support: LGTM

Adding children to ActionModalButtonProps is a useful extension and backward compatible.


44-45: Modal content slot: LGTM

content prop placement is appropriate and doesn’t disrupt existing sections.


69-70: Prop threading: LGTM

New props are correctly threaded through the component.


135-136: Content rendering: LGTM

Rendering content before checkbox/CTAs is sensible.


173-195: CTA children rendering: LGTM

Children rendered before left icon/text is fine and won’t break existing CTAs.

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

37-46: Error code branch likely unreachable here.

handleLogin throws codes 'LOGIN_CANCELED' and 'LOGIN_ERROR'; 'NO_PASSKEY' isn’t thrown in the login flow. Consider removing or aligning with actual error codes.

src/components/Common/ActionList.tsx (7)

21-28: LGTM on Redux wiring.

Importing useAppDispatch and setupActions is appropriate for passing inviteCode into setup flow.


34-35: Props surface addition looks good.

Optional isInviteLink keeps existing call sites unaffected.


45-51: Component signature update is fine.

No breaking change since isInviteLink is optional.


68-71: State for invite modal flow looks good.

selectedMethod/showInviteModal are scoped correctly.


165-171: LGTM on "Continue with Peanut" CTA.

Redirect preservation via redirect_uri is correct. No change requested.


182-189: Invite gating on method cards works as intended.

Modal interlock prevents users from accidentally skipping invite-based onboarding.


218-257: Invite modal UX copy and actions look good.

Join CTA preserves invite, secondary CTA proceeds with the chosen method. Clean state reset on close.

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

Caution

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

⚠️ Outside diff range comments (1)
src/components/Setup/Views/SetupPasskey.tsx (1)

38-49: Decode redirect_uri before sanitizing/pushing.

redirect_uri can arrive URL-encoded; decode first, then sanitize.

Apply this diff:

-                    const redirect_uri = searchParams.get('redirect_uri')
-                    if (redirect_uri) {
-                        const sanitizedRedirectUrl = sanitizeRedirectURL(redirect_uri)
-                        router.push(sanitizedRedirectUrl)
-                        return
-                    }
+                    const redirect_uri = searchParams.get('redirect_uri')
+                    if (redirect_uri) {
+                        let decodedRedirect = redirect_uri
+                        try {
+                            decodedRedirect = decodeURIComponent(redirect_uri)
+                        } catch {}
+                        const sanitizedRedirectUrl = sanitizeRedirectURL(decodedRedirect)
+                        router.push(sanitizedRedirectUrl)
+                        return
+                    }
🧹 Nitpick comments (18)
src/components/Setup/Setup.types.ts (1)

1-33: Prune stale 'join-beta' entries if unused.

Since the 'join-beta' step was removed from setupSteps, consider removing it from ScreenId and ScreenProps to keep the public surface clean.

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

27-76: Effect uses inviteCode/handleNext/searchParams but misses them in deps.

Add them to avoid stale closures.

Apply this diff:

-    }, [address, user, isFetchingUser])
+    }, [address, user, isFetchingUser, inviteCode, handleNext, searchParams])

21-23: Avoid calling useAuth() twice.

Destructure once to prevent duplicate subscriptions.

Apply this diff:

-    const { user, isFetchingUser } = useAuth()
-    const { addAccount } = useAuth()
+    const { user, isFetchingUser, addAccount } = useAuth()
src/components/Setup/Views/CollectEmail.tsx (2)

15-15: Normalize state setter name (readability).

Prefer setIsLoading for consistency.

Apply this diff and update references:

-    const [isLoading, setisLoading] = useState(false)
+    const [isLoading, setIsLoading] = useState(false)

Then replace setisLoading(...) with setIsLoading(...) on Lines 27, 29, 34, 61, 64.


41-41: Typo in placeholder.

Use “your”.

Apply this diff:

-                placeholder="Enter you email"
+                placeholder="Enter your email"
src/components/Setup/Setup.consts.tsx (1)

6-7: Remove unused import(s).

JoinBetaStep is imported but unused. Also consider removing peanutWithGlassesAnim if no longer used in this file.

Apply this diff:

-import { InstallPWA, SetupPasskey, SignupStep, JoinBetaStep, CollectEmail } from '@/components/Setup/Views'
+import { InstallPWA, SetupPasskey, SignupStep, CollectEmail } from '@/components/Setup/Views'
src/components/Setup/Views/JoinWaitlist.tsx (1)

96-101: Trim invite codes before storing.

Prevents subtle validation/accept issues from whitespace.

Apply this diff:

-                    onClick={() => {
-                        dispatch(setupActions.setInviteCode(inviteCode))
-                        handleNext()
-                    }}
+                    onClick={() => {
+                        dispatch(setupActions.setInviteCode(inviteCode.trim()))
+                        handleNext()
+                    }}
src/app/(mobile-ui)/home/page.tsx (1)

223-228: Increase tap target and add accessible name; use the StaticImport directly.

The star is a small 20x20 image wrapped directly in a Link, which makes the tap target too small on mobile. Also, prefer passing the StaticImport to Image (not .src) and add an aria-label.

Apply this diff:

-                        <Link href="/points">
-                            <Image src={starImage.src} alt="star" width={20} height={20} />
-                        </Link>
+                        <Link
+                            href="/points"
+                            aria-label="Open invites and points"
+                            className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-gray-100"
+                        >
+                            <Image src={starImage} alt="Points" width={20} height={20} />
+                        </Link>
src/components/Global/CopyToClipboard/CopyToClipboardButton.tsx (2)

16-22: Harden clipboard copy with fallback and error handling.

navigator.clipboard can be unavailable (non-secure context, older browsers). Add a fallback and catch errors.

-    const handleCopy = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
-        e.stopPropagation()
-        navigator.clipboard.writeText(textToCopy).then(() => {
-            setCopied(true)
-            setTimeout(() => setCopied(false), 2000)
-        })
-    }
+    const handleCopy = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
+        e.stopPropagation()
+        try {
+            if (navigator.clipboard && window.isSecureContext) {
+                await navigator.clipboard.writeText(textToCopy)
+            } else {
+                const el = document.createElement('textarea')
+                el.value = textToCopy
+                el.setAttribute('readonly', '')
+                el.style.position = 'absolute'
+                el.style.left = '-9999px'
+                document.body.appendChild(el)
+                el.select()
+                document.execCommand('copy')
+                document.body.removeChild(el)
+            }
+            setCopied(true)
+            setTimeout(() => setCopied(false), 2000)
+        } catch {
+            setCopied(false)
+        }
+    }

24-33: Minor UX: reflect “Copied!” state and guard empty input.

Show a success label for 2s and disable when textToCopy is empty.

         <Button
             size={size}
             className={className}
             onClick={handleCopy}
             icon={copied ? 'check' : 'copy'}
             shadowSize="4"
             variant="primary-soft"
+            disabled={!textToCopy}
         >
-            <p className="text-xs"> Copy code</p>
+            <p className="text-xs" aria-live="polite">{copied ? 'Copied!' : 'Copy code'}</p>
         </Button>
src/app/(mobile-ui)/points/page.tsx (3)

49-58: Guard Share button on valid inviteCode.

Avoids sharing a link with an empty code.

-                        <ShareButton
+                        {inviteCode && (
+                            <ShareButton
                             generateText={() =>
                                 Promise.resolve(
                                     `I’m using Peanut, an invite-only app for easy payments. With it you can pay friends, use merchants, and move money in and out of your bank, even cross-border. Here’s my invite: ${inviteLink}`
                                 )
                             }
                             title="Share your invite link"
-                        >
-                            Share Invite link
-                        </ShareButton>
+                            >
+                                Share Invite link
+                            </ShareButton>
+                        )}

99-109: Same: guard Share button when there’s no inviteCode.

-                        <ShareButton
+                        {inviteCode && (
+                            <ShareButton
                             generateText={() =>
                                 Promise.resolve(
                                     `I’m using Peanut, an invite-only app for easy payments. With it you can pay friends, use merchants, and move money in and out of your bank, even cross-border. Here’s my invite: ${inviteLink}`
                                 )
                             }
                             title="Share your invite link"
-                        >
-                            Share Invite link
-                        </ShareButton>
+                            >
+                                Share Invite link
+                            </ShareButton>
+                        )}

61-65: Type invites for safer access to invitee fields.

Replace any with a typed shape to avoid runtime surprises.

Example:

type Invite = {
  id: string
  invitee: { username: string; bridgeKycStatus: 'approved' | 'rejected' | 'pending' | string }
}

{inviteList.map((invite: Invite, i: number) => { ... })}
src/components/Profile/components/PublicProfile.tsx (5)

136-166: Remove unreachable branch inside guest‑only block.
Inside a container rendered only when !isLoggedIn, the {isLoggedIn ? … : …} check is dead code.

Apply:

-                        <Card position="single" className="z-10 mt-28 space-y-2 p-4 text-center">
-                            {isLoggedIn ? (
-                                <>
-                                    <h2 className="text-lg font-extrabold">You're all set</h2>
-                                    <p className="mx-auto max-w-[55%] text-sm">
-                                        Now send or request money to get started.
-                                    </p>
-                                </>
-                            ) : (
-                                <div className="space-y-4">
+                        <Card position="single" className="z-10 mt-28 space-y-2 p-4 text-center">
+                            <div className="space-y-4">
                                 <div className="space-y-2">
                                     <h2 className="text-lg font-extrabold">No invite, no Peanut</h2>
                                     <p>
                                         Peanut is invite-only.
                                         <br />
                                         Go beg your friend for an invite link!
                                     </p>
                                 </div>
-                                <ShareButton
-                                    generateText={() =>
-                                        Promise.resolve(
-                                            `Bro… I’m on my knees. Peanut is invite-only and I’m locked outside. Save my life and send me your invite`
-                                        )
-                                    }
-                                    title="Beg for an invite"
-                                >
-                                    Beg for an invite
-                                </ShareButton>
-                                </div>
-                            )}
+                                <ShareButton
+                                    generateText={() => Promise.resolve(INVITE_SHARE_TEXT)}
+                                    title="Beg for an invite"
+                                >
+                                    Beg for an invite
+                                </ShareButton>
+                            </div>

Additionally, deduplicate the share text; see constant suggestion below.


48-56: Avoid shadowing ‘user’ from auth context.
Rename the fetched variable for clarity.

-        usersApi.getByUsername(username).then((user) => {
-            if (user?.fullName) setFullName(user.fullName)
-            if (user?.bridgeKycStatus === 'approved') setIsKycVerified(true)
-            // to check if the logged in user has sent money to the profile user,
-            // we check the amount that the profile user has received from the logged in user.
-            if (user?.totalUsdReceivedFromCurrentUser) {
-                setTotalSentByLoggedInUser(user.totalUsdReceivedFromCurrentUser)
-            }
+        usersApi.getByUsername(username).then((profile) => {
+            if (profile?.fullName) setFullName(profile.fullName)
+            if (profile?.bridgeKycStatus === 'approved') setIsKycVerified(true)
+            // to check if the logged in user has sent money to the profile user,
+            // we check the amount that the profile user has received from the logged in user.
+            if (profile?.totalUsdReceivedFromCurrentUser) {
+                setTotalSentByLoggedInUser(profile.totalUsdReceivedFromCurrentUser)
+            }

31-61: Prefer numeric state for amounts to avoid repeated parsing.
Store totals as number and compute directly.

-    const [totalSentByLoggedInUser, setTotalSentByLoggedInUser] = useState<string>('0')
+    const [totalSentByLoggedInUser, setTotalSentByLoggedInUser] = useState<number>(0)
-            if (profile?.totalUsdReceivedFromCurrentUser) {
-                setTotalSentByLoggedInUser(profile.totalUsdReceivedFromCurrentUser)
-            }
+            if (profile?.totalUsdReceivedFromCurrentUser != null) {
+                setTotalSentByLoggedInUser(Number(profile.totalUsdReceivedFromCurrentUser) || 0)
+            }
-    const haveSentMoneyToUser = useMemo(() => Number(totalSentByLoggedInUser) > 0, [totalSentByLoggedInUser])
+    const haveSentMoneyToUser = useMemo(() => totalSentByLoggedInUser > 0, [totalSentByLoggedInUser])

148-165: Deduplicate share text and prep for i18n later.
Extract the invite share copy to a constant and reuse it here (and in the modal below).

-                                    <ShareButton
-                                        generateText={() =>
-                                            Promise.resolve(
-                                                `Bro… I’m on my knees. Peanut is invite-only and I’m locked outside. Save my life and send me your invite`
-                                            )
-                                        }
-                                        title="Beg for an invite"
-                                    >
-                                        Beg for an invite
-                                    </ShareButton>
+                                    <ShareButton
+                                        generateText={() => Promise.resolve(INVITE_SHARE_TEXT)}
+                                        title="Beg for an invite"
+                                    >
+                                        Beg for an invite
+                                    </ShareButton>

Add once near the top (after imports):

const INVITE_SHARE_TEXT =
  "Bro… I’m on my knees. Peanut is invite-only and I’m locked outside. Save my life and send me your invite";

188-208: Extract invite messages and enable newline rendering in ActionModal

  • Pull out both the modal description and share text into constants (e.g. INVITE_MODAL_DESCRIPTION, INVITE_SHARE_TEXT) instead of inlining.
  • Ensure “\n” renders as a line break by adding whitespace-pre-line (Tailwind) to the description container via descriptionClassName or passing a React fragment with <br/> tags.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6d430da and 67cd406.

📒 Files selected for processing (18)
  • src/app/(mobile-ui)/home/page.tsx (2 hunks)
  • src/app/(mobile-ui)/layout.tsx (2 hunks)
  • src/app/(mobile-ui)/points/page.tsx (1 hunks)
  • src/app/(setup)/layout.tsx (1 hunks)
  • src/app/invite/page.tsx (1 hunks)
  • src/components/Global/CopyToClipboard/CopyToClipboardButton.tsx (1 hunks)
  • src/components/Global/Icons/trophy.tsx (1 hunks)
  • src/components/Invites/InvitesPageLayout.tsx (1 hunks)
  • src/components/Invites/JoinWaitlistPage.tsx (1 hunks)
  • src/components/Profile/components/PublicProfile.tsx (5 hunks)
  • src/components/Profile/index.tsx (4 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 (2 hunks)
  • src/components/Setup/Views/index.ts (1 hunks)
  • src/services/invites.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/app/(mobile-ui)/layout.tsx
  • src/services/invites.ts
  • src/components/Profile/index.tsx
  • src/components/Invites/InvitesPageLayout.tsx
  • src/app/invite/page.tsx
  • src/components/Invites/JoinWaitlistPage.tsx
  • src/components/Global/Icons/trophy.tsx
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-09-18T09:30:42.901Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1230
File: src/app/(mobile-ui)/withdraw/page.tsx:92-97
Timestamp: 2025-09-18T09:30:42.901Z
Learning: In src/app/(mobile-ui)/withdraw/page.tsx, the useEffect that calls setShowAllWithdrawMethods(true) when amountFromContext exists is intentionally designed to run only on component mount (empty dependency array), not when amountFromContext changes. This is the correct behavior for the withdraw flow where showing all methods should only happen on initial load when an amount is already present.

Applied to files:

  • src/app/(mobile-ui)/home/page.tsx
  • src/app/(setup)/layout.tsx
📚 Learning: 2025-09-11T17:46:12.507Z
Learnt from: Hugo0
PR: peanutprotocol/peanut-ui#1200
File: src/app/(mobile-ui)/recover-funds/page.tsx:9-9
Timestamp: 2025-09-11T17:46:12.507Z
Learning: Functions in Next.js that are not marked with "use server" and contain secrets are unsafe to import in client components, as they get bundled into the client JavaScript and can leak environment variables to the browser.

Applied to files:

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

Applied to files:

  • src/app/(mobile-ui)/points/page.tsx
📚 Learning: 2025-05-13T10:05:24.057Z
Learnt from: kushagrasarathe
PR: peanutprotocol/peanut-ui#845
File: src/components/Request/link/views/Create.request.link.view.tsx:81-81
Timestamp: 2025-05-13T10:05:24.057Z
Learning: In the peanut-ui project, pages that handle request flows (like Create.request.link.view.tsx) are only accessible to logged-in users who will always have a username, making null checks for user?.user.username unnecessary in these contexts.

Applied to files:

  • src/app/(mobile-ui)/points/page.tsx
🧬 Code graph analysis (7)
src/app/(mobile-ui)/home/page.tsx (1)
src/components/SearchUsers/index.tsx (1)
  • SearchUsers (82-121)
src/components/Setup/Views/CollectEmail.tsx (2)
src/app/actions/users.ts (1)
  • updateUserById (12-35)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/components/Setup/Views/SetupPasskey.tsx (2)
src/redux/hooks.ts (1)
  • useSetupStore (9-9)
src/hooks/useSetupFlow.ts (1)
  • useSetupFlow (6-68)
src/components/Global/CopyToClipboard/CopyToClipboardButton.tsx (1)
src/components/0_Bruddle/Button.tsx (2)
  • ButtonSize (17-17)
  • Button (76-267)
src/components/Setup/Views/JoinWaitlist.tsx (7)
src/hooks/useZeroDev.ts (1)
  • useZeroDev (36-172)
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/services/invites.ts (1)
  • invitesApi (6-79)
src/utils/general.utils.ts (2)
  • getFromLocalStorage (126-148)
  • sanitizeRedirectURL (1220-1232)
src/redux/slices/setup-slice.ts (1)
  • setupActions (60-60)
src/app/(mobile-ui)/points/page.tsx (6)
src/services/invites.ts (1)
  • invitesApi (6-79)
src/context/authContext.tsx (1)
  • useAuth (182-188)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/components/Global/Card/index.tsx (1)
  • getCardPosition (14-19)
src/components/UserHeader/index.tsx (1)
  • VerifiedUserLabel (34-81)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (198-207)
src/app/(setup)/layout.tsx (3)
src/redux/hooks.ts (2)
  • useAppDispatch (5-5)
  • useSetupStore (9-9)
src/components/Setup/Setup.consts.tsx (1)
  • setupSteps (9-91)
src/redux/slices/setup-slice.ts (1)
  • setupActions (60-60)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (14)
src/components/Setup/Setup.types.ts (1)

13-13: Add 'collect-email' types: LGTM.

New ScreenId and ScreenProps entries look correct and align with the new step.

src/components/Setup/Views/index.ts (1)

6-6: Re-export of CollectEmail: LGTM.

Keeps the views barrel up to date.

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

21-28: Deps fix and invite-aware filtering: LGTM.

Including selectedStep and inviteCode in deps resolves staleness; gating out 'welcome' when deep-linking to signup with an invite is reasonable.

Please confirm this behavior is intended only when step=signup is present; otherwise, 'welcome' remains for non-invite flows.

src/components/Setup/Setup.consts.tsx (2)

49-56: Welcome step update to invite flow: LGTM.

Copy and component swap to JoinWaitlist fits the new gating.


80-90: New 'collect-email' step: LGTM.

Positioning after passkey aligns with SetupPasskey’s invite-based branch.

src/components/Setup/Views/JoinWaitlist.tsx (2)

52-65: Redirect handling on login: LGTM.

Decoding then sanitizing redirect_uri avoids malformed pushes.


106-110: Confirm intended flow without an invite.

“Join Waitlist” advances the setup flow; verify users without invite cannot inadvertently get full access and are correctly routed to collect-email/limited-access paths.

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

44-44: LGTM on static asset import.

Static image import is fine for Next/Image usage.

src/components/Global/CopyToClipboard/CopyToClipboardButton.tsx (1)

1-1: Client directive present.

Good catch—'use client' is correctly added.

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

26-29: Username guard implemented correctly.

Avoids rendering "undefinedINVITESYOU". Good.


19-22: Default invites to [] to avoid .length/.map on undefined.

Without a default, a failed/undefined data state can crash the page. Use a safe local default and update references.

Apply this diff:

     const router = useRouter()
+    const inviteList = invites ?? []
@@
-                {invites.length > 0 && (
+                {inviteList.length > 0 && (
@@
-                            {invites.map((invite: any, i: number) => {
+                            {inviteList.map((invite: any, i: number) => {
@@
-                                    <Card key={invite.id} position={getCardPosition(i, invites.length)}>
+                                    <Card key={invite.id} position={getCardPosition(i, inviteList.length)}>
@@
-                {invites.length === 0 && (
+                {inviteList.length === 0 && (

Also applies to: 47-48, 61-65, 89-111

src/components/Profile/components/PublicProfile.tsx (3)

20-21: Imports LGTM for invite/share UI.


37-37: State for invite modal is appropriate.


114-130: Previous “Request button regression” is addressed.
Logged‑in users with hasAppAccess now navigate to Request; others see the invite modal. Looks good.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/app/(mobile-ui)/home/page.tsx (1)

223-228: Increase tap‑target size and add accessible label for the points link

Make the star link a 40–44px tappable area (like SearchUsers) and add an accessible label. This improves mobile usability and a11y.

-                    <div className="flex items-center gap-2">
-                        <Link href="/points">
-                            <Image src={starImage} alt="star" width={20} height={20} />
-                        </Link>
-                        <SearchUsers />
-                    </div>
+                    <div className="flex items-center gap-2">
+                        <Link
+                            href="/points"
+                            aria-label="Points"
+                            className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-gray-100"
+                        >
+                            <Image src={starImage} alt="Points" width={20} height={20} />
+                        </Link>
+                        <SearchUsers />
+                    </div>

Please confirm the /points route exists on mobile to avoid dead links.

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

747-752: Don’t force invite flow for all claim links; derive isInviteLink from context

Passing bare isInviteLink makes it always true. Gate this only when the URL/context indicates an invite (e.g., a query param).

-                        <ActionList
+                        <ActionList
                             flow="claim"
                             claimLinkData={claimLinkData}
                             isLoggedIn={!!user?.user.userId}
-                            isInviteLink
+                            isInviteLink={searchParams.get('invite') === '1'}
                         />

If invite identification differs (e.g., from claimLinkData metadata), adjust accordingly.

src/components/Common/ActionList.tsx (2)

196-203: Clarify invite gating for anonymous users

isInviteLink && !userHasAppAccess treats anonymous users (undefined) as “no access,” gating all methods behind the invite modal. If that’s unintended, coerce to a strict boolean and gate only when logged in and hasAppAccess === false.

-                                if (isInviteLink && !userHasAppAccess) {
+                                if (isInviteLink && user && userHasAppAccess === false) {
                                     setSelectedMethod(method)
                                     setShowInviteModal(true)
                                 } else {
                                     handleMethodClick(method)
                                 }

Alternatively, set const userHasAppAccess = !!user?.user.hasAppAccess to remove tri‑state ambiguity.


180-186: Minor: improve star banner semantics

Consider using the static import directly (src={starImage}) and a more descriptive alt text (e.g., “Early access”).

-                    <Image src={starImage.src} alt="star" width={20} height={20} />{' '}
+                    <Image src={starImage} alt="Early access" width={20} height={20} />{' '}
...
-                    <Image src={starImage.src} alt="star" width={20} height={20} />
+                    <Image src={starImage} alt="Early access" width={20} height={20} />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67cd406 and 9118f50.

📒 Files selected for processing (4)
  • src/app/(mobile-ui)/home/page.tsx (2 hunks)
  • src/app/(mobile-ui)/layout.tsx (2 hunks)
  • src/components/Claim/Link/Initial.view.tsx (1 hunks)
  • src/components/Common/ActionList.tsx (6 hunks)
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-07-24T13:26:10.290Z
Learnt from: Hugo0
PR: peanutprotocol/peanut-ui#1014
File: src/components/Claim/Link/Initial.view.tsx:413-413
Timestamp: 2025-07-24T13:26:10.290Z
Learning: In the peanut-ui repository, the change from `${SQUID_API_URL}/route` to `${SQUID_API_URL}/v2/route` in src/components/Claim/Link/Initial.view.tsx was a typo fix, not an API migration, as the codebase was already using Squid API v2.

Applied to files:

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

Applied to files:

  • src/app/(mobile-ui)/home/page.tsx
📚 Learning: 2025-05-13T10:05:24.057Z
Learnt from: kushagrasarathe
PR: peanutprotocol/peanut-ui#845
File: src/components/Request/link/views/Create.request.link.view.tsx:81-81
Timestamp: 2025-05-13T10:05:24.057Z
Learning: In the peanut-ui project, pages that handle request flows (like Create.request.link.view.tsx) are only accessible to logged-in users who will always have a username, making null checks for user?.user.username unnecessary in these contexts.

Applied to files:

  • src/components/Common/ActionList.tsx
📚 Learning: 2024-12-02T17:19:18.532Z
Learnt from: jjramirezn
PR: peanutprotocol/peanut-ui#551
File: src/components/Request/Create/Views/Initial.view.tsx:151-156
Timestamp: 2024-12-02T17:19:18.532Z
Learning: In the `InitialView` component at `src/components/Request/Create/Views/Initial.view.tsx`, when setting the default chain and token in the `useEffect` triggered by `isPeanutWallet`, it's acceptable to omit the setters from the dependency array and not include additional error handling for invalid defaults.

Applied to files:

  • src/components/Common/ActionList.tsx
📚 Learning: 2025-09-05T07:31:11.396Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1185
File: src/components/Claim/useClaimLink.tsx:14-0
Timestamp: 2025-09-05T07:31:11.396Z
Learning: In the peanut-ui codebase, `window.history.replaceState` is preferred over `router.replace` when immediate/synchronous URL parameter updates are required, as `router.replace` is asynchronous and doesn't guarantee instant URL changes that subsequent code can rely on. This pattern is used consistently across usePaymentInitiator.ts, Confirm.payment.view.tsx, and useClaimLink.tsx.

Applied to files:

  • src/components/Common/ActionList.tsx
📚 Learning: 2024-10-23T09:38:27.670Z
Learnt from: jjramirezn
PR: peanutprotocol/peanut-ui#469
File: src/app/request/pay/page.tsx:32-64
Timestamp: 2024-10-23T09:38:27.670Z
Learning: In `src/app/request/pay/page.tsx`, if `linkRes` is not OK in the `generateMetadata` function, the desired behavior is to use the standard title and preview image without throwing an error.

Applied to files:

  • src/components/Common/ActionList.tsx
🧬 Code graph analysis (3)
src/components/Claim/Link/Initial.view.tsx (1)
src/components/Common/ActionList.tsx (1)
  • ActionList (46-274)
src/app/(mobile-ui)/home/page.tsx (1)
src/components/SearchUsers/index.tsx (1)
  • SearchUsers (82-121)
src/components/Common/ActionList.tsx (6)
src/services/sendLinks.ts (1)
  • ClaimLinkData (62-62)
src/lib/url-parser/types/payment.ts (1)
  • ParsedURL (7-16)
src/constants/actionlist.consts.ts (1)
  • PaymentMethod (5-11)
src/context/authContext.tsx (1)
  • useAuth (182-188)
src/redux/hooks.ts (1)
  • useAppDispatch (5-5)
src/redux/slices/setup-slice.ts (1)
  • setupActions (60-60)
🔇 Additional comments (5)
src/app/(mobile-ui)/home/page.tsx (1)

44-44: LGTM: static image import

The star asset import looks correct for next/image static usage.

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

98-101: Don’t override public paths with waitlist; gate only on private paths (duplicate of prior feedback)

Show JoinWaitlistPage only for logged‑in users without access and not on public paths.

-    // Show waitlist page if user doesn't have app access
-    if (!isFetchingUser && user && !user?.user.hasAppAccess) {
+    // Show waitlist page only for logged-in users without access, and not on public paths
+    if (!isFetchingUser && user && user.user.hasAppAccess === false && !isPublicPath) {
         return <JoinWaitlistPage />
     }

25-25: LGTM: waitlist page import

Import looks correct.


28-28: No /join-waitlist route under src/app; publicPathRegex is correct as-is.

src/components/Common/ActionList.tsx (1)

21-36: LGTM: props and imports expanded for invite flow

New props and imports (dispatch, setupActions, useAuth) integrate cleanly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
src/app/(setup)/setup/page.tsx (1)

147-152: Fix direction regression when steps change

Line 147 puts currentStepIndex in the effect dependency array while mutating it inside the same effect. After every step change the effect runs twice; on the second run newIndex === currentStepIndex, so setDirection always falls back to -1, reversing the animation even when advancing. Please compute the new index against the previous value in a single pass (and drop currentStepIndex from the deps) so the direction reflects the actual transition.

-    useEffect(() => {
-        if (step) {
-            const newIndex = steps.findIndex((s: ISetupStep) => s.screenId === step.screenId)
-            setDirection(newIndex > currentStepIndex ? 1 : -1)
-            setCurrentStepIndex(newIndex)
-        }
-    }, [step, currentStepIndex, steps])
+    useEffect(() => {
+        if (!step) return
+
+        const newIndex = steps.findIndex((s: ISetupStep) => s.screenId === step.screenId)
+        if (newIndex === -1) return
+
+        setCurrentStepIndex((prevIndex) => {
+            setDirection(newIndex > prevIndex ? 1 : newIndex < prevIndex ? -1 : 0)
+            return newIndex
+        })
+    }, [step, steps])
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d26f18 and fcec045.

📒 Files selected for processing (5)
  • src/app/(setup)/layout.tsx (1 hunks)
  • src/app/(setup)/setup/page.tsx (2 hunks)
  • src/app/invite/page.tsx (1 hunks)
  • src/components/Common/ActionList.tsx (6 hunks)
  • src/components/Setup/Views/CollectEmail.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/(setup)/layout.tsx
🧰 Additional context used
🧠 Learnings (3)
📚 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/Setup/Views/CollectEmail.tsx
📚 Learning: 2025-06-22T16:10:53.167Z
Learnt from: kushagrasarathe
PR: peanutprotocol/peanut-ui#915
File: src/hooks/useKycFlow.ts:96-124
Timestamp: 2025-06-22T16:10:53.167Z
Learning: The `initiateKyc` function in `src/app/actions/users.ts` already includes comprehensive error handling with try-catch blocks and returns structured responses with either `{ data }` or `{ error }` fields, so additional try-catch blocks around its usage are not needed.

Applied to files:

  • src/components/Setup/Views/CollectEmail.tsx
📚 Learning: 2025-08-26T17:38:37.055Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1132
File: src/components/Common/ActionList.tsx:153-156
Timestamp: 2025-08-26T17:38:37.055Z
Learning: In ActionList.tsx, when there are circular dependency concerns with ACTION_METHODS being imported by other components, the preferred solution is to move ACTION_METHODS to a separate constants file (like src/constants/actionlist.consts.ts) rather than using prop drilling. This centralizes constants management and creates a cleaner dependency graph.

Applied to files:

  • src/components/Common/ActionList.tsx
🧬 Code graph analysis (4)
src/components/Setup/Views/CollectEmail.tsx (2)
src/app/actions/users.ts (1)
  • updateUserById (12-35)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/app/(setup)/setup/page.tsx (1)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/components/Common/ActionList.tsx (6)
src/services/sendLinks.ts (1)
  • ClaimLinkData (62-62)
src/lib/url-parser/types/payment.ts (1)
  • ParsedURL (7-16)
src/constants/actionlist.consts.ts (1)
  • PaymentMethod (5-11)
src/context/authContext.tsx (1)
  • useAuth (182-188)
src/redux/hooks.ts (1)
  • useAppDispatch (5-5)
src/redux/slices/setup-slice.ts (1)
  • setupActions (60-60)
src/app/invite/page.tsx (4)
src/redux/hooks.ts (1)
  • useAppDispatch (5-5)
src/services/invites.ts (1)
  • invitesApi (6-79)
src/redux/slices/setup-slice.ts (1)
  • setupActions (60-60)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (2)
src/components/Setup/Views/CollectEmail.tsx (1)

73-73: Ensure ErrorAlert actually renders when there is an error

This repeats the earlier review note: because Line 73 starts with !!error || …, React short-circuits to the boolean when error is truthy, so the ErrorAlert never renders. Wrap the component behind a boolean guard instead.

-            {!!error || (!isValid && !isChanging && !!email && <ErrorAlert description={error || 'Invalid email'} />)}
+            {(!!error || (!isValid && !isChanging && !!email)) && (
+                <ErrorAlert description={error || 'Invalid email'} />
+            )}
src/components/Common/ActionList.tsx (1)

157-163: Do not fabricate invite codes when the username is missing

Lines 157-163 concatenate ${username}INVITESYOU without checking whether username is truthy. When the upstream payload lacks a username (which happens on some links), we end up storing “INVITESYOU” as the invite code and send the user into setup with an invalid code. Please fall back to the non-invite path unless you have a real username.

-        if (isInviteLink && !userHasAppAccess) {
-            const rawUsername =
-                flow === 'request' ? requestLinkData?.recipient?.identifier : claimLinkData?.sender?.username
-            const username = rawUsername ? rawUsername.toUpperCase() : ''
-            const inviteCode = `${username}INVITESYOU`
-            dispatch(setupActions.setInviteCode(inviteCode))
-            router.push(`/setup?step=signup&redirect_uri=${redirectUri}`)
-        } else {
-            router.push(`/setup?redirect_uri=${redirectUri}`)
-        }
+        if (isInviteLink && !userHasAppAccess) {
+            const rawUsername =
+                flow === 'request' ? requestLinkData?.recipient?.identifier : claimLinkData?.sender?.username
+            const username = rawUsername ? rawUsername.toUpperCase() : ''
+            if (username) {
+                const inviteCode = `${username}INVITESYOU`
+                dispatch(setupActions.setInviteCode(inviteCode))
+                router.push(`/setup?step=signup&redirect_uri=${redirectUri}`)
+                return
+            }
+        }
+
+        router.push(`/setup?redirect_uri=${redirectUri}`)

Copy link
Contributor

@Hugo0 Hugo0 left a comment

Choose a reason for hiding this comment

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

Good PR overall!

My main concerns:

  1. No heart icon for invitees in app - this should always show up next to verified double tick when you see that person
  2. im not sure about the flow of accepting invite and jwt

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (8)
src/app/(setup)/setup/page.tsx (1)

27-148: Re-run the setup initializer when inviteCode changes.

inviteCode drives the branch that jumps straight to the signup step, but the effect never re-executes when that value updates because it isn’t in the dependency array. If the user enters an invite after the initial run, we still show the welcome step. Include inviteCode (and keep dispatch) in the dependencies so the initializer reruns when the code arrives.

-    }, [dispatch, steps])
+    }, [dispatch, steps, inviteCode])
src/app/(mobile-ui)/layout.tsx (1)

97-100: Don’t override public routes with the waitlist gate.

This renders JoinWaitlistPage for every route whenever the user lacks hasAppAccess, including /invite, /claim, etc. The earlier feedback asked to keep those public surfaces accessible. Add an !isPublicPath guard before rendering the waitlist page so public paths continue to work.

-    if (!isFetchingUser && user && !user?.user.hasAppAccess) {
+    if (!isFetchingUser && user && !user?.user.hasAppAccess && !isPublicPath) {
         return <JoinWaitlistPage />
     }
src/components/Profile/components/PublicProfile.tsx (1)

122-138: Harden Request button logic; encode username; only block when hasAppAccess is explicitly false.

The logic still needs hardening as previously noted:

  1. URL-encode the username to prevent unsafe characters in the URL.
  2. Only block navigation when hasAppAccess is explicitly false (not undefined), to avoid showing the modal while auth context is still loading.
  3. Separate handling for not logged in vs. no access cases.

Apply this diff:

-                        <Button
-                            onClick={() => {
-                                if (isLoggedIn && user?.user.hasAppAccess) {
-                                    router.push(`/request/${username}`)
-                                } else {
-                                    setShowInviteModal(true)
-                                }
-                            }}
-                            variant="purple"
-                            shadowSize="4"
-                            className="flex w-1/2 items-center justify-center gap-2 rounded-full py-3"
-                        >
+                        <Button
+                            onClick={() => {
+                                if (!isLoggedIn) {
+                                    setShowInviteModal(true)
+                                    return
+                                }
+                                if (user?.user?.hasAppAccess === false) {
+                                    setShowInviteModal(true)
+                                } else {
+                                    router.push(`/request/${encodeURIComponent(username)}`)
+                                }
+                            }}
+                            variant="purple"
+                            shadowSize="4"
+                            className="flex w-1/2 items-center justify-center gap-2 rounded-full py-3"
+                        >
                             <div className="flex size-5 items-center justify-center">
                                 <Icon name="arrow-down-left" size={8} fill="black" />
                             </div>
                             <span className="font-bold">Request</span>
                         </Button>
src/app/(mobile-ui)/home/page.tsx (1)

222-227: Accessibility issue still present.

The Link wrapping InvitesIcon still lacks an accessible label. This was flagged in a previous review and remains unaddressed.

Apply this diff to add an accessible label:

-                    <div className="flex items-center gap-2">
-                        <Link href="/points">
+                    <div className="flex items-center gap-2">
+                        <Link href="/points" aria-label="View invites and points">
                             <InvitesIcon />
                         </Link>
                         <SearchUsers />
src/app/(mobile-ui)/points/page.tsx (1)

28-30: Guard against empty inviteCode display.

While the username check prevents "undefinedINVITESYOU", the UI at lines 44-46 will still render an empty inviteCode when username is falsy. This could show an empty Card with just a copy button.

Consider wrapping the invite code Card (lines 42-47) in a conditional check:

                 <h1 className="font-bold">Invite friends with your code</h1>
-                <div className="flex w-full items-center justify-between gap-3">
-                    <Card className="flex w-full items-center justify-between py-3.5">
-                        <p className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold md:text-base">{`${inviteCode}`}</p>
-                        <CopyToClipboard textToCopy={inviteCode} />
-                    </Card>
-                </div>
+                {inviteCode && (
+                    <div className="flex w-full items-center justify-between gap-3">
+                        <Card className="flex w-full items-center justify-between py-3.5">
+                            <p className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold md:text-base">
+                                {inviteCode}
+                            </p>
+                            <CopyToClipboard textToCopy={inviteCode} />
+                        </Card>
+                    </div>
+                )}
src/components/Profile/index.tsx (1)

33-34: Guard inviteCode against undefined usernames.

Using user?.user.username directly can produce "undefinedINVITESYOU". Derive from the computed username variable (line 31) instead.

Apply this diff:

-    const inviteCode = `${user?.user.username?.toUpperCase()}INVITESYOU`
+    const inviteCode = `${username.toUpperCase()}INVITESYOU`
src/components/Common/ActionList.tsx (1)

194-211: Only dispatch invite code when username is truthy.

If rawUsername is falsy or an empty string, username becomes '', and inviteCode becomes 'INVITESYOU' (invalid). Dispatch should only occur when username is non-empty.

Apply this diff:

         if (isInviteLink && !userHasAppAccess && rawUsername) {
             const username = rawUsername ? rawUsername.toUpperCase() : ''
-            const inviteCode = `${username}INVITESYOU`
-            dispatch(setupActions.setInviteCode(inviteCode))
-            dispatch(setupActions.setInviteType(EInviteType.PAYMENT_LINK))
-            router.push(`/setup?step=signup&redirect_uri=${redirectUri}`)
+            if (username) {
+                const inviteCode = `${username}INVITESYOU`
+                dispatch(setupActions.setInviteCode(inviteCode))
+                dispatch(setupActions.setInviteType(EInviteType.PAYMENT_LINK))
+                router.push(`/setup?step=signup&redirect_uri=${redirectUri}`)
+            } else {
+                router.push(`/setup?redirect_uri=${redirectUri}`)
+            }
         } else {
             router.push(`/setup?redirect_uri=${redirectUri}`)
         }
src/components/Common/ActionListDaimoPayButton.tsx (1)

178-195: Fix duplicate payment initiation.

handleLoseInvite calls handleInitiateDaimoPayment (line 185), then triggers daimoPayButtonClickRef.current() (line 188). This invokes onClick, which calls onBeforeShow, which calls handleInitiateDaimoPayment again (line 150), resulting in two payment initiations.

Solution: Remove the direct call to handleInitiateDaimoPayment from handleLoseInvite. Instead, set confirmLoseInvite to true and trigger the Daimo button click, allowing onBeforeShow to perform the single payment initiation.

Apply this diff:

                 handleLoseInvite={async () => {
                     setShowInviteModal(false)
                     setConfirmLoseInvite(true)
-                    // Directly initiate the Daimo payment instead of triggering button click
-                    const success = await handleInitiateDaimoPayment()
-                    if (success && daimoPayButtonClickRef.current) {
-                        // Only trigger the actual Daimo widget if payment initiation was successful
-                        daimoPayButtonClickRef.current()
-                    }
+                    // Trigger Daimo button; onBeforeShow will perform the single initiation
+                    daimoPayButtonClickRef.current?.()
                 }}
🧹 Nitpick comments (5)
src/app/(mobile-ui)/layout.tsx (1)

102-102: Remove stray console log.

console.log(user, 'user') looks like leftover debugging—drop it before shipping.

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

156-172: Consider extracting the hardcoded invite text to avoid duplication.

The "beg for an invite" message text is duplicated here and in the ActionModal (lines 206-209). Consider extracting it to a constant or using a utility function (e.g., generateInvitesShareText) to maintain consistency.

Example:

+const INVITE_BEG_TEXT = `Bro… I'm on my knees. Peanut is invite-only and I'm locked outside. Save my life and send me your invite`
+
                                    <ShareButton
-                                        generateText={() =>
-                                            Promise.resolve(
-                                                `Bro… I'm on my knees. Peanut is invite-only and I'm locked outside. Save my life and send me your invite`
-                                            )
-                                        }
+                                        generateText={() => Promise.resolve(INVITE_BEG_TEXT)}
                                        title="Beg for an invite"
                                    >
src/app/(mobile-ui)/points/page.tsx (2)

44-44: Remove redundant template literal.

The template literal wrapper around inviteCode is unnecessary.

-                        <p className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold md:text-base">{`${inviteCode}`}</p>
+                        <p className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold md:text-base">{inviteCode}</p>

93-95: Extract invite share text into a shared constant
The sentence "Send your invite link to start earning more rewards" is currently defined only in src/app/(mobile-ui)/points/page.tsx; centralize it (e.g. in a constants or i18n file) for consistent reuse across other invite flows (profile modal, public profile).

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

65-71: Consider omitting the dummy href.

Using href="/dummy" is confusing. Since onClick is the primary handler, either omit href or use href="#" to clarify it's not a navigation target.

Apply this diff:

                    <ProfileMenuItem
                        icon="smile"
                        label="Invite friends to Peanut"
                        onClick={() => setIsInviteFriendsModalOpen(true)}
-                       href="/dummy" // Dummy link, wont be called
+                       href="#"
                        position="single"
                    />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 08dd943 and 0ee0f27.

📒 Files selected for processing (18)
  • src/app/(mobile-ui)/home/page.tsx (2 hunks)
  • src/app/(mobile-ui)/layout.tsx (2 hunks)
  • src/app/(mobile-ui)/points/page.tsx (1 hunks)
  • src/app/(setup)/layout.tsx (1 hunks)
  • src/app/(setup)/setup/page.tsx (3 hunks)
  • src/app/[...recipient]/client.tsx (1 hunks)
  • src/components/Claim/Link/Initial.view.tsx (1 hunks)
  • src/components/Common/ActionList.tsx (7 hunks)
  • src/components/Common/ActionListDaimoPayButton.tsx (4 hunks)
  • src/components/Global/CopyToClipboard/index.tsx (1 hunks)
  • src/components/Profile/components/PublicProfile.tsx (5 hunks)
  • src/components/Profile/index.tsx (4 hunks)
  • src/components/Setup/Views/CollectEmail.tsx (1 hunks)
  • src/components/Setup/Views/JoinWaitlist.tsx (1 hunks)
  • src/context/authContext.tsx (1 hunks)
  • src/interfaces/interfaces.ts (1 hunks)
  • src/services/services.types.ts (2 hunks)
  • src/utils/general.utils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/utils/general.utils.ts
  • src/interfaces/interfaces.ts
  • src/services/services.types.ts
  • src/components/Claim/Link/Initial.view.tsx
  • src/components/Setup/Views/JoinWaitlist.tsx
  • src/context/authContext.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 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/app/(mobile-ui)/layout.tsx
🧬 Code graph analysis (11)
src/components/Global/CopyToClipboard/index.tsx (1)
src/components/0_Bruddle/Button.tsx (2)
  • ButtonSize (17-17)
  • Button (76-267)
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/(setup)/layout.tsx (4)
src/redux/hooks.ts (1)
  • useAppDispatch (5-5)
src/components/Setup/Setup.consts.tsx (1)
  • setupSteps (9-91)
src/redux/slices/setup-slice.ts (1)
  • setupActions (65-65)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/components/Profile/index.tsx (2)
src/context/authContext.tsx (1)
  • useAuth (183-189)
src/utils/general.utils.ts (1)
  • generateInvitesShareText (1323-1325)
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 (198-207)
src/app/(setup)/setup/page.tsx (3)
src/redux/hooks.ts (1)
  • useSetupStore (9-9)
src/components/Setup/Setup.types.ts (1)
  • ISetupStep (41-53)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/components/Common/ActionList.tsx (4)
src/constants/actionlist.consts.ts (1)
  • PaymentMethod (5-11)
src/context/authContext.tsx (1)
  • useAuth (183-189)
src/redux/hooks.ts (1)
  • useAppDispatch (5-5)
src/redux/slices/setup-slice.ts (1)
  • setupActions (65-65)
src/app/(mobile-ui)/layout.tsx (1)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/app/(mobile-ui)/points/page.tsx (8)
src/context/authContext.tsx (1)
  • useAuth (183-189)
src/services/invites.ts (1)
  • invitesApi (7-83)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/utils/general.utils.ts (1)
  • generateInvitesShareText (1323-1325)
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 (41-88)
src/components/Global/Icons/Icon.tsx (1)
  • Icon (198-207)
src/components/Setup/Views/CollectEmail.tsx (3)
src/context/authContext.tsx (1)
  • useAuth (183-189)
src/app/actions/users.ts (1)
  • updateUserById (12-35)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/app/(mobile-ui)/home/page.tsx (1)
src/components/SearchUsers/index.tsx (1)
  • SearchUsers (82-121)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (26)
src/components/Profile/components/PublicProfile.tsx (3)

20-21: LGTM!

The new imports are correctly added to support the invite-gated flows.


38-38: LGTM!

The state variable is correctly defined for controlling the invite modal visibility.


196-216: Verify ActionModal newline rendering
Unable to locate the ActionModal implementation; please confirm that the description prop’s \n characters render correctly as line breaks (e.g., via <br/> conversion or white-space: pre-line CSS).

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

43-43: LGTM!

The import statement is correct and the component path follows the project's component organization pattern.

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

1-18: LGTM!

All imports are correctly structured and follow the project's organization patterns. The use of explicit type imports (e.g., Invite) and utility functions (e.g., generateInvitesShareText) demonstrates good separation of concerns.


22-26: Good implementation of query scoping.

The query key is properly scoped to the userId, which ensures correct cache invalidation per user. The enabled guard prevents unnecessary API calls when user data isn't available.

Based on learnings: This aligns with @tanstack/query best practices for stable query keys and conditional fetching.


32-34: LGTM!

The loading state appropriately covers the full screen and provides a clear visual indicator while data is being fetched.


51-56: Good use of share utility.

The share functionality correctly leverages the centralized generateInvitesShareText utility, promoting consistency across the app.


59-82: Proper typing and safe access patterns.

The code correctly uses the Invite type and safely accesses nested properties. The getCardPosition utility ensures proper visual grouping of list items.


86-103: Good empty state UX.

The empty state provides clear messaging and an actionable CTA to share the invite link. The trophy icon reinforces the rewards/gamification aspect of the invites feature.

src/components/Global/CopyToClipboard/index.tsx (2)

4-4: LGTM: Clean prop extension for button mode.

The new type and buttonSize props are well-typed and preserve backward compatibility with the default type='icon'.

Also applies to: 11-15


18-18: LGTM: Button mode correctly implemented.

The event type is appropriately broadened, and the conditional rendering cleanly adds button support while preserving the existing icon behavior.

Also applies to: 26-39

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

497-499: LGTM: Invite link correctly gated to username request flows.

The conditional logic properly restricts invite behavior to request_pay flows with USERNAME recipients, and optional chaining prevents crashes.

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

10-12: LGTM: Imports and state for invite modals.

All new imports are used later in the file, and state initialization is correct.

Also applies to: 16-17, 21-22


138-177: LGTM: Invite Friends modal structure is correct.

The modal content correctly uses CopyToClipboard and ShareButton components. However, ensure the inviteCode fix (lines 33-34) is applied so the modal displays a valid code.

src/components/Common/ActionList.tsx (7)

21-21: LGTM: Invite link prop and imports.

The isInviteLink prop is correctly added with a default value of false, preserving backward compatibility.

Also applies to: 27-31, 40-40, 51-57


82-83: LGTM: State and dispatch for invite flow.

State initialization and useAppDispatch hook are correct.

Also applies to: 86-86


213-214: LGTM: Username and access flag correctly computed.

Optional chaining and nullish coalescing prevent crashes for guests.


227-227: LGTM: Button and invite banner.

The banner is correctly guarded with && username to prevent displaying "Invited by undefined".

Also applies to: 235-241


246-253: LGTM: Daimo button receives invite gating flag.

The showConfirmModal prop correctly gates the invite modal for Daimo payment flows.


257-264: LGTM: Method click correctly gated for invite flows.

The conditional logic properly opens the invite modal when necessary before proceeding with payment.


293-308: LGTM: ConfirmInviteModal correctly wired.

The modal handlers properly manage state and invoke the appropriate actions for continue-with-peanut vs. lose-invite flows.

src/components/Common/ActionListDaimoPayButton.tsx (4)

1-1: LGTM: Props and imports for invite flow.

The new ActionListDaimoPayButtonProps interface correctly exposes showConfirmModal to gate the invite modal.

Also applies to: 12-19


31-33: LGTM: State and ref for invite modal flow.

State and ref initialization are correct for coordinating the invite modal with the Daimo payment flow.


144-151: LGTM: onBeforeShow correctly gates invite modal.

The logic now checks showConfirmModal to conditionally show the invite modal, and payment initiation occurs only once inside onBeforeShow.


160-162: LGTM: DaimoPayButton onClick captured in ref.

Storing the onClick handler allows the invite modal to trigger the Daimo widget after payment initiation.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0ee0f27 and 0e4fa50.

⛔ Files ignored due to path filters (1)
  • public/arrows/top-right-arrow-2.svg is excluded by !**/*.svg
📒 Files selected for processing (4)
  • src/app/api/og/route.tsx (2 hunks)
  • src/app/invite/page.tsx (1 hunks)
  • src/components/Invites/InvitesPage.tsx (1 hunks)
  • src/components/og/InviteCardOG.tsx (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 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/app/invite/page.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
🧬 Code graph analysis (3)
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 (89-95)
src/app/api/og/route.tsx (1)
src/components/og/InviteCardOG.tsx (1)
  • InviteCardOG (6-142)
src/components/Invites/InvitesPage.tsx (5)
src/context/authContext.tsx (1)
  • useAuth (183-189)
src/redux/hooks.ts (1)
  • useAppDispatch (5-5)
src/services/invites.ts (1)
  • invitesApi (7-83)
src/redux/slices/setup-slice.ts (1)
  • setupActions (65-65)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (16)
src/app/invite/page.tsx (4)

1-7: LGTM: Imports and dynamic config are correct.

The force-dynamic export ensures fresh invite validation on each request, which is appropriate for this page.


9-21: LGTM: Invite code validation helper is correct.

The function correctly validates the invite code and returns username on success or null on failure.


60-87: LGTM: Metadata structure is correct.

The dynamic OG metadata with conditional metadataBase, OpenGraph, and Twitter card configuration is properly implemented.


89-91: LGTM: Default export is correct.

The page component correctly renders the InvitesPage component. Invite validation and error handling logic is delegated to InvitesPage.

src/components/Invites/InvitesPage.tsx (6)

1-16: LGTM: Imports are correct.

All necessary dependencies are imported for the invite page functionality.


17-33: LGTM: Invite validation query is correctly configured.

The useQuery hook properly validates the invite code with an enabled guard to prevent unnecessary calls.


35-41: LGTM: Invite claim logic is correct.

The function properly dispatches invite code and type to Redux and navigates to the signup step. The hardcoded EInviteType.PAYMENT_LINK appears intentional for this invite flow.


43-58: LGTM: Loading and error states are correct.

The loading state and error handling with redirect to /setup (which contains waitlist) are properly implemented. Based on learnings.


89-95: LGTM: Suspense wrapper is correct.

The Suspense boundary properly handles the async searchParams access with an appropriate loading fallback.


60-87: LGTM: Invite claim and logout buttons correctly trigger navigation and auth actions.

src/app/api/og/route.tsx (3)

13-13: LGTM: InviteCardOG import is correct.


77-83: LGTM: Arrow sources object is correct.

The arrowSrcs object provides the necessary SVG paths for the invite card rendering. Note that a similar object is defined again on lines 150-155 for receipt rendering, but this duplication is acceptable given the different arrow sets needed.


85-105: LGTM: Invite OG card rendering is correct.

The conditional rendering for invite cards is properly implemented with correct dimensions, fonts, and props passed to InviteCardOG.

src/components/og/InviteCardOG.tsx (3)

1-4: LGTM: Username width calculation is acceptable.

The heuristic approach to calculating scribble width based on character count is reasonable for OG image rendering. While more precise font metrics could be used, the current implementation is sufficient.


6-79: LGTM: Username and scribble layout is correct.

The component properly renders the username with an overlaid scribble image. The empty alt attribute for the decorative scribble is correct and prevents layout issues. Based on learnings.


81-142: LGTM: Action text and branding section is correct.

The invitation text and branding elements are properly rendered with appropriate fonts, spacing, and alt attributes. The conditional arrows and descriptive alt text for the icon/logo follow best practices.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (3)
src/components/Common/ActionListDaimoPayButton.tsx (1)

144-150: Duplicate payment initiation remains: two charge attempts.

The flow calls handleInitiateDaimoPayment twice:

  1. Line 185 in handleLoseInvite → creates charge.
  2. Line 188 triggers daimoPayButtonClickRef.current() → calls onClick → calls onBeforeShow again.
  3. Line 150 in onBeforeShow (second call, confirmLoseInvite now true) → creates charge again.

This creates two "Charge Created" attempts, as flagged in previous reviews.

Apply the suggested fix from the previous review: remove the direct call to handleInitiateDaimoPayment from handleLoseInvite and ensure initiation happens exactly once inside onBeforeShow:

                onBeforeShow={async () => {
-                    if (!confirmLoseInvite && showConfirmModal) {
+                    // Non‑invite flows: proceed normally
+                    if (!showConfirmModal) {
+                        return await handleInitiateDaimoPayment()
+                    }
+                    // Invite flows: gate on first click, then initiate exactly once
+                    if (!confirmLoseInvite) {
                        setShowInviteModal(true)
                        return false
                    }
-                    // Don't reset confirmLoseInvite here - let it be reset only when modal is closed or payment is initiated
+                    setConfirmLoseInvite(false) // user confirmed losing invite; proceed
                    return await handleInitiateDaimoPayment()
                }}
                handleLoseInvite={async () => {
                    setShowInviteModal(false)
                    setConfirmLoseInvite(true)
-                    // Directly initiate the Daimo payment instead of triggering button click
-                    const success = await handleInitiateDaimoPayment()
-                    if (success && daimoPayButtonClickRef.current) {
-                        // Only trigger the actual Daimo widget if payment initiation was successful
-                        daimoPayButtonClickRef.current()
-                    }
+                    // Trigger Daimo button; onBeforeShow will perform the single initiation
+                    daimoPayButtonClickRef.current?.()
                }}

This ensures handleInitiateDaimoPayment is called exactly once (in onBeforeShow) and the widget opens only after successful charge creation.

Also applies to: 181-189

src/components/Setup/Views/JoinWaitlist.tsx (2)

32-48: Good fix: loading is always cleared via finally.

This resolves the previous “stuck loading” issue when validation throws.


66-73: Nice: decode then sanitize redirect_uri before push.

Decoding the query param first avoids malformed encoded paths.

🧹 Nitpick comments (2)
src/components/Setup/Views/JoinWaitlist.tsx (2)

113-121: Block “Next” while validation is pending and only persist invite code after it’s valid.

Race: with debounce, users can click before validation completes; we advance and persist an invalid code. Gate progression using handleNext(callback) and disable while isChanging.

-            <Button
-                disabled={(inviteCode.length !== 0 && !!error) || isLoading}
-                onClick={() => {
-                    if (inviteCode.length !== 0) {
-                        dispatch(setupActions.setInviteCode(inviteCode))
-                    }
-
-                    handleNext()
-                }}
+            <Button
+                disabled={(inviteCode.length !== 0 && (isChanging || !!error || isLoading)) || isLoading}
+                onClick={() => {
+                    if (inviteCode.length === 0) {
+                        handleNext()
+                        return
+                    }
+                    handleNext(async () => {
+                        const ok = await validateInviteCode(inviteCode)
+                        if (ok) {
+                            dispatch(setupActions.setInviteCode(inviteCode))
+                        }
+                        return ok
+                    })
+                }}

22-22: Nit: rename setisLoading → setIsLoading (React state setter convention).

Improves readability and consistency.

-    const [isLoading, setisLoading] = useState(false)
+    const [isLoading, setIsLoading] = useState(false)

Also update usages:

-            setisLoading(true)
+            setIsLoading(true)
...
-            setisLoading(false)
+            setIsLoading(false)
...
-                disabled={(inviteCode.length !== 0 && !!error) || isLoading}
+                disabled={(inviteCode.length !== 0 && !!error) || isLoading}

Also applies to: 35-35, 46-46, 114-114

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0e4fa50 and e89dca0.

📒 Files selected for processing (4)
  • src/app/api/og/route.tsx (2 hunks)
  • src/app/invite/page.tsx (1 hunks)
  • src/components/Common/ActionListDaimoPayButton.tsx (4 hunks)
  • src/components/Setup/Views/JoinWaitlist.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/invite/page.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 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
🧬 Code graph analysis (3)
src/app/api/og/route.tsx (1)
src/components/og/InviteCardOG.tsx (1)
  • InviteCardOG (6-142)
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/components/Setup/Views/JoinWaitlist.tsx (5)
src/hooks/useZeroDev.ts (1)
  • useZeroDev (36-179)
src/components/0_Bruddle/Toast.tsx (1)
  • useToast (111-117)
src/hooks/useSetupFlow.ts (1)
  • useSetupFlow (6-68)
src/services/invites.ts (1)
  • invitesApi (7-83)
src/utils/general.utils.ts (2)
  • getFromLocalStorage (112-134)
  • sanitizeRedirectURL (1217-1229)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (4)
src/components/Common/ActionListDaimoPayButton.tsx (1)

192-196: onClose correctly resets state.

The onClose handler properly resets both showInviteModal and confirmLoseInvite when the user dismisses the modal without confirming. This prevents the modal from being bypassed on subsequent clicks.

src/app/api/og/route.tsx (3)

13-13: LGTM!

Clean import following the existing pattern for OG components.


83-103: LGTM!

The invite OG image generation is well-implemented:

  • Standard 1200×630 dimensions
  • Appropriate font selection for the invite card
  • Props correctly match the InviteCardOG interface
  • Early return improves readability

185-212: LGTM!

The payment card OG generation correctly uses the top-level arrowSrcs definition, benefiting from the refactoring.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
src/components/Common/ActionList.tsx (1)

194-214: Simplify username assignment inside rawUsername guard.

Since line 202 already verifies rawUsername is truthy, the ternary on line 203 is redundant. You can safely call .toUpperCase() directly.

Apply this diff:

         if (isInviteLink && !userHasAppAccess && rawUsername) {
-            const username = rawUsername ? rawUsername.toUpperCase() : ''
+            const username = rawUsername.toUpperCase()
             const inviteCode = `${username}INVITESYOU`
src/app/invite/page.tsx (1)

23-87: LGTM: Metadata generation correctly structured.

The metadata function properly handles invite validation, constructs dynamic OG images, and falls back gracefully when data is missing. The defensive siteUrl check on line 53 is reasonable, though given line 31's fallback to BASE_URL, it's unlikely to trigger.

Optional: Consider simplifying OG image assignment.

Since siteUrl on line 31 has a fallback to BASE_URL, the check on line 53 is defensive but may be unnecessary. You could simplify:

         const ogUrl = new URL(`${siteUrl}/api/og`)
         ogUrl.searchParams.set('isInvite', 'true')
         ogUrl.searchParams.set('username', inviteCodeData.username)
-
-        if (!siteUrl) {
-            console.error('Error: Unable to determine site origin')
-        } else {
-            ogImageUrl = ogUrl.toString()
-        }
+        ogImageUrl = ogUrl.toString()

However, keeping the defensive check is also fine for robustness.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e89dca0 and 94f1154.

📒 Files selected for processing (2)
  • src/app/invite/page.tsx (1 hunks)
  • src/components/Common/ActionList.tsx (7 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 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/app/invite/page.tsx
🧬 Code graph analysis (2)
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 (89-95)
src/components/Common/ActionList.tsx (4)
src/constants/actionlist.consts.ts (1)
  • PaymentMethod (5-11)
src/context/authContext.tsx (1)
  • useAuth (183-189)
src/redux/hooks.ts (1)
  • useAppDispatch (5-5)
src/redux/slices/setup-slice.ts (1)
  • setupActions (65-65)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (9)
src/components/Common/ActionList.tsx (6)

21-31: LGTM: Imports and prop addition are correct.

The new imports support the invite flow integration, and the isInviteLink prop is properly typed with a sensible default.

Also applies to: 40-40


82-86: LGTM: State additions are appropriate.

The selectedMethod and showInviteModal state variables, along with the dispatch hook, are necessary for the invite modal flow.


227-241: LGTM: CTA and invite banner properly guarded.

The continue button correctly uses handleContinueWithPeanut, and the invite banner now guards against rendering "Invited by undefined" by checking username existence.


246-252: LGTM: DaimoPayButton properly wired with invite gating.

The showConfirmModal prop correctly gates the invite modal based on isInviteLink && !userHasAppAccess, addressing the past review comment about conditional modal display.


257-264: LGTM: MethodCard click handling correctly intercepts invite flow.

The logic properly checks for invite context and shows the modal with the selected method, falling back to direct action for non-invite flows.


290-308: LGTM: ConfirmInviteModal properly integrated with cleanup.

The modal is correctly wired with both continue and lose-invite paths, and state cleanup is handled appropriately on close and completion.

src/app/invite/page.tsx (3)

7-7: LGTM: Dynamic rendering correctly enforced.

The force-dynamic directive is appropriate for invite validation that must run on each request.


9-21: LGTM: getInviteCodeData helper is well-structured.

The helper correctly validates the invite code and returns structured data or null, with appropriate error handling.


89-91: LGTM: Page component properly delegates to InvitesPage.

The default export correctly renders the InvitesPage component, which handles the invite validation and waitlist redirect logic.

Based on learnings: The InvitesPage component handles the waitlist flow, so the redirect concerns from past reviews are addressed at the component level.

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 94f1154 and 207142a.

📒 Files selected for processing (1)
  • src/components/Global/ConfirmInviteModal/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/Global/ConfirmInviteModal/index.tsx (1)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)

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

♻️ Duplicate comments (2)
src/components/Profile/index.tsx (1)

34-35: Guard inviteCode against undefined usernames.

Using user?.user.username directly can yield "undefinedINVITESYOU". Derive from the computed username var on line 32.

Apply this diff:

-    const inviteCode = `${user?.user.username?.toUpperCase()}INVITESYOU`
+    const inviteCode = `${username.toUpperCase()}INVITESYOU`
src/components/Setup/Views/JoinWaitlist.tsx (1)

71-76: Fix open-redirect fallback in sanitizeRedirectURL

sanitizeRedirectURL currently returns the original URL if it’s not same-origin or doesn’t start with /, enabling open redirects. Change its fallback to a safe path:

 export const sanitizeRedirectURL = (redirectUrl: string): string => {
     try {
         const u = new URL(redirectUrl, window.location.origin)
         if (u.origin === window.location.origin) {
             return u.pathname + u.search + u.hash
         }
     } catch {
         if (redirectUrl.startsWith('/')) {
             return redirectUrl
         }
     }
-    return redirectUrl
+    return '/home'
 }
🧹 Nitpick comments (5)
src/components/Global/ConfirmInviteModal/index.tsx (2)

44-44: Remove unnecessary whitespace.

The leading {' '} space is unnecessary here and can be removed.

Apply this diff:

-                                    <p>
-                                        {' '}
-                                        This link lets you skip the Peanut waitlist. Using {method} will skip your
-                                        invite.
-                                    </p>
+                                    <p>
+                                        This link lets you skip the Peanut waitlist. Using {method} will skip your
+                                        invite.
+                                    </p>

70-72: Consider using Tailwind for transform.

The inline style prop mixes styling approaches. For consistency with the rest of the codebase, consider using Tailwind's arbitrary value syntax.

Apply this diff:

-                <div
-                    className="absolute left-0 top-7 flex w-full justify-center"
-                    style={{ transform: 'translateY(-80%)' }}
-                >
+                <div className="absolute left-0 top-7 flex w-full justify-center -translate-y-[80%]">
src/components/Profile/index.tsx (2)

66-72: Reconsider dummy href pattern.

The dummy href="/dummy" with a comment stating it won't be called is a code smell. Consider one of these approaches:

  1. Use href="#" to indicate a non-navigating link
  2. Modify ProfileMenuItem to accept an optional href when only onClick is needed
  3. Use a button-based variant of the component instead of a link-based one

This makes the intent clearer and avoids confusion.


167-169: Simplify template literals.

Both lines wrap a single variable in template literal syntax without concatenation. You can use the variable directly.

Apply this diff:

-                                <p className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold ">{`${inviteCode}`}</p>
+                                <p className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold ">{inviteCode}</p>

-                                <CopyToClipboard textToCopy={`${inviteCode}`} />
+                                <CopyToClipboard textToCopy={inviteCode} />
src/app/(setup)/layout.tsx (1)

25-25: Include dispatch in the dependency array per React best practices.

Although Redux ensures dispatch is stable, including it in the dependency array aligns with React exhaustive-deps rules and future-proofs the code.

-    }, [isPWA, selectedStep])
+    }, [dispatch, isPWA, selectedStep])
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 207142a and 1228144.

📒 Files selected for processing (6)
  • src/app/(mobile-ui)/layout.tsx (2 hunks)
  • src/app/(setup)/layout.tsx (2 hunks)
  • src/app/api/og/route.tsx (2 hunks)
  • src/components/Global/ConfirmInviteModal/index.tsx (1 hunks)
  • src/components/Profile/index.tsx (4 hunks)
  • src/components/Setup/Views/JoinWaitlist.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/app/(mobile-ui)/layout.tsx
  • src/app/api/og/route.tsx
🧰 Additional context used
🧬 Code graph analysis (4)
src/components/Profile/index.tsx (2)
src/context/authContext.tsx (1)
  • useAuth (183-189)
src/utils/general.utils.ts (1)
  • generateInvitesShareText (1323-1325)
src/app/(setup)/layout.tsx (4)
src/redux/hooks.ts (1)
  • useAppDispatch (5-5)
src/components/Setup/Setup.consts.tsx (1)
  • setupSteps (9-91)
src/redux/slices/setup-slice.ts (1)
  • setupActions (65-65)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
src/components/Global/ConfirmInviteModal/index.tsx (1)
src/components/0_Bruddle/Button.tsx (1)
  • Button (76-267)
src/components/Setup/Views/JoinWaitlist.tsx (7)
src/hooks/useZeroDev.ts (1)
  • useZeroDev (36-179)
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/services/invites.ts (1)
  • invitesApi (7-83)
src/utils/general.utils.ts (2)
  • getFromLocalStorage (112-134)
  • sanitizeRedirectURL (1217-1229)
src/redux/slices/setup-slice.ts (1)
  • setupActions (65-65)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (3)
src/components/Global/ConfirmInviteModal/index.tsx (1)

75-75: Past review comment is outdated.

The code already uses the correct Next.js 13+ Image API with the fill boolean prop and className="object-contain" for styling, which is the recommended approach. The deprecated layout="fill" and objectFit="contain" props are not present in the current code.

src/components/Setup/Views/JoinWaitlist.tsx (2)

32-48: LGTM: Loading state now correctly reset in all paths.

The finally block on Lines 45-46 ensures isLoading is reset whether validation succeeds or throws, addressing the critical issue flagged in the previous review.


61-83: LGTM: Redirect decoding and sanitization implemented.

Lines 67-72 decode redirect_uri with a try-catch fallback, and Lines 71-76 sanitize both redirect_uri and localStorageRedirect before pushing, addressing previous review concerns.

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

Caution

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

⚠️ Outside diff range comments (1)
src/app/(setup)/layout.tsx (1)

13-32: Core PR functionality not implemented: invite code filtering missing.

Past reviews flagged this critical issue but it remains unresolved. The PR objectives state "skip Welcome when step=signup and invite present," yet this file doesn't:

  1. Retrieve inviteCode from Redux using useSetupStore
  2. Filter out the 'welcome' step when invite code exists and step=signup
  3. Include proper dependencies in the useEffect

Apply this diff to implement the missing functionality:

+'use client'
+
+import { usePWAStatus } from '@/hooks/usePWAStatus'
-import { useAppDispatch } from '@/redux/hooks'
+import { useAppDispatch, useSetupStore } from '@/redux/hooks'
+import { setupActions } from '@/redux/slices/setup-slice'
+import { useEffect, Suspense } from 'react'
+import { setupSteps } from '../../components/Setup/Setup.consts'
+import '../../styles/globals.css'
+import { useSearchParams } from 'next/navigation'
+import PeanutLoading from '@/components/Global/PeanutLoading'
+import { Banner } from '@/components/Global/Banner'
+
 function SetupLayoutContent({ children }: { children?: React.ReactNode }) {
     const dispatch = useAppDispatch()
     const isPWA = usePWAStatus()
     const searchParams = useSearchParams()
+    const selectedStep = searchParams.get('step')
+    const { inviteCode } = useSetupStore()
 
     useEffect(() => {
         // filter steps and set them in redux state
         const filteredSteps = setupSteps.filter((step) => {
-            return step.screenId !== 'pwa-install' || !isPWA
+            if (step.screenId === 'pwa-install' && isPWA) {
+                return false
+            }
+            if (step.screenId === 'welcome' && inviteCode && selectedStep === 'signup') {
+                return false
+            }
+            return true
         })
         dispatch(setupActions.setSteps(filteredSteps))
-    }, [isPWA])
+    }, [dispatch, isPWA, selectedStep, inviteCode])
♻️ Duplicate comments (1)
src/components/Profile/index.tsx (1)

34-36: Guard inviteCode against undefined usernames (duplicate issue).

Using user?.user.username directly can yield "undefinedINVITESYOU" when the username is undefined. Derive inviteCode from the computed username variable on line 32, which safely falls back to 'anonymous'.

Apply this diff:

-    const inviteCode = `${user?.user.username?.toUpperCase()}INVITESYOU`
+    const inviteCode = `${username.toUpperCase()}INVITESYOU`
🧹 Nitpick comments (1)
src/components/Profile/index.tsx (1)

64-70: Consider refactoring ProfileMenuItem to optionally omit href.

The href="/dummy" with a comment indicating it won't be called suggests the component API could be improved to make href optional when onClick is provided. This would avoid the need for dummy values.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1228144 and 6891653.

📒 Files selected for processing (2)
  • src/app/(setup)/layout.tsx (2 hunks)
  • src/components/Profile/index.tsx (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/Profile/index.tsx (2)
src/context/authContext.tsx (1)
  • useAuth (183-189)
src/utils/general.utils.ts (1)
  • generateInvitesShareText (1323-1325)
src/app/(setup)/layout.tsx (2)
src/redux/hooks.ts (1)
  • useAppDispatch (5-5)
src/components/Global/PeanutLoading/index.tsx (1)
  • PeanutLoading (4-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Deploy-Preview
🔇 Additional comments (2)
src/app/(setup)/layout.tsx (1)

34-40: LGTM: Suspense boundary properly wraps dynamic content.

The Suspense wrapper with PeanutLoading fallback correctly handles the async useSearchParams hook in SetupLayoutContent, preventing hydration mismatches.

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

155-178: LGTM! Invite Friends modal is well-implemented.

The modal correctly displays the invite code, provides copy functionality, and integrates the share button with generateInvitesShareText. The description clearly explains the referral rewards.

Note: This implementation depends on fixing the inviteCode construction issue at lines 34-36 to avoid displaying/copying invalid codes.

@Zishan-7 Zishan-7 merged commit dacb05e into peanut-wallet-dev Oct 6, 2025
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants