-
Notifications
You must be signed in to change notification settings - Fork 13
feat: handle onesignal push notification initialization #1247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: peanut-wallet-dev
Are you sure you want to change the base?
Conversation
kushagrasarathe
commented
Sep 23, 2025
- contributes to TASK-14356 : integrated onesignal web sdk for notifications initialization and subscription handling
- also handles notification banners/modal for subscription
WalkthroughAdds OneSignal web push support: env vars, dependency, service worker, notification hook, UI (modal, banner, nav, page), notification service, icon, and setup/layout updates; also removes an unused import from History. 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)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/app/(mobile-ui)/home/page.tsx (2)
168-188
: Remove duplicate balance warning modal effectLines 168-188 are an exact duplicate of the effect on lines 144-165. This appears to be a merge conflict resolution error that left duplicate code.
Apply this diff to remove the duplicate effect:
-// effect for showing balance warning modal -useEffect(() => { - if (isFetchingBalance || balance === undefined) return - - if (typeof window !== 'undefined') { - const hasSeenBalanceWarning = getFromLocalStorage(`${user!.user.userId}-hasSeenBalanceWarning`) - const balanceInUsd = Number(formatUnits(balance, PEANUT_WALLET_TOKEN_DECIMALS)) - - // show if: - // 1. balance is above the threshold - // 2. user hasn't seen this warning in the current session - // 3. no other modals are currently active - if ( - balanceInUsd > BALANCE_WARNING_THRESHOLD && - !hasSeenBalanceWarning && - !showIOSPWAInstallModal && - !showAddMoneyPromptModal - ) { - setShowBalanceWarningModal(true) - } - } -}, [balance, isFetchingBalance, showIOSPWAInstallModal, showAddMoneyPromptModal])
144-165
: Add notification modal state to balance warning dependenciesThe first balance warning modal effect should include
showPermissionModal
andisPostSignupActionModalVisible
in its dependencies to ensure proper modal priority and prevent multiple modals from appearing simultaneously.Apply this diff to fix the dependencies and conditions:
// effect for showing balance warning modal useEffect(() => { if (isFetchingBalance || balance === undefined) return if (typeof window !== 'undefined') { const hasSeenBalanceWarning = getFromLocalStorage(`${user!.user.userId}-hasSeenBalanceWarning`) const balanceInUsd = Number(formatUnits(balance, PEANUT_WALLET_TOKEN_DECIMALS)) // show if: // 1. balance is above the threshold // 2. user hasn't seen this warning in the current session // 3. no other modals are currently active if ( balanceInUsd > BALANCE_WARNING_THRESHOLD && !hasSeenBalanceWarning && + !showPermissionModal && !showIOSPWAInstallModal && !showAddMoneyPromptModal && !isPostSignupActionModalVisible ) { setShowBalanceWarningModal(true) } } -}, [balance, isFetchingBalance, showIOSPWAInstallModal, showAddMoneyPromptModal]) +}, [balance, isFetchingBalance, showPermissionModal, showIOSPWAInstallModal, showAddMoneyPromptModal, isPostSignupActionModalVisible])
🧹 Nitpick comments (4)
src/hooks/useNotifications.ts (1)
74-118
: Optimize timer management for banner schedulingThe
evaluateVisibility
function recursively calls itself viasetTimeout
, which could lead to multiple timers if called rapidly. Consider canceling existing timers before scheduling new ones.Add timer cleanup before scheduling:
// handle scheduled banner display const now = Date.now() const bannerShowAt = typeof bannerShowAtVal === 'number' ? bannerShowAtVal : parseInt(bannerShowAtVal || '0', 10) if (bannerShowAt > 0) { if (now >= bannerShowAt) { setShowReminderBanner(true) } else { setShowReminderBanner(false) // schedule banner to show at the right time if (bannerTimerRef.current) clearTimeout(bannerTimerRef.current) const delay = Math.max(0, bannerShowAt - now) + // Prevent scheduling if delay is too large (>30 days) + if (delay > 30 * 24 * 60 * 60 * 1000) { + console.warn('Banner scheduled too far in future, skipping timer') + return + } bannerTimerRef.current = setTimeout(() => { evaluateVisibility() }, delay) } } else { setShowReminderBanner(false) }src/components/Notifications/SetupNotifcationsModal.tsx (1)
22-22
: Minor: Consider making the modal dismissibleSetting
preventClose={true}
forces users to make a decision, which could be frustrating. Consider allowing dismissal via backdrop click while still hiding the close button.- preventClose={true} + preventClose={false}src/components/Notifications/NotificationBanner.tsx (1)
71-76
: Consider improving the permission-denied messageThe current message about "re-installing the PWA" might be confusing for non-technical users. Consider providing clearer instructions or a link to documentation.
- <span>Please enable notifications to receive updates by re-installing the PWA.</span> + <span>Notifications are currently blocked by your browser settings.</span> <br /> <span> - Unfortunately browser limitations prevent us from re-enabling notifications until you - manually re-install the PWA. + To enable notifications, you'll need to update your browser's permission settings + for this site or reinstall the app. </span>src/app/(mobile-ui)/home/page.tsx (1)
267-270
: Consider error handling for permission requestThe async permission request could fail, but there's no error handling in the onClick handler. Consider wrapping it in a try-catch to handle potential errors gracefully.
Apply this diff to add error handling:
onClick={async () => { - await requestPermission() - await afterPermissionAttempt() + try { + await requestPermission() + await afterPermissionAttempt() + } catch (error) { + console.error('Failed to request notification permission:', error) + // Optionally show user-friendly error message + } }}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yaml
is excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (15)
.env.example
(1 hunks)package.json
(1 hunks)public/OneSignalSDKUpdaterWorker.js
(1 hunks)public/OneSignalSDKWorker.js
(1 hunks)public/onesignal/OneSignalSDKUpdaterWorker.js
(1 hunks)public/onesignal/OneSignalSDKWorker.js
(1 hunks)src/app/(mobile-ui)/history/page.tsx
(0 hunks)src/app/(mobile-ui)/home/page.tsx
(4 hunks)src/components/Global/Icons/Icon.tsx
(3 hunks)src/components/Global/Icons/bell.tsx
(1 hunks)src/components/Notifications/NotificationBanner.tsx
(1 hunks)src/components/Notifications/SetupNotifcationsModal.tsx
(1 hunks)src/components/Setup/Setup.types.ts
(1 hunks)src/components/Setup/components/SetupWrapper.tsx
(3 hunks)src/hooks/useNotifications.ts
(1 hunks)
💤 Files with no reviewable changes (1)
- src/app/(mobile-ui)/history/page.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
src/components/Notifications/NotificationBanner.tsx (1)
src/components/Global/Icons/Icon.tsx (1)
Icon
(199-208)
src/hooks/useNotifications.ts (2)
src/redux/hooks.ts (1)
useUserStore
(13-13)src/utils/general.utils.ts (2)
getFromLocalStorage
(117-139)saveToLocalStorage
(99-115)
src/components/Notifications/SetupNotifcationsModal.tsx (3)
src/hooks/useNotifications.ts (1)
useNotifications
(8-373)src/components/Setup/components/SetupWrapper.tsx (1)
SetupImageSection
(92-166)src/components/0_Bruddle/Button.tsx (1)
Button
(76-267)
src/app/(mobile-ui)/home/page.tsx (2)
src/hooks/useNotifications.ts (1)
useNotifications
(8-373)src/components/Notifications/SetupNotifcationsModal.tsx (1)
SetupNotifcationsModal
(8-65)
src/components/Global/Icons/Icon.tsx (1)
src/components/Global/Icons/bell.tsx (1)
BellIcon
(3-10)
🪛 dotenv-linter (3.3.0)
.env.example
[warning] 61-61: [UnorderedKey] The NEXT_PUBLIC_ONEIGNAL_WEBHOOK key should go before the NEXT_PUBLIC_ONESIGNAL_APP_ID key
(UnorderedKey)
⏰ 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 (12)
src/components/Setup/Setup.types.ts (1)
14-14
: LGTM! Type definition correctly extended.The
LayoutType
union type properly includes the new'notification-permission'
variant, maintaining consistency with the existing pattern and aligning with theScreenId
type.src/components/Global/Icons/bell.tsx (1)
3-10
: Clean SVG icon implementation.The BellIcon component follows good React patterns:
- Proper TypeScript typing with
FC<SVGProps<SVGSVGElement>>
- Uses
currentColor
for theme compatibility- Spreads props for flexibility
- Clean, accessible SVG markup
public/onesignal/OneSignalSDKUpdaterWorker.js (1)
1-1
: Same CDN security concerns and potential duplication.This file has identical security risks as
public/OneSignalSDKWorker.js
(external CDN loading) and appears to be a duplicate worker with the same functionality.public/onesignal/OneSignalSDKWorker.js (1)
1-1
: Third duplicate of the same OneSignal worker.This is yet another file with identical content to the other OneSignal workers, raising the same security concerns and creating maintenance overhead.
public/OneSignalSDKUpdaterWorker.js (1)
1-1
: Fourth duplicate worker file - consolidation needed.This creates the fourth identical OneSignal worker file across the codebase. Consider consolidating to a single worker file to reduce maintenance burden and potential inconsistencies.
Based on the PR summary mentioning multiple worker variants, verify if OneSignal actually requires multiple worker files or if this is over-engineering:
#!/bin/bash # Description: Check OneSignal documentation requirements for multiple worker files # Expected: Understand if multiple worker files are actually needed # Find all OneSignal worker files to assess duplication fd -t f "OneSignal.*Worker\.js" . --exec echo "File: {}" \; --exec cat {} \; --exec echo "---" \; # Search for any references to these specific worker file paths in the codebase rg -i "onesignal.*worker" --type=js --type=ts -C2package.json (1)
72-72
: react-onesignal v3.2.3 — verified; no known runtime vulnerabilitiesnpm shows 3.2.3 as the latest; no direct vulnerabilities reported (CVE-2023-28430 affected OneSignal GitHub Actions workflows, not runtime). ^3.2.3 allows future minor/patch updates (semver); pin to 3.2.3 or use ~3.2.3 if you want stricter update control.
src/components/Setup/components/SetupWrapper.tsx (2)
42-42
: Good alignment with existing layout patternsThe new
notification-permission
layout type follows the established pattern and maintains consistency with other layout types.
92-100
: LGTM - Well-structured component exportThe
SetupImageSection
component is properly exported with clear prop types and the newcustomContainerClass
prop provides good flexibility for layout customization.src/components/Global/Icons/Icon.tsx (1)
61-61
: LGTM - Clean icon integrationThe bell icon is properly integrated into the icon system following the established pattern.
Also applies to: 72-72, 141-141
src/components/Notifications/NotificationBanner.tsx (1)
24-28
: Good practice: Proper event propagation handlingThe
stopPropagation
call correctly prevents the click event from bubbling up to the parent Card component.src/app/(mobile-ui)/home/page.tsx (2)
263-275
: Good implementation of notification UI components!The integration of the notification modal and banner is well-structured:
- Modal is conditionally rendered based on
showPermissionModal
- Banner correctly handles both permission requests and closure actions
- The async flow with
afterPermissionAttempt
ensures proper state updates
49-56
: Typo in imported component name:SetupNotifcationsModal
should beSetupNotificationsModal
The import statement has a typo in the component name. This inconsistency could lead to confusion and should be corrected to maintain naming consistency throughout the codebase.
Apply this diff to fix the typo:
-import SetupNotifcationsModal from '@/components/Notifications/SetupNotifcationsModal' +import SetupNotificationsModal from '@/components/Notifications/SetupNotificationsModal'Also update the usage on line 263:
-{showPermissionModal && <SetupNotifcationsModal />} +{showPermissionModal && <SetupNotificationsModal />}⛔ Skipped due to learnings
Learnt from: Zishan-7 PR: peanutprotocol/peanut-ui#1072 File: src/app/(setup)/setup/page.tsx:173-175 Timestamp: 2025-08-07T12:53:50.946Z Learning: In the peanut-ui setup flow at `src/app/(setup)/setup/page.tsx`, when handling unsupported scenarios, both device not supported and browser not supported cases should show the same "Unsupported browser" message using the `UnsupportedBrowserModal` component, rather than having distinct messaging for each scenario.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (1)
.env.example (1)
58-61
: Fix dotenv-linter ordering warning.Reorder keys to satisfy dotenv-linter.
-# one signal -export NEXT_PUBLIC_ONESIGNAL_APP_ID= -export NEXT_PUBLIC_SAFARI_WEB_ID= -export NEXT_PUBLIC_ONESIGNAL_WEBHOOK= +# one signal +export NEXT_PUBLIC_ONESIGNAL_APP_ID= +export NEXT_PUBLIC_ONESIGNAL_WEBHOOK= +export NEXT_PUBLIC_SAFARI_WEB_ID=
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
.env.example
(1 hunks)src/components/Notifications/NotificationBanner.tsx
(1 hunks)src/hooks/useNotifications.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/components/Notifications/NotificationBanner.tsx
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-07-07T20:22:11.092Z
Learnt from: jjramirezn
PR: peanutprotocol/peanut-ui#958
File: src/app/actions/tokens.ts:266-266
Timestamp: 2025-07-07T20:22:11.092Z
Learning: In `src/app/actions/tokens.ts`, within the `fetchWalletBalances` function, using the non-null assertion operator `!` on `process.env.MOBULA_API_KEY!` is intentional and correct, and should not be flagged for replacement with explicit validation.
Applied to files:
src/hooks/useNotifications.ts
📚 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/hooks/useNotifications.ts
📚 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:
.env.example
🧬 Code graph analysis (1)
src/hooks/useNotifications.ts (2)
src/redux/hooks.ts (1)
useUserStore
(13-13)src/utils/general.utils.ts (2)
getFromLocalStorage
(117-139)saveToLocalStorage
(99-115)
🪛 dotenv-linter (3.3.0)
.env.example
[warning] 61-61: [UnorderedKey] The NEXT_PUBLIC_ONESIGNAL_WEBHOOK key should go before the NEXT_PUBLIC_SAFARI_WEB_ID key
(UnorderedKey)
⏰ 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 (6)
.env.example (1)
61-61
: Verify webhook value is safe to expose publicly.This is a NEXT_PUBLIC_ value and will be bundled to the client. Ensure it contains no secrets/tokens. If the endpoint needs credentials, move that handling server‑side and expose a safe public endpoint.
Would you confirm that NEXT_PUBLIC_ONESIGNAL_WEBHOOK points to a tokenless, public ingestion URL? If not, I can propose a server‑side proxy pattern.
src/hooks/useNotifications.ts (5)
236-257
: Avoid empty catch blocks; log at least debug-level context.Silent failures hinder debugging. Log and continue.
try { await OneSignal.User.PushSubscription.optIn() - } catch (_) {} + } catch (error) { + console.debug('OneSignal optIn failed (may be normal if already opted in):', error) + } @@ } catch (error) { console.warn('Error with slidedown prompt, trying direct permission request:', error) try { await OneSignal.Notifications.requestPermission() - } catch (_) {} + } catch (err2) { + console.debug('Direct permission request failed:', err2) + } }const logoutUser = async () => { try { await OneSignal.logout() - } catch (_) {} + } catch (error) { + console.debug('OneSignal.logout failed:', error) + } }Also applies to: 312-314
126-184
: Solid singleton bootstrap to prevent duplicate SDK inits.The in-flight promise and idempotent flags are correct and avoid race conditions and double listeners.
186-221
: Event listener setup looks correct.Listeners are added once and tied to state refresh; good use of a gate to prevent duplicates.
55-56
: OneSignal.Notifications.permission is boolean — current check is correct.It returns true/false (true when granted), so
resolve(perm === true)
is valid; you can optionally simplify toresolve(perm)
.
142-173
: Remove non-null assertion and make webhooks optional.If NEXT_PUBLIC_ONESIGNAL_WEBHOOK is unset, the current code passes undefined strings into OneSignal.init and may cause init failures.
- const webhookUrl = process.env.NEXT_PUBLIC_ONESIGNAL_WEBHOOK! + const webhookUrl = process.env.NEXT_PUBLIC_ONESIGNAL_WEBHOOK @@ - await OneSignal.init({ + const webhooks = + webhookUrl + ? { + cors: true, + 'notification.willDisplay': webhookUrl, + 'notification.clicked': webhookUrl, + } + : undefined + + await OneSignal.init({ appId: appId, safari_web_id: safariWebId, @@ - webhooks: { - cors: true, - 'notification.willDisplay': webhookUrl, - 'notification.clicked': webhookUrl, - }, + webhooks,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Havent finished review (no time for useNotifs hook)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
src/components/Notifications/SetupNotificationsModal.tsx (2)
21-23
: Modal is non-dismissible; confirm UX/accessibility intentpreventClose={true} with a hidden close button forces a choice. For accessibility, consider allowing ESC/backdrop close or provide a clear alternative path if this is not intended to be a blocking modal.
36-42
: Consider i18n for user-facing copyStrings are hard-coded. If the app uses i18n elsewhere, route these through the translation layer.
src/app/(mobile-ui)/home/page.tsx (1)
263-276
: Prevent simultaneous banner+modal; ensure post-attempt runsGuard the banner when the modal is visible and always run afterPermissionAttempt even if the request throws.
- {showReminderBanner && ( + {showReminderBanner && !showPermissionModal && ( <NotificationBanner isPermissionDenied={isPermissionDenied} - onClick={async () => { - await requestPermission() - await afterPermissionAttempt() - }} + onClick={async () => { + try { + await requestPermission() + } finally { + await afterPermissionAttempt() + } + }} onClose={() => { snoozeReminderBanner() }} /> )}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
public/onesignal/OneSignalSDKWorker.js
(1 hunks)src/app/(mobile-ui)/home/page.tsx
(4 hunks)src/components/Notifications/NotificationBanner.tsx
(1 hunks)src/components/Notifications/SetupNotificationsModal.tsx
(1 hunks)src/hooks/useNotifications.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/components/Notifications/NotificationBanner.tsx
- public/onesignal/OneSignalSDKWorker.js
- src/hooks/useNotifications.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/components/Notifications/SetupNotificationsModal.tsx (3)
src/hooks/useNotifications.ts (1)
useNotifications
(11-390)src/components/Setup/components/SetupWrapper.tsx (1)
SetupImageSection
(92-166)src/components/0_Bruddle/Button.tsx (1)
Button
(76-267)
src/app/(mobile-ui)/home/page.tsx (2)
src/hooks/useNotifications.ts (1)
useNotifications
(11-390)src/components/Notifications/SetupNotificationsModal.tsx (1)
SetupNotificationsModal
(8-69)
⏰ 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 (7)
src/components/Notifications/SetupNotificationsModal.tsx (2)
45-56
: Always run afterPermissionAttempt; add loading state to prevent double-clicksEnsure post-permission evaluation happens even on failures and avoid accidental repeated clicks.
- <Button - variant="purple" - shadowSize="4" - onClick={async () => { - try { - await requestPermission() - await afterPermissionAttempt() - } finally { - // Always close, even if requestPermission throws or user cancels - closePermissionModal() - } - }} - > + <Button + variant="purple" + shadowSize="4" + loading={isRequesting} + disabled={isRequesting} + onClick={async () => { + setIsRequesting(true) + try { + await requestPermission() + } catch (error) { + console.error('Notification permission request failed:', error) + } finally { + await afterPermissionAttempt() + closePermissionModal() + setIsRequesting(false) + } + }} + >Add state and import (outside the selected range):
import { useState } from 'react' // inside component const [isRequesting, setIsRequesting] = useState(false)
2-2
: Verify asset path spellingConfirm the folder name is correct: GIF_ALPHA_BACKGORUND vs. BACKGROUND. If it’s a typo, fix the path to avoid runtime import failures.
src/app/(mobile-ui)/home/page.tsx (5)
41-44
: Imports look goodHooks and components for notifications are correctly wired.
49-56
: Hook usage LGTMDestructured values align with the hook’s contract.
198-201
: Good prioritization commentThe inline docs clarify modal precedence.
203-212
: Block add-money prompt when permission modal is openGating with !showPermissionModal prevents overlapping prompts. Good call.
214-221
: Dependency array updated correctlyIncluding showPermissionModal avoids stale evaluations.
feat: notifications in-app center
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (5)
src/components/Notifications/NotificationNavigation.tsx (1)
10-12
: Remove unused loading state, add unmount guard, and improve a11y
- isLoading is unused and causes extra re-renders.
- Guard setState on unmount to avoid React state update warnings.
- Add aria-label for the link.
Apply this diff:
export default function NotificationNavigation() { const [notificationCount, setNotificationCount] = useState<number>(0) - const [, setIsLoading] = useState<boolean>(false) useEffect(() => { - const fetchNotificationCount = async () => { - setIsLoading(true) - try { - const { count } = await notificationsApi.unreadCount() - setNotificationCount(count) - } catch (error) { - console.error(error) - } finally { - setIsLoading(false) - } - } - fetchNotificationCount() + let isMounted = true + ;(async () => { + try { + const { count } = await notificationsApi.unreadCount() + if (isMounted) setNotificationCount(count) + } catch (error) { + console.error(error) + } + })() + return () => { + isMounted = false + } }, []) return ( - <Link href={'/notifications'} className={twMerge('relative', notificationCount > 0 && 'animate-pulsate')}> + <Link + href={'/notifications'} + aria-label="Notifications" + className={twMerge('relative', notificationCount > 0 && 'animate-pulsate')} + > <Icon name="bell" size={20} /> {notificationCount > 0 && <div className="absolute -right-1 -top-1.5 h-2 w-2 rounded-full bg-orange-2" />} </Link> ) }Also applies to: 13-26, 28-32
src/app/(mobile-ui)/notifications/page.tsx (1)
47-62
: Disconnect IntersectionObserver in cleanupUnobserving a single element is fine, but disconnecting is safer and prevents leaks if the ref changes.
Apply this diff:
useEffect(() => { - const observer = new IntersectionObserver( + const observer = new IntersectionObserver( (entries) => { const target = entries[0] if (target.isIntersecting && nextPageCursor && !isLoadingMore) { void loadNextPage() } }, { threshold: 0.1 } ) - const element = loadingRef.current - if (element) observer.observe(element) + const element = loadingRef.current + if (element) observer.observe(element) return () => { - if (element) observer.unobserve(element) + observer.disconnect() } }, [nextPageCursor, isLoadingMore])src/services/notifications.ts (3)
29-39
: Avoid "Bearer undefined" and fail fast when unauthenticatedGuard against missing jwt-token and don’t send an Authorization header with undefined.
Apply this diff:
- const token = Cookies.get('jwt-token') + const token = Cookies.get('jwt-token') + if (!token) throw new Error('not authenticated') const url = `${PEANUT_API_URL}/notifications?${search.toString()}` try { const response = await fetch(url, { headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${token}`, }, })
46-55
: Same auth guard for unreadCountApply this diff:
- const response = await fetch(`${PEANUT_API_URL}/notifications/unread-count`, { + const token = Cookies.get('jwt-token') + if (!token) throw new Error('not authenticated') + const response = await fetch(`${PEANUT_API_URL}/notifications/unread-count`, { headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${Cookies.get('jwt-token')}`, + Authorization: `Bearer ${token}`, }, })
57-66
: Same auth guard for markReadApply this diff:
- const response = await fetch(`${PEANUT_API_URL}/notifications/mark-read`, { + const token = Cookies.get('jwt-token') + if (!token) throw new Error('not authenticated') + const response = await fetch(`${PEANUT_API_URL}/notifications/mark-read`, { method: 'POST', headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${Cookies.get('jwt-token')}`, + Authorization: `Bearer ${token}`, }, body: JSON.stringify({ ids }), })
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/app/(mobile-ui)/home/page.tsx
(5 hunks)src/app/(mobile-ui)/notifications/page.tsx
(1 hunks)src/components/Notifications/NotificationNavigation.tsx
(1 hunks)src/services/notifications.ts
(1 hunks)tailwind.config.js
(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/services/notifications.ts (1)
src/constants/general.consts.ts (1)
PEANUT_API_URL
(43-47)
src/app/(mobile-ui)/notifications/page.tsx (6)
src/services/notifications.ts (2)
InAppItem
(4-16)notificationsApi
(20-69)src/utils/dateGrouping.utils.ts (3)
getDateGroup
(69-93)getDateGroupKey
(128-148)formatGroupHeaderDate
(102-120)src/components/Global/PeanutLoading/index.tsx (1)
PeanutLoading
(4-19)src/components/Global/EmptyStates/EmptyState.tsx (1)
EmptyState
(13-28)src/components/0_Bruddle/Button.tsx (1)
Button
(76-267)src/components/Global/Card/index.tsx (1)
CardPosition
(4-4)
src/components/Notifications/NotificationNavigation.tsx (2)
src/services/notifications.ts (1)
notificationsApi
(20-69)src/components/Global/Icons/Icon.tsx (1)
Icon
(199-208)
src/app/(mobile-ui)/home/page.tsx (4)
src/hooks/useNotifications.ts (1)
useNotifications
(11-390)src/components/SearchUsers/index.tsx (1)
SearchUsers
(82-121)src/components/Notifications/NotificationNavigation.tsx (1)
NotificationNavigation
(9-34)src/components/Notifications/SetupNotificationsModal.tsx (1)
SetupNotificationsModal
(8-69)
⏰ 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 (6)
tailwind.config.js (1)
89-92
: LGTM: new color tokenorange.2 looks consistent and is used by the unread badges.
src/app/(mobile-ui)/notifications/page.tsx (2)
81-98
: Assumes API returns items sorted by createdAtGrouping relies on contiguous items sharing the same group; unsorted data will misgroup. Ensure the API returns items sorted (e.g., createdAt desc), or sort locally.
Would you confirm the backend returns notifications sorted by createdAt (desc)? If not, consider sorting client-side before grouping.
156-163
: No action required — remote images are allowednext.config.js includes images.remotePatterns with hostname '*' (around lines 27–31), so notif.iconUrl will be permitted.
src/app/(mobile-ui)/home/page.tsx (3)
49-58
: OneSignal init failure fallback (duplicate of prior review)If OneSignal.init fails/blocked, the hook may suppress native Notification flows. Please address per the earlier comment.
199-222
: LGTM: gating add-money prompt on permission modalGood prioritization to avoid modal conflicts; deps updated accordingly.
237-240
: LGTM: header integrates NotificationNavigationPlacement alongside SearchUsers is clean and minimal.
<Link href={href ?? ''} className="relative flex items-start gap-3"> | ||
<Image | ||
src={notif.iconUrl ?? PEANUTMAN_LOGO} | ||
alt="icon" | ||
width={32} | ||
height={32} | ||
className="size-8 self-center" | ||
/> | ||
|
||
<div className="flex min-w-0 flex-col"> | ||
<div className="flex items-center gap-2"> | ||
<div className="truncate font-semibold">{notif.title}</div> | ||
</div> | ||
{notif.body ? ( | ||
<div className="truncate text-sm text-gray-600"> | ||
{notif.body} | ||
</div> | ||
) : null} | ||
</div> | ||
{!notif.state.readAt ? ( | ||
<span className="absolute -right-3 top-0 size-2 rounded-full bg-orange-2" /> | ||
) : null} | ||
</Link> | ||
</Card> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid invalid Next.js Link href when ctaDeeplink is missing
Passing an empty string to Link can cause runtime navigation errors. Render a non-Link container when href is absent.
Apply this diff:
- <Link href={href ?? ''} className="relative flex items-start gap-3">
+ {href ? (
+ <Link href={href} className="relative flex items-start gap-3">
+ <Image
+ src={notif.iconUrl ?? PEANUTMAN_LOGO}
+ alt="icon"
+ width={32}
+ height={32}
+ className="size-8 self-center"
+ />
+
+ <div className="flex min-w-0 flex-col">
+ <div className="flex items-center gap-2">
+ <div className="truncate font-semibold">{notif.title}</div>
+ </div>
+ {notif.body ? (
+ <div className="truncate text-sm text-gray-600">{notif.body}</div>
+ ) : null}
+ </div>
+ {!notif.state.readAt ? (
+ <span className="absolute -right-3 top-0 size-2 rounded-full bg-orange-2" />
+ ) : null}
+ </Link>
+ ) : (
+ <div className="relative flex items-start gap-3" aria-disabled="true">
+ <Image
+ src={notif.iconUrl ?? PEANUTMAN_LOGO}
+ alt="icon"
+ width={32}
+ height={32}
+ className="size-8 self-center"
+ />
+
+ <div className="flex min-w-0 flex-col">
+ <div className="flex items-center gap-2">
+ <div className="truncate font-semibold">{notif.title}</div>
+ </div>
+ {notif.body ? (
+ <div className="truncate text-sm text-gray-600">{notif.body}</div>
+ ) : null}
+ </div>
+ {!notif.state.readAt ? (
+ <span className="absolute -right-3 top-0 size-2 rounded-full bg-orange-2" />
+ ) : null}
+ </div>
+ )}
- </Link>
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
<Link href={href ?? ''} className="relative flex items-start gap-3"> | |
<Image | |
src={notif.iconUrl ?? PEANUTMAN_LOGO} | |
alt="icon" | |
width={32} | |
height={32} | |
className="size-8 self-center" | |
/> | |
<div className="flex min-w-0 flex-col"> | |
<div className="flex items-center gap-2"> | |
<div className="truncate font-semibold">{notif.title}</div> | |
</div> | |
{notif.body ? ( | |
<div className="truncate text-sm text-gray-600"> | |
{notif.body} | |
</div> | |
) : null} | |
</div> | |
{!notif.state.readAt ? ( | |
<span className="absolute -right-3 top-0 size-2 rounded-full bg-orange-2" /> | |
) : null} | |
</Link> | |
</Card> | |
{href ? ( | |
<Link href={href} className="relative flex items-start gap-3"> | |
<Image | |
src={notif.iconUrl ?? PEANUTMAN_LOGO} | |
alt="icon" | |
width={32} | |
height={32} | |
className="size-8 self-center" | |
/> | |
<div className="flex min-w-0 flex-col"> | |
<div className="flex items-center gap-2"> | |
<div className="truncate font-semibold">{notif.title}</div> | |
</div> | |
{notif.body ? ( | |
<div className="truncate text-sm text-gray-600">{notif.body}</div> | |
) : null} | |
</div> | |
{!notif.state.readAt ? ( | |
<span className="absolute -right-3 top-0 size-2 rounded-full bg-orange-2" /> | |
) : null} | |
</Link> | |
) : ( | |
<div className="relative flex items-start gap-3" aria-disabled="true"> | |
<Image | |
src={notif.iconUrl ?? PEANUTMAN_LOGO} | |
alt="icon" | |
width={32} | |
height={32} | |
className="size-8 self-center" | |
/> | |
<div className="flex min-w-0 flex-col"> | |
<div className="flex items-center gap-2"> | |
<div className="truncate font-semibold">{notif.title}</div> | |
</div> | |
{notif.body ? ( | |
<div className="truncate text-sm text-gray-600">{notif.body}</div> | |
) : null} | |
</div> | |
{!notif.state.readAt ? ( | |
<span className="absolute -right-3 top-0 size-2 rounded-full bg-orange-2" /> | |
) : null} | |
</div> | |
)} | |
</Card> |