Skip to content

Conversation

Zishan-7
Copy link
Contributor

Also contributes to TASK-14878

Copy link

Implement UI Changes

Copy link

Copy link
Contributor

coderabbitai bot commented Sep 24, 2025

Walkthrough

Adds invite/waitlist support across backend and frontend: server action and service APIs for invites, invite pages/layouts, setup flow/state for inviteCode, UI for sharing/copying invites, ActionList invite handling, profile invite UX, icons, and a new User.hasAppAccess field.

Changes

Cohort / File(s) Summary
Invites API & pages
src/services/invites.ts, src/app/actions/invites.ts, src/app/invite/page.tsx, src/components/Invites/InvitesPageLayout.tsx, src/components/Invites/JoinWaitlistPage.tsx, src/app/(mobile-ui)/points/page.tsx
New server action and client service methods (validate/get/accept/waitlist position); added invite landing page, invites layout, join-waitlist page; reworked Points page to invite-code/share/copy flow and invited-users list.
Setup flow & state
src/components/Setup/Setup.consts.tsx, 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.types.ts, src/app/(setup)/layout.tsx, src/app/(setup)/setup/page.tsx, src/redux/slices/setup-slice.ts, src/redux/types/setup.types.ts
Added JoinWaitlist and CollectEmail steps; introduced inviteCode in Redux (setter + reset); setup routing/step filtering updated to consider inviteCode; passkey/setup flows adjusted; Suspense/wrapping reorganized.
Invite integration in auth/flows
src/hooks/useZeroDev.ts, src/components/Setup/Views/JoinWaitlist.tsx, src/components/Setup/Views/SetupPasskey.tsx
Registration now accepts inviteCode (calls acceptInvite) during signup; JoinWaitlist validates/accepts invites and controls login/redirect flows.
Action list & claim flow
src/components/Common/ActionList.tsx, src/app/[...recipient]/client.tsx, src/components/Claim/Link/Initial.view.tsx
ActionList gains optional isInviteLink prop and invite-confirm modal flow; client passes isInviteLink for certain request_pay/USERNAME cases; Initial claim view updated to pass flag conditionally.
Profile & public profile UX
src/components/Profile/index.tsx, src/components/Profile/components/PublicProfile.tsx
Added Invite Friends modal/UI (invite code card, copy/share), updated guest messaging to invite-only, replaced request-link CTA with invite/share flows and modal.
Global UI components
src/components/Global/ActionModal/index.tsx, src/components/Global/CopyToClipboard/CopyToClipboardButton.tsx
ActionModal accepts new content prop and per-button children; new CopyToClipboardButton component added for copying invite codes.
Icons
src/components/Global/Icons/Icon.tsx, src/components/Global/Icons/trophy.tsx
Added TrophyIcon and updated IconName mapping (removed duplicate shield, added trophy).
Mobile UI header & layout
src/app/(mobile-ui)/home/page.tsx, src/app/(mobile-ui)/layout.tsx
Added star icon linking to /points in mobile header; layout expands public-path regex and early-returns JoinWaitlistPage for users without app access.
Interfaces / types
src/interfaces/interfaces.ts
Added hasAppAccess: boolean to exported User interface.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • kushagrasarathe
  • jjramirezn

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description Check ❓ Inconclusive The description only cites another task reference and does not outline any of the actual changes or goals of this pull request, leaving reviewers without context on what has been implemented. Please provide a brief summary of the key changes and objectives introduced by this pull request to give reviewers sufficient context.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title “[TASK-14876] Feat/invites” clearly references the associated task and concisely summarizes the primary change—adding invite-related features—so it is directly related to and representative of the pull request’s main content.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/invites

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.

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 Sep 25, 2025 10:47am

@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="https://github.com/points">
-                            <Image src={starImage} alt="star" width={20} height={20} />
-                        </Link>
+                        <Link
+                            href="https://github.com/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="https://github.com/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="https://github.com/points">
-                            <Image src={starImage.src} alt="star" width={20} height={20} />
-                        </Link>
+                        <Link
+                            href="https://github.com/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="https://github.com/points">
-                            <Image src={starImage} alt="star" width={20} height={20} />
-                        </Link>
-                        <SearchUsers />
-                    </div>
+                    <div className="flex items-center gap-2">
+                        <Link
+                            href="https://github.com/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}`)

}

const username = claimLinkData?.sender?.username ?? requestLinkData?.recipient?.identifier
const userHasAppAccess = user?.user.hasAppAccess
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Guard hasAppAccess to prevent crashes for guests

When guests hit this screen, user (or user.user) is often null. Line 169 accesses .hasAppAccess without optional chaining, which throws immediately and breaks invite links for anyone not logged in. Please optional-chain both levels and default to false.

