Skip to content

Conversation

kushagrasarathe
Copy link
Contributor

  • contributes to TASK-14356 : integrated onesignal web sdk for notifications initialization and subscription handling
  • also handles notification banners/modal for subscription

Copy link

Integrate OneSignal

Copy link
Contributor

coderabbitai bot commented Sep 23, 2025

Walkthrough

Adds 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

Cohort / File(s) Summary
Environment configuration
\.env.example
Adds NEXT_PUBLIC_ONESIGNAL_APP_ID, NEXT_PUBLIC_SAFARI_WEB_ID, NEXT_PUBLIC_ONESIGNAL_WEBHOOK under a OneSignal section.
Dependencies
package.json
Adds "react-onesignal": "^3.2.3" to dependencies.
OneSignal service worker
public/onesignal/OneSignalSDKWorker.js
New worker file loading OneSignal v16 SDK via CDN (importScripts).
Mobile UI pages
src/app/(mobile-ui)/home/page.tsx, src/app/(mobile-ui)/history/page.tsx
home: integrates notification UI (modal/banner/navigation), updates effects and gating. history: removes unused usePathname usage.
Notifications UI
src/components/Notifications/NotificationBanner.tsx, src/components/Notifications/SetupNotificationsModal.tsx, src/components/Notifications/NotificationNavigation.tsx, src/app/(mobile-ui)/notifications/page.tsx
Adds banner, setup modal, nav bell with unread count, and a Notifications page with pagination, grouping, and mark-read behavior.
Notifications hook & service
src/hooks/useNotifications.ts, src/services/notifications.ts
New useNotifications hook (OneSignal init, permission flow, timers, linking) and notificationsApi (list, unreadCount, markRead) with types.
Icons
src/components/Global/Icons/Icon.tsx, src/components/Global/Icons/bell.tsx
Adds BellIcon component; registers 'bell' in IconName and mapping.
Setup/layout updates
src/components/Setup/Setup.types.ts, src/components/Setup/components/SetupWrapper.tsx
Adds notification-permission layout type; exports SetupImageSection with optional customContainerClass; aligns image rendering for new layout.
Styling config
tailwind.config.js
Extends orange palette with a new shade (2: #FF5656).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Zishan-7
  • jjramirezn

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title "feat: handle onesignal push notification initialization" accurately captures the primary change in the PR — integrating OneSignal initialization and related notification handling. It is concise, specific, and maps directly to the added env keys, dependency, service worker, hook, and UI components. There are no extraneous words or misleading references.
Description Check ✅ Passed The PR description states integration of the OneSignal Web SDK and handling for notification banners/modals and references TASK-14356, which aligns with the changes shown (env vars, dependency, worker, hook, UI components, and services). It is brief but directly relevant to the changeset and not off-topic. This satisfies the lenient acceptance criteria for the description.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/notifications-fixed

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

vercel bot commented Sep 23, 2025

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

Project Deployment Preview Comments Updated (UTC)
peanut-wallet Ready Ready Preview Comment Sep 24, 2025 2:40pm

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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 effect

Lines 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 dependencies

The first balance warning modal effect should include showPermissionModal and isPostSignupActionModalVisible 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 scheduling

The evaluateVisibility function recursively calls itself via setTimeout, 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 dismissible

Setting 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 message

The 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 request

The 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8a18dff and 0b2f69b.

⛔ 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 the ScreenId 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 -C2
package.json (1)

72-72: react-onesignal v3.2.3 — verified; no known runtime vulnerabilities

npm 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 patterns

The new notification-permission layout type follows the established pattern and maintains consistency with other layout types.


92-100: LGTM - Well-structured component export

The SetupImageSection component is properly exported with clear prop types and the new customContainerClass prop provides good flexibility for layout customization.

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

61-61: LGTM - Clean icon integration

The 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 handling

The 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 be SetupNotificationsModal

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (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

📥 Commits

Reviewing files that changed from the base of the PR and between 0b2f69b and 88af85c.

📒 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 to resolve(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,

Copy link
Contributor

@Hugo0 Hugo0 left a comment

Choose a reason for hiding this comment

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

Havent finished review (no time for useNotifs hook)

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/components/Notifications/SetupNotificationsModal.tsx (2)

21-23: Modal is non-dismissible; confirm UX/accessibility intent

preventClose={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 copy

Strings 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 runs

Guard 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

📥 Commits

Reviewing files that changed from the base of the PR and between 88af85c and e89783c.

📒 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-clicks

Ensure 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 spelling

Confirm 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 good

Hooks and components for notifications are correctly wired.


49-56: Hook usage LGTM

Destructured values align with the hook’s contract.


198-201: Good prioritization comment

The inline docs clarify modal precedence.


203-212: Block add-money prompt when permission modal is open

Gating with !showPermissionModal prevents overlapping prompts. Good call.


214-221: Dependency array updated correctly

Including showPermissionModal avoids stale evaluations.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (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 cleanup

Unobserving 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 unauthenticated

Guard 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 unreadCount

Apply 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 markRead

Apply 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

📥 Commits

Reviewing files that changed from the base of the PR and between e89783c and e352b5f.

📒 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 token

orange.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 createdAt

Grouping 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 allowed

next.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 modal

Good prioritization to avoid modal conflicts; deps updated accordingly.


237-240: LGTM: header integrates NotificationNavigation

Placement alongside SearchUsers is clean and minimal.

Comment on lines +156 to +179
<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>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
<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>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants