[TASK-14876] Feat/invites#1251
Conversation
|
Warning Rate limit exceeded@Zishan-7 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 0 minutes and 52 seconds before requesting another review. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (2)
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughAdds an invite-first feature set: server invite validation and services, invite landing/OG generation, invite/join-waitlist UIs and layout, setup flow wiring and Redux invite state, ActionList invite/payment modal flows, new icons/assets, and mobile layout gating by user app access. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
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.
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="/points"> - <Image src={starImage} alt="star" width={20} height={20} /> - </Link> + <Link + href="/points" + aria-label="Points" + className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-gray-100" + > + <Icon name="trophy" size={20} /> + </Link>src/components/Profile/components/PublicProfile.tsx (1)
173-182: Wire up the modal CTA.The CTA onClick is a no-op. Consider calling navigator.share (with a fallback to copy) or reusing ShareButton logic so the modal actually helps guests obtain invites.
src/hooks/useZeroDev.ts (1)
59-61: Handle invite acceptance result and optionally clear the code.acceptInvite failures are silently ignored; this can leave users without access. Check the result, log to Sentry on failure, and consider clearing inviteCode on success.
- if (inviteCode.trim().length > 0) { - await invitesApi.acceptInvite(inviteCode) - } + if (inviteCode?.trim()) { + const res = await invitesApi.acceptInvite(inviteCode.trim()) + if (!res.success) { + captureException(new Error(`Invite acceptance failed for code: ${inviteCode}`)) + } + // Optionally clear the invite code after successful acceptance: + // dispatch(setupActions.setInviteCode('')) + }Add import if you choose to clear the code afterward:
import { setupActions } from '@/redux/slices/setup-slice'Also confirm a valid JWT cookie exists at registration time, since acceptInvite expects Authorization.
src/components/Profile/index.tsx (2)
61-67: Avoid dummy href to prevent accidental 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="/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.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/components/Setup/Views/SetupPasskey.tsx (1)
38-49: Decode redirect_uri before sanitizing/pushing.redirect_uri can arrive URL-encoded; decode first, then sanitize.
Apply this diff:
- const redirect_uri = searchParams.get('redirect_uri') - if (redirect_uri) { - const sanitizedRedirectUrl = sanitizeRedirectURL(redirect_uri) - router.push(sanitizedRedirectUrl) - return - } + const redirect_uri = searchParams.get('redirect_uri') + if (redirect_uri) { + let decodedRedirect = redirect_uri + try { + decodedRedirect = decodeURIComponent(redirect_uri) + } catch {} + const sanitizedRedirectUrl = sanitizeRedirectURL(decodedRedirect) + router.push(sanitizedRedirectUrl) + return + }
🧹 Nitpick comments (18)
src/components/Setup/Setup.types.ts (1)
1-33: Prune stale 'join-beta' entries if unused.Since the 'join-beta' step was removed from setupSteps, consider removing it from ScreenId and ScreenProps to keep the public surface clean.
src/components/Setup/Views/SetupPasskey.tsx (2)
27-76: Effect uses inviteCode/handleNext/searchParams but misses them in deps.Add them to avoid stale closures.
Apply this diff:
- }, [address, user, isFetchingUser]) + }, [address, user, isFetchingUser, inviteCode, handleNext, searchParams])
21-23: Avoid calling useAuth() twice.Destructure once to prevent duplicate subscriptions.
Apply this diff:
- const { user, isFetchingUser } = useAuth() - const { addAccount } = useAuth() + const { user, isFetchingUser, addAccount } = useAuth()src/components/Setup/Views/CollectEmail.tsx (2)
15-15: Normalize state setter name (readability).Prefer setIsLoading for consistency.
Apply this diff and update references:
- const [isLoading, setisLoading] = useState(false) + const [isLoading, setIsLoading] = useState(false)Then replace setisLoading(...) with setIsLoading(...) on Lines 27, 29, 34, 61, 64.
41-41: Typo in placeholder.Use “your”.
Apply this diff:
- placeholder="Enter you email" + placeholder="Enter your email"src/components/Setup/Setup.consts.tsx (1)
6-7: Remove unused import(s).JoinBetaStep is imported but unused. Also consider removing peanutWithGlassesAnim if no longer used in this file.
Apply this diff:
-import { InstallPWA, SetupPasskey, SignupStep, JoinBetaStep, CollectEmail } from '@/components/Setup/Views' +import { InstallPWA, SetupPasskey, SignupStep, CollectEmail } from '@/components/Setup/Views'src/components/Setup/Views/JoinWaitlist.tsx (1)
96-101: Trim invite codes before storing.Prevents subtle validation/accept issues from whitespace.
Apply this diff:
- onClick={() => { - dispatch(setupActions.setInviteCode(inviteCode)) - handleNext() - }} + onClick={() => { + dispatch(setupActions.setInviteCode(inviteCode.trim())) + handleNext() + }}src/app/(mobile-ui)/home/page.tsx (1)
223-228: Increase tap target and add accessible name; use the StaticImport directly.The star is a small 20x20 image wrapped directly in a Link, which makes the tap target too small on mobile. Also, prefer passing the StaticImport to Image (not .src) and add an aria-label.
Apply this diff:
- <Link href="/points"> - <Image src={starImage.src} alt="star" width={20} height={20} /> - </Link> + <Link + href="/points" + aria-label="Open invites and points" + className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-gray-100" + > + <Image src={starImage} alt="Points" width={20} height={20} /> + </Link>src/components/Global/CopyToClipboard/CopyToClipboardButton.tsx (2)
16-22: Harden clipboard copy with fallback and error handling.navigator.clipboard can be unavailable (non-secure context, older browsers). Add a fallback and catch errors.
- const handleCopy = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { - e.stopPropagation() - navigator.clipboard.writeText(textToCopy).then(() => { - setCopied(true) - setTimeout(() => setCopied(false), 2000) - }) - } + const handleCopy = async (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { + e.stopPropagation() + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(textToCopy) + } else { + const el = document.createElement('textarea') + el.value = textToCopy + el.setAttribute('readonly', '') + el.style.position = 'absolute' + el.style.left = '-9999px' + document.body.appendChild(el) + el.select() + document.execCommand('copy') + document.body.removeChild(el) + } + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch { + setCopied(false) + } + }
24-33: Minor UX: reflect “Copied!” state and guard empty input.Show a success label for 2s and disable when textToCopy is empty.
<Button size={size} className={className} onClick={handleCopy} icon={copied ? 'check' : 'copy'} shadowSize="4" variant="primary-soft" + disabled={!textToCopy} > - <p className="text-xs"> Copy code</p> + <p className="text-xs" aria-live="polite">{copied ? 'Copied!' : 'Copy code'}</p> </Button>src/app/(mobile-ui)/points/page.tsx (3)
49-58: Guard Share button on valid inviteCode.Avoids sharing a link with an empty code.
- <ShareButton + {inviteCode && ( + <ShareButton generateText={() => Promise.resolve( `I’m using Peanut, an invite-only app for easy payments. With it you can pay friends, use merchants, and move money in and out of your bank, even cross-border. Here’s my invite: ${inviteLink}` ) } title="Share your invite link" - > - Share Invite link - </ShareButton> + > + Share Invite link + </ShareButton> + )}
99-109: Same: guard Share button when there’s no inviteCode.- <ShareButton + {inviteCode && ( + <ShareButton generateText={() => Promise.resolve( `I’m using Peanut, an invite-only app for easy payments. With it you can pay friends, use merchants, and move money in and out of your bank, even cross-border. Here’s my invite: ${inviteLink}` ) } title="Share your invite link" - > - Share Invite link - </ShareButton> + > + Share Invite link + </ShareButton> + )}
61-65: Type invites for safer access to invitee fields.Replace any with a typed shape to avoid runtime surprises.
Example:
type Invite = { id: string invitee: { username: string; bridgeKycStatus: 'approved' | 'rejected' | 'pending' | string } } {inviteList.map((invite: Invite, i: number) => { ... })}src/components/Profile/components/PublicProfile.tsx (5)
136-166: Remove unreachable branch inside guest‑only block.
Inside a container rendered only when!isLoggedIn, the{isLoggedIn ? … : …}check is dead code.Apply:
- <Card position="single" className="z-10 mt-28 space-y-2 p-4 text-center"> - {isLoggedIn ? ( - <> - <h2 className="text-lg font-extrabold">You're all set</h2> - <p className="mx-auto max-w-[55%] text-sm"> - Now send or request money to get started. - </p> - </> - ) : ( - <div className="space-y-4"> + <Card position="single" className="z-10 mt-28 space-y-2 p-4 text-center"> + <div className="space-y-4"> <div className="space-y-2"> <h2 className="text-lg font-extrabold">No invite, no Peanut</h2> <p> Peanut is invite-only. <br /> Go beg your friend for an invite link! </p> </div> - <ShareButton - generateText={() => - Promise.resolve( - `Bro… I’m on my knees. Peanut is invite-only and I’m locked outside. Save my life and send me your invite` - ) - } - title="Beg for an invite" - > - Beg for an invite - </ShareButton> - </div> - )} + <ShareButton + generateText={() => Promise.resolve(INVITE_SHARE_TEXT)} + title="Beg for an invite" + > + Beg for an invite + </ShareButton> + </div>Additionally, deduplicate the share text; see constant suggestion below.
48-56: Avoid shadowing ‘user’ from auth context.
Rename the fetched variable for clarity.- usersApi.getByUsername(username).then((user) => { - if (user?.fullName) setFullName(user.fullName) - if (user?.bridgeKycStatus === 'approved') setIsKycVerified(true) - // to check if the logged in user has sent money to the profile user, - // we check the amount that the profile user has received from the logged in user. - if (user?.totalUsdReceivedFromCurrentUser) { - setTotalSentByLoggedInUser(user.totalUsdReceivedFromCurrentUser) - } + usersApi.getByUsername(username).then((profile) => { + if (profile?.fullName) setFullName(profile.fullName) + if (profile?.bridgeKycStatus === 'approved') setIsKycVerified(true) + // to check if the logged in user has sent money to the profile user, + // we check the amount that the profile user has received from the logged in user. + if (profile?.totalUsdReceivedFromCurrentUser) { + setTotalSentByLoggedInUser(profile.totalUsdReceivedFromCurrentUser) + }
31-61: Prefer numeric state for amounts to avoid repeated parsing.
Store totals as number and compute directly.- const [totalSentByLoggedInUser, setTotalSentByLoggedInUser] = useState<string>('0') + const [totalSentByLoggedInUser, setTotalSentByLoggedInUser] = useState<number>(0)- if (profile?.totalUsdReceivedFromCurrentUser) { - setTotalSentByLoggedInUser(profile.totalUsdReceivedFromCurrentUser) - } + if (profile?.totalUsdReceivedFromCurrentUser != null) { + setTotalSentByLoggedInUser(Number(profile.totalUsdReceivedFromCurrentUser) || 0) + }- const haveSentMoneyToUser = useMemo(() => Number(totalSentByLoggedInUser) > 0, [totalSentByLoggedInUser]) + const haveSentMoneyToUser = useMemo(() => totalSentByLoggedInUser > 0, [totalSentByLoggedInUser])
148-165: Deduplicate share text and prep for i18n later.
Extract the invite share copy to a constant and reuse it here (and in the modal below).- <ShareButton - generateText={() => - Promise.resolve( - `Bro… I’m on my knees. Peanut is invite-only and I’m locked outside. Save my life and send me your invite` - ) - } - title="Beg for an invite" - > - Beg for an invite - </ShareButton> + <ShareButton + generateText={() => Promise.resolve(INVITE_SHARE_TEXT)} + title="Beg for an invite" + > + Beg for an invite + </ShareButton>Add once near the top (after imports):
const INVITE_SHARE_TEXT = "Bro… I’m on my knees. Peanut is invite-only and I’m locked outside. Save my life and send me your invite";
188-208: Extract invite messages and enable newline rendering in ActionModal
- Pull out both the modal description and share text into constants (e.g. INVITE_MODAL_DESCRIPTION, INVITE_SHARE_TEXT) instead of inlining.
- Ensure “\n” renders as a line break by adding
whitespace-pre-line(Tailwind) to the description container viadescriptionClassNameor 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.tsxsrc/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/.mapon 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.
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="/points"> - <Image src={starImage} alt="star" width={20} height={20} /> - </Link> - <SearchUsers /> - </div> + <div className="flex items-center gap-2"> + <Link + href="/points" + aria-label="Points" + className="flex h-10 w-10 items-center justify-center rounded-full hover:bg-gray-100" + > + <Image src={starImage} alt="Points" width={20} height={20} /> + </Link> + <SearchUsers /> + </div>Please confirm the /points route exists on mobile to avoid dead links.
src/components/Claim/Link/Initial.view.tsx (1)
747-752: Don’t force invite flow for all claim links; derive isInviteLink from contextPassing bare
isInviteLinkmakes 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 && !userHasAppAccesstreats 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.hasAppAccessto 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.
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
currentStepIndexin the effect dependency array while mutating it inside the same effect. After every step change the effect runs twice; on the second runnewIndex === currentStepIndex, sosetDirectionalways falls back to-1, reversing the animation even when advancing. Please compute the new index against the previous value in a single pass (and dropcurrentStepIndexfrom 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: EnsureErrorAlertactually renders when there is an errorThis repeats the earlier review note: because Line 73 starts with
!!error || …, React short-circuits to the boolean whenerroris truthy, so theErrorAlertnever 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}INVITESYOUwithout checking whetherusernameis 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}`)
Hugo0
left a comment
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (8)
src/app/(setup)/setup/page.tsx (1)
27-148: Re-run the setup initializer when inviteCode changes.
inviteCodedrives the branch that jumps straight to the signup step, but the effect never re-executes when that value updates because it isn’t in the dependency array. If the user enters an invite after the initial run, we still show the welcome step. IncludeinviteCode(and keepdispatch) in the dependencies so the initializer reruns when the code arrives.- }, [dispatch, steps]) + }, [dispatch, steps, inviteCode])src/app/(mobile-ui)/layout.tsx (1)
97-100: Don’t override public routes with the waitlist gate.This renders
JoinWaitlistPagefor every route whenever the user lackshasAppAccess, including/invite,/claim, etc. The earlier feedback asked to keep those public surfaces accessible. Add an!isPublicPathguard before rendering the waitlist page so public paths continue to work.- if (!isFetchingUser && user && !user?.user.hasAppAccess) { + if (!isFetchingUser && user && !user?.user.hasAppAccess && !isPublicPath) { return <JoinWaitlistPage /> }src/components/Profile/components/PublicProfile.tsx (1)
122-138: Harden Request button logic; encode username; only block when hasAppAccess is explicitly false.The logic still needs hardening as previously noted:
- URL-encode the username to prevent unsafe characters in the URL.
- Only block navigation when
hasAppAccessis explicitlyfalse(notundefined), to avoid showing the modal while auth context is still loading.- Separate handling for not logged in vs. no access cases.
Apply this diff:
- <Button - onClick={() => { - if (isLoggedIn && user?.user.hasAppAccess) { - router.push(`/request/${username}`) - } else { - setShowInviteModal(true) - } - }} - variant="purple" - shadowSize="4" - className="flex w-1/2 items-center justify-center gap-2 rounded-full py-3" - > + <Button + onClick={() => { + if (!isLoggedIn) { + setShowInviteModal(true) + return + } + if (user?.user?.hasAppAccess === false) { + setShowInviteModal(true) + } else { + router.push(`/request/${encodeURIComponent(username)}`) + } + }} + variant="purple" + shadowSize="4" + className="flex w-1/2 items-center justify-center gap-2 rounded-full py-3" + > <div className="flex size-5 items-center justify-center"> <Icon name="arrow-down-left" size={8} fill="black" /> </div> <span className="font-bold">Request</span> </Button>src/app/(mobile-ui)/home/page.tsx (1)
222-227: Accessibility issue still present.The Link wrapping InvitesIcon still lacks an accessible label. This was flagged in a previous review and remains unaddressed.
Apply this diff to add an accessible label:
- <div className="flex items-center gap-2"> - <Link href="/points"> + <div className="flex items-center gap-2"> + <Link href="/points" aria-label="View invites and points"> <InvitesIcon /> </Link> <SearchUsers />src/app/(mobile-ui)/points/page.tsx (1)
28-30: Guard against empty inviteCode display.While the username check prevents "undefinedINVITESYOU", the UI at lines 44-46 will still render an empty inviteCode when username is falsy. This could show an empty Card with just a copy button.
Consider wrapping the invite code Card (lines 42-47) in a conditional check:
<h1 className="font-bold">Invite friends with your code</h1> - <div className="flex w-full items-center justify-between gap-3"> - <Card className="flex w-full items-center justify-between py-3.5"> - <p className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold md:text-base">{`${inviteCode}`}</p> - <CopyToClipboard textToCopy={inviteCode} /> - </Card> - </div> + {inviteCode && ( + <div className="flex w-full items-center justify-between gap-3"> + <Card className="flex w-full items-center justify-between py-3.5"> + <p className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold md:text-base"> + {inviteCode} + </p> + <CopyToClipboard textToCopy={inviteCode} /> + </Card> + </div> + )}src/components/Profile/index.tsx (1)
33-34: Guard inviteCode against undefined usernames.Using
user?.user.usernamedirectly can produce"undefinedINVITESYOU". Derive from the computedusernamevariable (line 31) instead.Apply this diff:
- const inviteCode = `${user?.user.username?.toUpperCase()}INVITESYOU` + const inviteCode = `${username.toUpperCase()}INVITESYOU`src/components/Common/ActionList.tsx (1)
194-211: Only dispatch invite code when username is truthy.If
rawUsernameis falsy or an empty string,usernamebecomes'', andinviteCodebecomes'INVITESYOU'(invalid). Dispatch should only occur whenusernameis non-empty.Apply this diff:
if (isInviteLink && !userHasAppAccess && rawUsername) { const username = rawUsername ? rawUsername.toUpperCase() : '' - const inviteCode = `${username}INVITESYOU` - dispatch(setupActions.setInviteCode(inviteCode)) - dispatch(setupActions.setInviteType(EInviteType.PAYMENT_LINK)) - router.push(`/setup?step=signup&redirect_uri=${redirectUri}`) + if (username) { + const inviteCode = `${username}INVITESYOU` + dispatch(setupActions.setInviteCode(inviteCode)) + dispatch(setupActions.setInviteType(EInviteType.PAYMENT_LINK)) + router.push(`/setup?step=signup&redirect_uri=${redirectUri}`) + } else { + router.push(`/setup?redirect_uri=${redirectUri}`) + } } else { router.push(`/setup?redirect_uri=${redirectUri}`) }src/components/Common/ActionListDaimoPayButton.tsx (1)
178-195: Fix duplicate payment initiation.
handleLoseInvitecallshandleInitiateDaimoPayment(line 185), then triggersdaimoPayButtonClickRef.current()(line 188). This invokesonClick, which callsonBeforeShow, which callshandleInitiateDaimoPaymentagain (line 150), resulting in two payment initiations.Solution: Remove the direct call to
handleInitiateDaimoPaymentfromhandleLoseInvite. Instead, setconfirmLoseInvitetotrueand trigger the Daimo button click, allowingonBeforeShowto perform the single payment initiation.Apply this diff:
handleLoseInvite={async () => { setShowInviteModal(false) setConfirmLoseInvite(true) - // Directly initiate the Daimo payment instead of triggering button click - const success = await handleInitiateDaimoPayment() - if (success && daimoPayButtonClickRef.current) { - // Only trigger the actual Daimo widget if payment initiation was successful - daimoPayButtonClickRef.current() - } + // Trigger Daimo button; onBeforeShow will perform the single initiation + daimoPayButtonClickRef.current?.() }}
🧹 Nitpick comments (5)
src/app/(mobile-ui)/layout.tsx (1)
102-102: Remove stray console log.
console.log(user, 'user')looks like leftover debugging—drop it before shipping.src/components/Profile/components/PublicProfile.tsx (1)
156-172: Consider extracting the hardcoded invite text to avoid duplication.The "beg for an invite" message text is duplicated here and in the ActionModal (lines 206-209). Consider extracting it to a constant or using a utility function (e.g.,
generateInvitesShareText) to maintain consistency.Example:
+const INVITE_BEG_TEXT = `Bro… I'm on my knees. Peanut is invite-only and I'm locked outside. Save my life and send me your invite` + <ShareButton - generateText={() => - Promise.resolve( - `Bro… I'm on my knees. Peanut is invite-only and I'm locked outside. Save my life and send me your invite` - ) - } + generateText={() => Promise.resolve(INVITE_BEG_TEXT)} title="Beg for an invite" >src/app/(mobile-ui)/points/page.tsx (2)
44-44: Remove redundant template literal.The template literal wrapper around
inviteCodeis unnecessary.- <p className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold md:text-base">{`${inviteCode}`}</p> + <p className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold md:text-base">{inviteCode}</p>
93-95: Extract invite share text into a shared constant
The sentence"Send your invite link to start earning more rewards"is currently defined only insrc/app/(mobile-ui)/points/page.tsx; centralize it (e.g. in a constants or i18n file) for consistent reuse across other invite flows (profile modal, public profile).src/components/Profile/index.tsx (1)
65-71: Consider omitting the dummy href.Using
href="/dummy"is confusing. SinceonClickis the primary handler, either omithrefor usehref="#"to clarify it's not a navigation target.Apply this diff:
<ProfileMenuItem icon="smile" label="Invite friends to Peanut" onClick={() => setIsInviteFriendsModalOpen(true)} - href="/dummy" // Dummy link, wont be called + href="#" position="single" />
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (18)
src/app/(mobile-ui)/home/page.tsx(2 hunks)src/app/(mobile-ui)/layout.tsx(2 hunks)src/app/(mobile-ui)/points/page.tsx(1 hunks)src/app/(setup)/layout.tsx(1 hunks)src/app/(setup)/setup/page.tsx(3 hunks)src/app/[...recipient]/client.tsx(1 hunks)src/components/Claim/Link/Initial.view.tsx(1 hunks)src/components/Common/ActionList.tsx(7 hunks)src/components/Common/ActionListDaimoPayButton.tsx(4 hunks)src/components/Global/CopyToClipboard/index.tsx(1 hunks)src/components/Profile/components/PublicProfile.tsx(5 hunks)src/components/Profile/index.tsx(4 hunks)src/components/Setup/Views/CollectEmail.tsx(1 hunks)src/components/Setup/Views/JoinWaitlist.tsx(1 hunks)src/context/authContext.tsx(1 hunks)src/interfaces/interfaces.ts(1 hunks)src/services/services.types.ts(2 hunks)src/utils/general.utils.ts(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
- src/utils/general.utils.ts
- src/interfaces/interfaces.ts
- src/services/services.types.ts
- src/components/Claim/Link/Initial.view.tsx
- src/components/Setup/Views/JoinWaitlist.tsx
- src/context/authContext.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-29T18:34:33.596Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1251
File: src/components/Invites/JoinWaitlistPage.tsx:41-55
Timestamp: 2025-09-29T18:34:33.596Z
Learning: In the JoinWaitlistPage component, after successfully accepting an invite via invitesApi.acceptInvite(), calling fetchUser() is sufficient to update the user state and automatically display the app. No manual navigation to /home or other pages is required since the user is already on the home page and the app will be displayed once user.hasAppAccess is updated.
Applied to files:
src/app/(mobile-ui)/layout.tsx
🧬 Code graph analysis (11)
src/components/Global/CopyToClipboard/index.tsx (1)
src/components/0_Bruddle/Button.tsx (2)
ButtonSize(17-17)Button(76-267)
src/components/Common/ActionListDaimoPayButton.tsx (3)
src/components/Global/DaimoPayButton/index.tsx (1)
DaimoPayButton(51-200)src/redux/slices/payment-slice.ts (1)
paymentActions(77-77)src/components/SearchUsers/SearchResultCard.tsx (1)
SearchResultCard(19-70)
src/app/(setup)/layout.tsx (4)
src/redux/hooks.ts (1)
useAppDispatch(5-5)src/components/Setup/Setup.consts.tsx (1)
setupSteps(9-91)src/redux/slices/setup-slice.ts (1)
setupActions(65-65)src/components/Global/PeanutLoading/index.tsx (1)
PeanutLoading(4-19)
src/components/Profile/index.tsx (2)
src/context/authContext.tsx (1)
useAuth(183-189)src/utils/general.utils.ts (1)
generateInvitesShareText(1323-1325)
src/components/Profile/components/PublicProfile.tsx (2)
src/components/0_Bruddle/Button.tsx (1)
Button(76-267)src/components/Global/Icons/Icon.tsx (1)
Icon(198-207)
src/app/(setup)/setup/page.tsx (3)
src/redux/hooks.ts (1)
useSetupStore(9-9)src/components/Setup/Setup.types.ts (1)
ISetupStep(41-53)src/components/Global/PeanutLoading/index.tsx (1)
PeanutLoading(4-19)
src/components/Common/ActionList.tsx (4)
src/constants/actionlist.consts.ts (1)
PaymentMethod(5-11)src/context/authContext.tsx (1)
useAuth(183-189)src/redux/hooks.ts (1)
useAppDispatch(5-5)src/redux/slices/setup-slice.ts (1)
setupActions(65-65)
src/app/(mobile-ui)/layout.tsx (1)
src/components/Global/PeanutLoading/index.tsx (1)
PeanutLoading(4-19)
src/app/(mobile-ui)/points/page.tsx (8)
src/context/authContext.tsx (1)
useAuth(183-189)src/services/invites.ts (1)
invitesApi(7-83)src/components/Global/PeanutLoading/index.tsx (1)
PeanutLoading(4-19)src/utils/general.utils.ts (1)
generateInvitesShareText(1323-1325)src/services/services.types.ts (1)
Invite(399-408)src/components/Global/Card/index.tsx (1)
getCardPosition(14-19)src/components/UserHeader/index.tsx (1)
VerifiedUserLabel(41-88)src/components/Global/Icons/Icon.tsx (1)
Icon(198-207)
src/components/Setup/Views/CollectEmail.tsx (3)
src/context/authContext.tsx (1)
useAuth(183-189)src/app/actions/users.ts (1)
updateUserById(12-35)src/components/0_Bruddle/Button.tsx (1)
Button(76-267)
src/app/(mobile-ui)/home/page.tsx (1)
src/components/SearchUsers/index.tsx (1)
SearchUsers(82-121)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Deploy-Preview
🔇 Additional comments (26)
src/components/Profile/components/PublicProfile.tsx (3)
20-21: LGTM!The new imports are correctly added to support the invite-gated flows.
38-38: LGTM!The state variable is correctly defined for controlling the invite modal visibility.
196-216: Verify ActionModal newline rendering
Unable to locate theActionModalimplementation; please confirm that the description prop’s\ncharacters render correctly as line breaks (e.g., via<br/>conversion orwhite-space: pre-lineCSS).src/app/(mobile-ui)/home/page.tsx (1)
43-43: LGTM!The import statement is correct and the component path follows the project's component organization pattern.
src/app/(mobile-ui)/points/page.tsx (6)
1-18: LGTM!All imports are correctly structured and follow the project's organization patterns. The use of explicit type imports (e.g.,
Invite) and utility functions (e.g.,generateInvitesShareText) demonstrates good separation of concerns.
22-26: Good implementation of query scoping.The query key is properly scoped to the userId, which ensures correct cache invalidation per user. The
enabledguard prevents unnecessary API calls when user data isn't available.Based on learnings: This aligns with @tanstack/query best practices for stable query keys and conditional fetching.
32-34: LGTM!The loading state appropriately covers the full screen and provides a clear visual indicator while data is being fetched.
51-56: Good use of share utility.The share functionality correctly leverages the centralized
generateInvitesShareTextutility, promoting consistency across the app.
59-82: Proper typing and safe access patterns.The code correctly uses the
Invitetype and safely accesses nested properties. ThegetCardPositionutility ensures proper visual grouping of list items.
86-103: Good empty state UX.The empty state provides clear messaging and an actionable CTA to share the invite link. The trophy icon reinforces the rewards/gamification aspect of the invites feature.
src/components/Global/CopyToClipboard/index.tsx (2)
4-4: LGTM: Clean prop extension for button mode.The new
typeandbuttonSizeprops are well-typed and preserve backward compatibility with the defaulttype='icon'.Also applies to: 11-15
18-18: LGTM: Button mode correctly implemented.The event type is appropriately broadened, and the conditional rendering cleanly adds button support while preserving the existing icon behavior.
Also applies to: 26-39
src/app/[...recipient]/client.tsx (1)
497-499: LGTM: Invite link correctly gated to username request flows.The conditional logic properly restricts invite behavior to
request_payflows withUSERNAMErecipients, and optional chaining prevents crashes.src/components/Profile/index.tsx (2)
10-12: LGTM: Imports and state for invite modals.All new imports are used later in the file, and state initialization is correct.
Also applies to: 16-17, 21-22
138-177: LGTM: Invite Friends modal structure is correct.The modal content correctly uses
CopyToClipboardandShareButtoncomponents. However, ensure theinviteCodefix (lines 33-34) is applied so the modal displays a valid code.src/components/Common/ActionList.tsx (7)
21-21: LGTM: Invite link prop and imports.The
isInviteLinkprop is correctly added with a default value offalse, preserving backward compatibility.Also applies to: 27-31, 40-40, 51-57
82-83: LGTM: State and dispatch for invite flow.State initialization and
useAppDispatchhook are correct.Also applies to: 86-86
213-214: LGTM: Username and access flag correctly computed.Optional chaining and nullish coalescing prevent crashes for guests.
227-227: LGTM: Button and invite banner.The banner is correctly guarded with
&& usernameto prevent displaying "Invited by undefined".Also applies to: 235-241
246-253: LGTM: Daimo button receives invite gating flag.The
showConfirmModalprop correctly gates the invite modal for Daimo payment flows.
257-264: LGTM: Method click correctly gated for invite flows.The conditional logic properly opens the invite modal when necessary before proceeding with payment.
293-308: LGTM: ConfirmInviteModal correctly wired.The modal handlers properly manage state and invoke the appropriate actions for continue-with-peanut vs. lose-invite flows.
src/components/Common/ActionListDaimoPayButton.tsx (4)
1-1: LGTM: Props and imports for invite flow.The new
ActionListDaimoPayButtonPropsinterface correctly exposesshowConfirmModalto gate the invite modal.Also applies to: 12-19
31-33: LGTM: State and ref for invite modal flow.State and ref initialization are correct for coordinating the invite modal with the Daimo payment flow.
144-151: LGTM: onBeforeShow correctly gates invite modal.The logic now checks
showConfirmModalto conditionally show the invite modal, and payment initiation occurs only once insideonBeforeShow.
160-162: LGTM: DaimoPayButton onClick captured in ref.Storing the
onClickhandler allows the invite modal to trigger the Daimo widget after payment initiation.
There was a problem hiding this comment.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
public/arrows/top-right-arrow-2.svgis excluded by!**/*.svg
📒 Files selected for processing (4)
src/app/api/og/route.tsx(2 hunks)src/app/invite/page.tsx(1 hunks)src/components/Invites/InvitesPage.tsx(1 hunks)src/components/og/InviteCardOG.tsx(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-25T11:18:10.633Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1251
File: src/app/invite/page.tsx:44-53
Timestamp: 2025-09-25T11:18:10.633Z
Learning: The setup page (/setup) contains the waitlist functionality in the welcome step using the JoinWaitlist component, so redirecting users to /setup for joining the waitlist is correct.
Applied to files:
src/app/invite/page.tsx
📚 Learning: 2025-07-24T10:57:15.315Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1000
File: src/components/og/ProfileCardOG.tsx:0-0
Timestamp: 2025-07-24T10:57:15.315Z
Learning: In `src/components/og/ProfileCardOG.tsx`, the scribble image should have an empty alt attribute (alt="") to prevent layout issues if the image fails to load. Since it's a decorative element positioned absolutely over the username text, showing alt text would interfere with the layout and username display.
Applied to files:
src/components/og/InviteCardOG.tsx
🧬 Code graph analysis (3)
src/app/invite/page.tsx (3)
src/app/actions/invites.ts (1)
validateInviteCode(8-43)src/lib/hosting/get-origin.ts (1)
getOrigin(3-16)src/components/Invites/InvitesPage.tsx (1)
InvitesPage(89-95)
src/app/api/og/route.tsx (1)
src/components/og/InviteCardOG.tsx (1)
InviteCardOG(6-142)
src/components/Invites/InvitesPage.tsx (5)
src/context/authContext.tsx (1)
useAuth(183-189)src/redux/hooks.ts (1)
useAppDispatch(5-5)src/services/invites.ts (1)
invitesApi(7-83)src/redux/slices/setup-slice.ts (1)
setupActions(65-65)src/components/Global/PeanutLoading/index.tsx (1)
PeanutLoading(4-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Deploy-Preview
🔇 Additional comments (16)
src/app/invite/page.tsx (4)
1-7: LGTM: Imports and dynamic config are correct.The
force-dynamicexport ensures fresh invite validation on each request, which is appropriate for this page.
9-21: LGTM: Invite code validation helper is correct.The function correctly validates the invite code and returns username on success or null on failure.
60-87: LGTM: Metadata structure is correct.The dynamic OG metadata with conditional metadataBase, OpenGraph, and Twitter card configuration is properly implemented.
89-91: LGTM: Default export is correct.The page component correctly renders the InvitesPage component. Invite validation and error handling logic is delegated to InvitesPage.
src/components/Invites/InvitesPage.tsx (6)
1-16: LGTM: Imports are correct.All necessary dependencies are imported for the invite page functionality.
17-33: LGTM: Invite validation query is correctly configured.The useQuery hook properly validates the invite code with an enabled guard to prevent unnecessary calls.
35-41: LGTM: Invite claim logic is correct.The function properly dispatches invite code and type to Redux and navigates to the signup step. The hardcoded
EInviteType.PAYMENT_LINKappears intentional for this invite flow.
43-58: LGTM: Loading and error states are correct.The loading state and error handling with redirect to /setup (which contains waitlist) are properly implemented. Based on learnings.
89-95: LGTM: Suspense wrapper is correct.The Suspense boundary properly handles the async searchParams access with an appropriate loading fallback.
60-87: LGTM: Invite claim and logout buttons correctly trigger navigation and auth actions.src/app/api/og/route.tsx (3)
13-13: LGTM: InviteCardOG import is correct.
77-83: LGTM: Arrow sources object is correct.The arrowSrcs object provides the necessary SVG paths for the invite card rendering. Note that a similar object is defined again on lines 150-155 for receipt rendering, but this duplication is acceptable given the different arrow sets needed.
85-105: LGTM: Invite OG card rendering is correct.The conditional rendering for invite cards is properly implemented with correct dimensions, fonts, and props passed to InviteCardOG.
src/components/og/InviteCardOG.tsx (3)
1-4: LGTM: Username width calculation is acceptable.The heuristic approach to calculating scribble width based on character count is reasonable for OG image rendering. While more precise font metrics could be used, the current implementation is sufficient.
6-79: LGTM: Username and scribble layout is correct.The component properly renders the username with an overlaid scribble image. The empty alt attribute for the decorative scribble is correct and prevents layout issues. Based on learnings.
81-142: LGTM: Action text and branding section is correct.The invitation text and branding elements are properly rendered with appropriate fonts, spacing, and alt attributes. The conditional arrows and descriptive alt text for the icon/logo follow best practices.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
src/components/Common/ActionListDaimoPayButton.tsx (1)
144-150: Duplicate payment initiation remains: two charge attempts.The flow calls
handleInitiateDaimoPaymenttwice:
- Line 185 in
handleLoseInvite→ creates charge.- Line 188 triggers
daimoPayButtonClickRef.current()→ callsonClick→ callsonBeforeShowagain.- Line 150 in
onBeforeShow(second call,confirmLoseInvitenow true) → creates charge again.This creates two "Charge Created" attempts, as flagged in previous reviews.
Apply the suggested fix from the previous review: remove the direct call to
handleInitiateDaimoPaymentfromhandleLoseInviteand ensure initiation happens exactly once insideonBeforeShow:onBeforeShow={async () => { - if (!confirmLoseInvite && showConfirmModal) { + // Non‑invite flows: proceed normally + if (!showConfirmModal) { + return await handleInitiateDaimoPayment() + } + // Invite flows: gate on first click, then initiate exactly once + if (!confirmLoseInvite) { setShowInviteModal(true) return false } - // Don't reset confirmLoseInvite here - let it be reset only when modal is closed or payment is initiated + setConfirmLoseInvite(false) // user confirmed losing invite; proceed return await handleInitiateDaimoPayment() }}handleLoseInvite={async () => { setShowInviteModal(false) setConfirmLoseInvite(true) - // Directly initiate the Daimo payment instead of triggering button click - const success = await handleInitiateDaimoPayment() - if (success && daimoPayButtonClickRef.current) { - // Only trigger the actual Daimo widget if payment initiation was successful - daimoPayButtonClickRef.current() - } + // Trigger Daimo button; onBeforeShow will perform the single initiation + daimoPayButtonClickRef.current?.() }}This ensures
handleInitiateDaimoPaymentis called exactly once (inonBeforeShow) and the widget opens only after successful charge creation.Also applies to: 181-189
src/components/Setup/Views/JoinWaitlist.tsx (2)
32-48: Good fix: loading is always cleared via finally.This resolves the previous “stuck loading” issue when validation throws.
66-73: Nice: decode then sanitize redirect_uri before push.Decoding the query param first avoids malformed encoded paths.
🧹 Nitpick comments (2)
src/components/Setup/Views/JoinWaitlist.tsx (2)
113-121: Block “Next” while validation is pending and only persist invite code after it’s valid.Race: with debounce, users can click before validation completes; we advance and persist an invalid code. Gate progression using handleNext(callback) and disable while isChanging.
- <Button - disabled={(inviteCode.length !== 0 && !!error) || isLoading} - onClick={() => { - if (inviteCode.length !== 0) { - dispatch(setupActions.setInviteCode(inviteCode)) - } - - handleNext() - }} + <Button + disabled={(inviteCode.length !== 0 && (isChanging || !!error || isLoading)) || isLoading} + onClick={() => { + if (inviteCode.length === 0) { + handleNext() + return + } + handleNext(async () => { + const ok = await validateInviteCode(inviteCode) + if (ok) { + dispatch(setupActions.setInviteCode(inviteCode)) + } + return ok + }) + }}
22-22: Nit: rename setisLoading → setIsLoading (React state setter convention).Improves readability and consistency.
- const [isLoading, setisLoading] = useState(false) + const [isLoading, setIsLoading] = useState(false)Also update usages:
- setisLoading(true) + setIsLoading(true) ... - setisLoading(false) + setIsLoading(false) ... - disabled={(inviteCode.length !== 0 && !!error) || isLoading} + disabled={(inviteCode.length !== 0 && !!error) || isLoading}Also applies to: 35-35, 46-46, 114-114
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/app/api/og/route.tsx(2 hunks)src/app/invite/page.tsx(1 hunks)src/components/Common/ActionListDaimoPayButton.tsx(4 hunks)src/components/Setup/Views/JoinWaitlist.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/app/invite/page.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-25T11:18:10.633Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1251
File: src/app/invite/page.tsx:44-53
Timestamp: 2025-09-25T11:18:10.633Z
Learning: The setup page (/setup) contains the waitlist functionality in the welcome step using the JoinWaitlist component, so redirecting users to /setup for joining the waitlist is correct.
Applied to files:
src/components/Setup/Views/JoinWaitlist.tsx
🧬 Code graph analysis (3)
src/app/api/og/route.tsx (1)
src/components/og/InviteCardOG.tsx (1)
InviteCardOG(6-142)
src/components/Common/ActionListDaimoPayButton.tsx (3)
src/components/Global/DaimoPayButton/index.tsx (1)
DaimoPayButton(51-200)src/redux/slices/payment-slice.ts (1)
paymentActions(77-77)src/components/SearchUsers/SearchResultCard.tsx (1)
SearchResultCard(19-70)
src/components/Setup/Views/JoinWaitlist.tsx (5)
src/hooks/useZeroDev.ts (1)
useZeroDev(36-179)src/components/0_Bruddle/Toast.tsx (1)
useToast(111-117)src/hooks/useSetupFlow.ts (1)
useSetupFlow(6-68)src/services/invites.ts (1)
invitesApi(7-83)src/utils/general.utils.ts (2)
getFromLocalStorage(112-134)sanitizeRedirectURL(1217-1229)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Deploy-Preview
🔇 Additional comments (4)
src/components/Common/ActionListDaimoPayButton.tsx (1)
192-196: onClose correctly resets state.The
onClosehandler properly resets bothshowInviteModalandconfirmLoseInvitewhen the user dismisses the modal without confirming. This prevents the modal from being bypassed on subsequent clicks.src/app/api/og/route.tsx (3)
13-13: LGTM!Clean import following the existing pattern for OG components.
83-103: LGTM!The invite OG image generation is well-implemented:
- Standard 1200×630 dimensions
- Appropriate font selection for the invite card
- Props correctly match the
InviteCardOGinterface- Early return improves readability
185-212: LGTM!The payment card OG generation correctly uses the top-level
arrowSrcsdefinition, benefiting from the refactoring.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
src/components/Common/ActionList.tsx (1)
194-214: Simplify username assignment inside rawUsername guard.Since line 202 already verifies
rawUsernameis truthy, the ternary on line 203 is redundant. You can safely call.toUpperCase()directly.Apply this diff:
if (isInviteLink && !userHasAppAccess && rawUsername) { - const username = rawUsername ? rawUsername.toUpperCase() : '' + const username = rawUsername.toUpperCase() const inviteCode = `${username}INVITESYOU`src/app/invite/page.tsx (1)
23-87: LGTM: Metadata generation correctly structured.The metadata function properly handles invite validation, constructs dynamic OG images, and falls back gracefully when data is missing. The defensive
siteUrlcheck on line 53 is reasonable, though given line 31's fallback toBASE_URL, it's unlikely to trigger.Optional: Consider simplifying OG image assignment.
Since
siteUrlon line 31 has a fallback toBASE_URL, the check on line 53 is defensive but may be unnecessary. You could simplify:const ogUrl = new URL(`${siteUrl}/api/og`) ogUrl.searchParams.set('isInvite', 'true') ogUrl.searchParams.set('username', inviteCodeData.username) - - if (!siteUrl) { - console.error('Error: Unable to determine site origin') - } else { - ogImageUrl = ogUrl.toString() - } + ogImageUrl = ogUrl.toString()However, keeping the defensive check is also fine for robustness.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/app/invite/page.tsx(1 hunks)src/components/Common/ActionList.tsx(7 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-25T11:18:10.633Z
Learnt from: Zishan-7
PR: peanutprotocol/peanut-ui#1251
File: src/app/invite/page.tsx:44-53
Timestamp: 2025-09-25T11:18:10.633Z
Learning: The setup page (/setup) contains the waitlist functionality in the welcome step using the JoinWaitlist component, so redirecting users to /setup for joining the waitlist is correct.
Applied to files:
src/app/invite/page.tsx
🧬 Code graph analysis (2)
src/app/invite/page.tsx (3)
src/app/actions/invites.ts (1)
validateInviteCode(8-43)src/lib/hosting/get-origin.ts (1)
getOrigin(3-16)src/components/Invites/InvitesPage.tsx (1)
InvitesPage(89-95)
src/components/Common/ActionList.tsx (4)
src/constants/actionlist.consts.ts (1)
PaymentMethod(5-11)src/context/authContext.tsx (1)
useAuth(183-189)src/redux/hooks.ts (1)
useAppDispatch(5-5)src/redux/slices/setup-slice.ts (1)
setupActions(65-65)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Deploy-Preview
🔇 Additional comments (9)
src/components/Common/ActionList.tsx (6)
21-31: LGTM: Imports and prop addition are correct.The new imports support the invite flow integration, and the
isInviteLinkprop is properly typed with a sensible default.Also applies to: 40-40
82-86: LGTM: State additions are appropriate.The
selectedMethodandshowInviteModalstate variables, along with the dispatch hook, are necessary for the invite modal flow.
227-241: LGTM: CTA and invite banner properly guarded.The continue button correctly uses
handleContinueWithPeanut, and the invite banner now guards against rendering "Invited by undefined" by checkingusernameexistence.
246-252: LGTM: DaimoPayButton properly wired with invite gating.The
showConfirmModalprop correctly gates the invite modal based onisInviteLink && !userHasAppAccess, addressing the past review comment about conditional modal display.
257-264: LGTM: MethodCard click handling correctly intercepts invite flow.The logic properly checks for invite context and shows the modal with the selected method, falling back to direct action for non-invite flows.
290-308: LGTM: ConfirmInviteModal properly integrated with cleanup.The modal is correctly wired with both continue and lose-invite paths, and state cleanup is handled appropriately on close and completion.
src/app/invite/page.tsx (3)
7-7: LGTM: Dynamic rendering correctly enforced.The
force-dynamicdirective is appropriate for invite validation that must run on each request.
9-21: LGTM: getInviteCodeData helper is well-structured.The helper correctly validates the invite code and returns structured data or null, with appropriate error handling.
89-91: LGTM: Page component properly delegates to InvitesPage.The default export correctly renders the
InvitesPagecomponent, which handles the invite validation and waitlist redirect logic.Based on learnings: The InvitesPage component handles the waitlist flow, so the redirect concerns from past reviews are addressed at the component level.
There was a problem hiding this comment.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/components/Global/ConfirmInviteModal/index.tsx(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/components/Global/ConfirmInviteModal/index.tsx (1)
src/components/0_Bruddle/Button.tsx (1)
Button(76-267)
There was a problem hiding this comment.
Actionable comments posted: 4
♻️ Duplicate comments (2)
src/components/Profile/index.tsx (1)
34-35: Guard inviteCode against undefined usernames.Using
user?.user.usernamedirectly can yield "undefinedINVITESYOU". Derive from the computedusernamevar on line 32.Apply this diff:
- const inviteCode = `${user?.user.username?.toUpperCase()}INVITESYOU` + const inviteCode = `${username.toUpperCase()}INVITESYOU`src/components/Setup/Views/JoinWaitlist.tsx (1)
71-76: Fix open-redirect fallback in sanitizeRedirectURLsanitizeRedirectURL currently returns the original URL if it’s not same-origin or doesn’t start with
/, enabling open redirects. Change its fallback to a safe path:export const sanitizeRedirectURL = (redirectUrl: string): string => { try { const u = new URL(redirectUrl, window.location.origin) if (u.origin === window.location.origin) { return u.pathname + u.search + u.hash } } catch { if (redirectUrl.startsWith('/')) { return redirectUrl } } - return redirectUrl + return '/home' }
🧹 Nitpick comments (5)
src/components/Global/ConfirmInviteModal/index.tsx (2)
44-44: Remove unnecessary whitespace.The leading
{' '}space is unnecessary here and can be removed.Apply this diff:
- <p> - {' '} - This link lets you skip the Peanut waitlist. Using {method} will skip your - invite. - </p> + <p> + This link lets you skip the Peanut waitlist. Using {method} will skip your + invite. + </p>
70-72: Consider using Tailwind for transform.The inline
styleprop mixes styling approaches. For consistency with the rest of the codebase, consider using Tailwind's arbitrary value syntax.Apply this diff:
- <div - className="absolute left-0 top-7 flex w-full justify-center" - style={{ transform: 'translateY(-80%)' }} - > + <div className="absolute left-0 top-7 flex w-full justify-center -translate-y-[80%]">src/components/Profile/index.tsx (2)
66-72: Reconsider dummy href pattern.The dummy
href="/dummy"with a comment stating it won't be called is a code smell. Consider one of these approaches:
- Use
href="#"to indicate a non-navigating link- Modify
ProfileMenuItemto accept an optionalhrefwhen onlyonClickis needed- Use a button-based variant of the component instead of a link-based one
This makes the intent clearer and avoids confusion.
167-169: Simplify template literals.Both lines wrap a single variable in template literal syntax without concatenation. You can use the variable directly.
Apply this diff:
- <p className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold ">{`${inviteCode}`}</p> + <p className="overflow-hidden text-ellipsis whitespace-nowrap text-sm font-bold ">{inviteCode}</p> - <CopyToClipboard textToCopy={`${inviteCode}`} /> + <CopyToClipboard textToCopy={inviteCode} />src/app/(setup)/layout.tsx (1)
25-25: Includedispatchin the dependency array per React best practices.Although Redux ensures
dispatchis stable, including it in the dependency array aligns with React exhaustive-deps rules and future-proofs the code.- }, [isPWA, selectedStep]) + }, [dispatch, isPWA, selectedStep])
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/app/(mobile-ui)/layout.tsx(2 hunks)src/app/(setup)/layout.tsx(2 hunks)src/app/api/og/route.tsx(2 hunks)src/components/Global/ConfirmInviteModal/index.tsx(1 hunks)src/components/Profile/index.tsx(4 hunks)src/components/Setup/Views/JoinWaitlist.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- src/app/(mobile-ui)/layout.tsx
- src/app/api/og/route.tsx
🧰 Additional context used
🧬 Code graph analysis (4)
src/components/Profile/index.tsx (2)
src/context/authContext.tsx (1)
useAuth(183-189)src/utils/general.utils.ts (1)
generateInvitesShareText(1323-1325)
src/app/(setup)/layout.tsx (4)
src/redux/hooks.ts (1)
useAppDispatch(5-5)src/components/Setup/Setup.consts.tsx (1)
setupSteps(9-91)src/redux/slices/setup-slice.ts (1)
setupActions(65-65)src/components/Global/PeanutLoading/index.tsx (1)
PeanutLoading(4-19)
src/components/Global/ConfirmInviteModal/index.tsx (1)
src/components/0_Bruddle/Button.tsx (1)
Button(76-267)
src/components/Setup/Views/JoinWaitlist.tsx (7)
src/hooks/useZeroDev.ts (1)
useZeroDev(36-179)src/components/0_Bruddle/Toast.tsx (1)
useToast(111-117)src/hooks/useSetupFlow.ts (1)
useSetupFlow(6-68)src/redux/hooks.ts (1)
useAppDispatch(5-5)src/services/invites.ts (1)
invitesApi(7-83)src/utils/general.utils.ts (2)
getFromLocalStorage(112-134)sanitizeRedirectURL(1217-1229)src/redux/slices/setup-slice.ts (1)
setupActions(65-65)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Deploy-Preview
🔇 Additional comments (3)
src/components/Global/ConfirmInviteModal/index.tsx (1)
75-75: Past review comment is outdated.The code already uses the correct Next.js 13+ Image API with the
fillboolean prop andclassName="object-contain"for styling, which is the recommended approach. The deprecatedlayout="fill"andobjectFit="contain"props are not present in the current code.src/components/Setup/Views/JoinWaitlist.tsx (2)
32-48: LGTM: Loading state now correctly reset in all paths.The
finallyblock on Lines 45-46 ensuresisLoadingis reset whether validation succeeds or throws, addressing the critical issue flagged in the previous review.
61-83: LGTM: Redirect decoding and sanitization implemented.Lines 67-72 decode
redirect_uriwith a try-catch fallback, and Lines 71-76 sanitize bothredirect_uriandlocalStorageRedirectbefore pushing, addressing previous review concerns.
…es in SetupLayoutContent
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/(setup)/layout.tsx (1)
13-32: Core PR functionality not implemented: invite code filtering missing.Past reviews flagged this critical issue but it remains unresolved. The PR objectives state "skip Welcome when step=signup and invite present," yet this file doesn't:
- Retrieve
inviteCodefrom Redux usinguseSetupStore- Filter out the 'welcome' step when invite code exists and step=signup
- Include proper dependencies in the useEffect
Apply this diff to implement the missing functionality:
+'use client' + +import { usePWAStatus } from '@/hooks/usePWAStatus' -import { useAppDispatch } from '@/redux/hooks' +import { useAppDispatch, useSetupStore } from '@/redux/hooks' +import { setupActions } from '@/redux/slices/setup-slice' +import { useEffect, Suspense } from 'react' +import { setupSteps } from '../../components/Setup/Setup.consts' +import '../../styles/globals.css' +import { useSearchParams } from 'next/navigation' +import PeanutLoading from '@/components/Global/PeanutLoading' +import { Banner } from '@/components/Global/Banner' + function SetupLayoutContent({ children }: { children?: React.ReactNode }) { const dispatch = useAppDispatch() const isPWA = usePWAStatus() const searchParams = useSearchParams() + const selectedStep = searchParams.get('step') + const { inviteCode } = useSetupStore() useEffect(() => { // filter steps and set them in redux state const filteredSteps = setupSteps.filter((step) => { - return step.screenId !== 'pwa-install' || !isPWA + if (step.screenId === 'pwa-install' && isPWA) { + return false + } + if (step.screenId === 'welcome' && inviteCode && selectedStep === 'signup') { + return false + } + return true }) dispatch(setupActions.setSteps(filteredSteps)) - }, [isPWA]) + }, [dispatch, isPWA, selectedStep, inviteCode])
♻️ Duplicate comments (1)
src/components/Profile/index.tsx (1)
34-36: Guard inviteCode against undefined usernames (duplicate issue).Using
user?.user.usernamedirectly can yield"undefinedINVITESYOU"when the username is undefined. DeriveinviteCodefrom the computedusernamevariable on line 32, which safely falls back to'anonymous'.Apply this diff:
- const inviteCode = `${user?.user.username?.toUpperCase()}INVITESYOU` + const inviteCode = `${username.toUpperCase()}INVITESYOU`
🧹 Nitpick comments (1)
src/components/Profile/index.tsx (1)
64-70: Consider refactoring ProfileMenuItem to optionally omit href.The
href="/dummy"with a comment indicating it won't be called suggests the component API could be improved to makehrefoptional whenonClickis provided. This would avoid the need for dummy values.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/app/(setup)/layout.tsx(2 hunks)src/components/Profile/index.tsx(4 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/Profile/index.tsx (2)
src/context/authContext.tsx (1)
useAuth(183-189)src/utils/general.utils.ts (1)
generateInvitesShareText(1323-1325)
src/app/(setup)/layout.tsx (2)
src/redux/hooks.ts (1)
useAppDispatch(5-5)src/components/Global/PeanutLoading/index.tsx (1)
PeanutLoading(4-19)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: Deploy-Preview
🔇 Additional comments (2)
src/app/(setup)/layout.tsx (1)
34-40: LGTM: Suspense boundary properly wraps dynamic content.The Suspense wrapper with
PeanutLoadingfallback correctly handles the asyncuseSearchParamshook inSetupLayoutContent, preventing hydration mismatches.src/components/Profile/index.tsx (1)
155-178: LGTM! Invite Friends modal is well-implemented.The modal correctly displays the invite code, provides copy functionality, and integrates the share button with
generateInvitesShareText. The description clearly explains the referral rewards.Note: This implementation depends on fixing the
inviteCodeconstruction issue at lines 34-36 to avoid displaying/copying invalid codes.
Also contributes to TASK-14878
Note
Adds invite-only gating with waitlist, a new invite claim page, revamped invites/points UI, and flow guards/modals across claim/request and setup.
Access & Routing:
user.user.hasAppAccess; showJoinWaitlistPagewhen access is missing.inviteand new pagesrc/app/invite/page.tsxvalidating codes and onboarding via signup.InvitesIconand link to/points.Invites & Points UX:
src/app/(mobile-ui)/points/page.tsx: fetch invites, show invite code/link, copy/share, and invited users list.InvitesPageLayout,JoinWaitlistPage,ConfirmInviteModal,Home/InvitesIcon; enhanceCopyToClipboard(button mode) andShareButtonlayout.starStraight.svg,trophyicon.Payment/Claim Flows:
ActionListandActionListDaimoPayButton: supportisInviteLink, show invite-protection modal, and pass through to setup with prefilled invite; enforce min amounts; integrate Daimo confirm modal.isInviteLinkfrom claim/request views.Setup Flow:
JoinWaitliststep; addCollectEmailstep; wrap setup/layout inSuspense.inviteCode,inviteType); accept invite during passkey registration; skip Welcome whenstep=signupand invite present.Services & Types:
invitesApi(accept, validate, list, waitlist position) and server actionvalidateInviteCode.interfaces.UserwithhasAppAccess; addInvite/EInviteType; addgenerateInvitesShareTextutil.Profile & Public Profile:
Written by Cursor Bugbot for commit ed5c25d. This will update automatically on new commits. Configure here.