-
Notifications
You must be signed in to change notification settings - Fork 13
[TASK-14876] Feat/invites #1251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: peanut-wallet-dev
Are you sure you want to change the base?
Conversation
WalkthroughAdds 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this 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?: booleansrc/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 navigationProfileMenuItem 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 errorsnavigator.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 linkTurn 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
📒 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 wiringPassing 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 goodinviteCode added to initial state.
30-31
: Reset includes inviteCodeResetting inviteCode to '' aligns with the rest of resetSetup.
54-56
: Reducer for inviteCodesetInviteCode reducer is correct and typed.
src/components/Global/ActionModal/index.tsx (5)
11-12
: CTA children support: LGTMAdding children to ActionModalButtonProps is a useful extension and backward compatible.
44-45
: Modal content slot: LGTMcontent prop placement is appropriate and doesn’t disrupt existing sections.
69-70
: Prop threading: LGTMNew props are correctly threaded through the component.
135-136
: Content rendering: LGTMRendering content before checkbox/CTAs is sensible.
173-195
: CTA children rendering: LGTMChildren 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.
There was a problem hiding this 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 viadescriptionClassName
or passing a React fragment with<br/>
tags.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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.
There was a problem hiding this 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 linkMake 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 contextPassing 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 andhasAppAccess === 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 semanticsConsider 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
📒 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 importThe 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 importImport 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 flowNew props and imports (dispatch, setupActions, useAuth) integrate cleanly.
There was a problem hiding this 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 changeLine 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 runnewIndex === currentStepIndex
, sosetDirection
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 dropcurrentStepIndex
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
📒 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
: EnsureErrorAlert
actually renders when there is an errorThis repeats the earlier review note: because Line 73 starts with
!!error || …
, React short-circuits to the boolean whenerror
is truthy, so theErrorAlert
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 missingLines 157-163 concatenate
${username}INVITESYOU
without checking whetherusername
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this 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:
- No heart icon for invitees in app - this should always show up next to verified double tick when you see that person
- im not sure about the flow of accepting invite and jwt
isVerified={user?.user.bridgeKycStatus === 'approved'} | ||
/> | ||
<SearchUsers /> | ||
<div className="flex items-center gap-2"> |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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!)
const username = user?.user.username | ||
const inviteCode = username ? `${username.toUpperCase()}INVITESYOU` : '' | ||
const inviteLink = `${process.env.NEXT_PUBLIC_BASE_URL}/invite?code=${inviteCode}` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice & clean toplevel consts
There was a problem hiding this comment.
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}` |
There was a problem hiding this comment.
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
<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> |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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!
const { data: invites, isLoading } = useQuery({ | ||
queryKey: ['invites'], | ||
queryFn: () => invitesApi.getInvites(), | ||
}) |
There was a problem hiding this comment.
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
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 |
There was a problem hiding this comment.
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}`) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👀
Also contributes to TASK-14878