-    const userHasAppAccess = user?.user.hasAppAccess
+    const userHasAppAccess = user?.user?.hasAppAccess ?? false
📝 Committable suggestion

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

Suggested change
const userHasAppAccess = user?.user.hasAppAccess
// before: user?.user.hasAppAccess can throw if user?.user is undefined
const userHasAppAccess = user?.user?.hasAppAccess ?? false
🤖 Prompt for AI Agents
In src/components/Common/ActionList.tsx around line 169, the code reads const
userHasAppAccess = user?.user.hasAppAccess which can throw when user or
user.user is null; change it to optional-chain both levels and default to false
(e.g., user?.user?.hasAppAccess ?? false) so guests won’t crash and invite links
continue to work.

Copy link
Contributor

Choose a reason for hiding this comment

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

resolve!

return (
<div className="flex flex-col gap-4">
<ValidatedInput
placeholder="Enter you email"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix typo in placeholder copy

Line 45 reads “Enter you email”. Please update it to “Enter your email” to avoid a user-facing typo.

🤖 Prompt for AI Agents
In src/components/Setup/Views/CollectEmail.tsx around line 45, the input
placeholder contains a typo "Enter you email"; update the placeholder string to
read "Enter your email" (replace "you" with "your") so the user-facing copy is
correct.

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

isVerified={user?.user.bridgeKycStatus === 'approved'}
/>
<SearchUsers />
<div className="flex items-center gap-2">
Copy link
Contributor

Choose a reason for hiding this comment

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

PR nit: plz add screenshots to PR!

Copy link
Contributor

Choose a reason for hiding this comment

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

visual nit: i don't see any animation here (pulsating, periodic wiggle, periodic shiny, or anth like that. I'm not sure if this was explicitly in the specs, but would be nice to have!)

Comment on lines +26 to +28
const username = user?.user.username
const inviteCode = username ? `${username.toUpperCase()}INVITESYOU` : ''
const inviteLink = `${process.env.NEXT_PUBLIC_BASE_URL}/invite?code=${inviteCode}`
Copy link
Contributor

Choose a reason for hiding this comment

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

nice & clean toplevel consts

Copy link
Contributor

Choose a reason for hiding this comment

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

<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}`
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: is this the copy we want?

dev process: imo if this was not defined, prolly quickest to quickly pull feedback in discord

Comment on lines +65 to +82
<Card key={invite.id} position={getCardPosition(i, invites.length)}>
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3">
<TransactionAvatarBadge
initials={username}
userName={username}
isLinkTransaction={false}
transactionType={'send'}
context="card"
size="small"
/>
</div>

<div className="min-w-0 flex-1 truncate font-roboto text-[16px] font-medium">
<VerifiedUserLabel name={username} isVerified={isVerified} />
</div>
</div>
</Card>
Copy link
Contributor

Choose a reason for hiding this comment

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

thought: I feel like this should be a more generic component, probably reused in a bunch of different places in the app

issue: where is the heart? image.png

}

// Show waitlist page if user doesn't have app access
if (!isFetchingUser && user && !user?.user.hasAppAccess) {
Copy link
Contributor

Choose a reason for hiding this comment

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

thouught: we def need to ensure migration before this goes live!

Comment on lines +19 to +22
const { data: invites, isLoading } = useQuery({
queryKey: ['invites'],
queryFn: () => invitesApi.getInvites(),
})
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: invites is not specific key to user

Suggested change
const { data: invites, isLoading } = useQuery({
queryKey: ['invites'],
queryFn: () => invitesApi.getInvites(),
})
const { data: invites } = useQuery({
queryKey: ['invites', user?.user.userId], // per-user scoped
queryFn: () => invitesApi.getInvites(),
enabled: !!user?.user.userId,
})

claimLinkData?: ClaimLinkData
requestLinkData?: ParsedURL
isLoggedIn: boolean
isInviteLink?: boolean
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: default false maybe?

const username = rawUsername ? rawUsername.toUpperCase() : ''
const inviteCode = `${username}INVITESYOU`
dispatch(setupActions.setInviteCode(inviteCode))
router.push(`/setup?step=signup&redirect_uri=${redirectUri}`)
Copy link
Contributor

Choose a reason for hiding this comment

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

nice

addParamStep('claim')
}
// push to setup page with redirect uri, to prevent the user from losing the flow context
const redirectUri = encodeURIComponent(window.location.pathname + window.location.search + window.location.hash)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: why 3 vars? window.location.href is more concise?

descriptionClassName?: string
buttonProps?: ButtonProps
footer?: React.ReactNode
content?: React.ReactNode
Copy link
Contributor

Choose a reason for hiding this comment

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

👀

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.

2 participants