From 933e0084cbf14df96fc7fc78b991b029cfe45e10 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Tue, 11 Nov 2025 16:49:14 -0800 Subject: [PATCH 1/2] RU-T39 Fix for push notification issue on android sound. --- .github/workflows/react-native-cicd.yml | 14 +- ...s-foreground-notifications-firebase-fix.md | 376 ++++++++++++++++++ expo-env.d.ts | 2 +- plugins/withForegroundNotifications.js | 29 +- src/services/app-initialization.service.ts | 24 ++ .../push-notification/__tests__/store.test.ts | 47 ++- src/stores/push-notification/store.ts | 13 +- 7 files changed, 467 insertions(+), 38 deletions(-) create mode 100644 docs/ios-foreground-notifications-firebase-fix.md diff --git a/.github/workflows/react-native-cicd.yml b/.github/workflows/react-native-cicd.yml index 11a86e04..15ff9545 100644 --- a/.github/workflows/react-native-cicd.yml +++ b/.github/workflows/react-native-cicd.yml @@ -352,7 +352,11 @@ jobs: PR_BODY=$(gh pr view "$PR_FROM_COMMIT" --json body --jq '.body' 2>/dev/null || echo "") if [ -n "$PR_BODY" ]; then + echo "PR body length: ${#PR_BODY}" NOTES="$(extract_release_notes "$PR_BODY")" + echo "Extracted notes length: ${#NOTES}" + else + echo "Warning: PR body is empty" fi else echo "No PR reference in commit message, searching by commit SHA..." @@ -370,7 +374,11 @@ jobs: PR_BODY=$(gh pr view "$PR_NUMBER" --json body --jq '.body' 2>/dev/null || echo "") if [ -n "$PR_BODY" ]; then + echo "PR body length: ${#PR_BODY}" NOTES="$(extract_release_notes "$PR_BODY")" + echo "Extracted notes length: ${#NOTES}" + else + echo "Warning: PR body is empty" fi else echo "No associated PR found for this commit" @@ -378,10 +386,10 @@ jobs: fi fi - # Fallback to recent commits if no PR body found + # Fallback to recent commits if no PR body found (skip merge commits) if [ -z "$NOTES" ]; then - echo "No PR body found, using recent commits..." - NOTES="$(git log -n 5 --pretty=format:'- %s')" + echo "No PR body found, using recent commits (excluding merge commits)..." + NOTES="$(git log -n 10 --pretty=format:'- %s' --no-merges | head -n 5)" fi # Fail if no notes extracted diff --git a/docs/ios-foreground-notifications-firebase-fix.md b/docs/ios-foreground-notifications-firebase-fix.md new file mode 100644 index 00000000..bcf96270 --- /dev/null +++ b/docs/ios-foreground-notifications-firebase-fix.md @@ -0,0 +1,376 @@ +# iOS Foreground Notifications Fix for Firebase Messaging + +## Problem + +# iOS Foreground Notifications Fix for Firebase Messaging + +## Problem + +iOS push notifications were not being delivered to the React Native app when it's in the foreground. The in-app modal wasn't appearing because the notification handling needed proper configuration. + +## Root Cause + +The initial implementation had two issues: + +1. **First attempt**: Custom delegate methods were calling completion handlers but not properly coordinating with Firebase Messaging +2. **Second attempt**: Removing delegate methods completely prevented iOS from displaying notifications in foreground + +## Solution + +Implement `UNUserNotificationCenterDelegate` methods that: +1. ✅ Tell iOS to display the notification banner in foreground +2. ✅ Allow Firebase Messaging to also process the notification +3. ✅ Both work together: native banner + JavaScript onMessage() handler + +## How It Works + +When a push notification arrives while app is in foreground: + +1. **iOS delivers notification to AppDelegate** +2. **`willPresent` delegate method runs**: + - Tells iOS to show banner, play sound, update badge + - Returns immediately via completion handler +3. **Firebase Messaging processes notification**: + - Automatically forwards to `messaging().onMessage()` + - Your `handleRemoteMessage` function runs + - In-app modal appears +4. **User sees both**: + - Native iOS banner at top + - In-app modal for interaction + +## Changes Made + +### 1. Updated Plugin ✅ + +The `plugins/withForegroundNotifications.js` now: +- Adds `UNUserNotificationCenterDelegate` conformance +- Sets delegate: `UNUserNotificationCenter.current().delegate = self` +- Implements `willPresent` to display foreground notifications +- Implements `didReceive` for notification tap handling +- Includes proper documentation + +### 2. Clean and Rebuild iOS + +Run the following commands to regenerate AppDelegate.swift with the correct delegate: + +```bash +# Remove the iOS folder to force regeneration +rm -rf ios + +# Regenerate native code with updated plugin +yarn prebuild + +# Or for specific environment +yarn prebuild:production # or staging, internal, development + +# Install pods +cd ios && pod install && cd .. + +# Rebuild the app +yarn ios +``` + +### 3. Verify AppDelegate.swift + +After rebuild, verify that `ios/ResgridUnit/AppDelegate.swift` includes: + +```swift +import UserNotifications + +public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate { + + public override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + // ... setup code ... + + FirebaseApp.configure() + + // Set the delegate + UNUserNotificationCenter.current().delegate = self + + // ... rest of method ... + } + + // MARK: - UNUserNotificationCenterDelegate + + public func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + // Display banner, sound, and badge in foreground + if #available(iOS 14.0, *) { + completionHandler([.banner, .sound, .badge]) + } else { + completionHandler([.alert, .sound, .badge]) + } + } + + public func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + completionHandler() + } +} +``` + +## Expected Behavior After Fix + +When a push notification arrives while app is in foreground: + +✅ Native iOS banner appears at top of screen +✅ Notification sound plays +✅ Badge updates +✅ Firebase Messaging forwards to JavaScript +✅ `handleRemoteMessage` is called with notification data +✅ In-app modal appears with notification details +✅ Sound plays via notification sound service (in addition to system sound) +✅ User can interact with modal (dismiss or view call) + +## Testing + +After rebuilding: + +1. **Test Foreground Notification**: + - Open the app + - Send a test push notification from backend + - Verify native banner appears at top + - Verify in-app modal also appears + - Verify sound plays + +2. **Test Background Notification**: + - Put app in background + - Send a test push notification + - Tap the notification + - Verify app opens and modal appears + +3. **Test Killed State**: + - Force quit the app + - Send a test push notification + - Tap the notification + - Verify app launches and modal appears + +## Why This Approach Works + +The key insight is that **both systems can work together**: + +1. **`UNUserNotificationCenterDelegate.willPresent`**: + - Controls iOS native UI (banner, sound, badge) + - Just tells iOS "yes, show this notification" + - Doesn't intercept or consume the notification data + +2. **Firebase Messaging**: + - Automatically receives all notifications + - Forwards to `messaging().onMessage()` in JavaScript + - Independent of the delegate's decision to show/hide banner + +They're not mutually exclusive - calling `completionHandler([.banner, .sound, .badge])` doesn't prevent Firebase from also processing the notification. + +## What Changed from Original Implementation + +**Original (Broken)**: +- Had delegate methods but no clear integration with Firebase +- Unclear if Firebase was receiving notifications + +**First Fix Attempt (Also Broken)**: +- Removed delegate methods completely +- Firebase received notifications but iOS didn't show them +- No native banner in foreground + +**Final Fix (Working)**: +- Delegate methods tell iOS to show banner +- Firebase independently processes and forwards to JS +- Both native banner AND in-app modal work + +## Related Files + +- `plugins/withForegroundNotifications.js` - Plugin configuration +- `src/services/push-notification.ts` - Push notification service +- `src/stores/push-notification/store.ts` - Modal state management +- `src/components/push-notification/push-notification-modal.tsx` - Modal UI +- `src/services/notification-sound.service.ts` - Sound playback +- `src/services/app-initialization.service.ts` - Service initialization + +## References + +- [Firebase Messaging iOS Documentation](https://firebase.google.com/docs/cloud-messaging/ios/receive) +- [React Native Firebase Messaging](https://rnfirebase.io/messaging/usage) +- [Apple UNUserNotificationCenter Documentation](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter) +- [Handling Notifications in Foreground](https://developer.apple.com/documentation/usernotifications/unusernotificationcenterdelegate/1649518-usernotificationcenter) + + +## Root Cause + +The current `AppDelegate.swift` implements `UNUserNotificationCenterDelegate` methods that: +1. Display notifications natively using iOS's notification UI +2. Call completion handlers immediately +3. **Do NOT forward notifications to Firebase Messaging's JavaScript handlers** + +This means: +- iOS shows a banner notification (which is good for visibility) +- BUT the notification data never reaches `messaging().onMessage()` in JavaScript +- Therefore `handleRemoteMessage` never runs +- The in-app modal never appears + +## Solution + +Remove the custom delegate implementation and let Firebase Messaging handle everything. Firebase Messaging's iOS SDK automatically: +- Sets up `UNUserNotificationCenter.current().delegate` +- Forwards foreground notifications to JavaScript via `onMessage()` listener +- Handles notification taps via `onNotificationOpenedApp()` +- Works with Notifee for displaying custom notifications + +## Changes Required + +### 1. Update Plugin (Already Done ✅) + +The `plugins/withForegroundNotifications.js` has been updated to: +- Remove all `withAppDelegate` modifications +- Keep only the entitlements configuration +- Add documentation explaining why we don't customize the delegate + +### 2. Clean and Rebuild iOS + +Run the following commands to regenerate AppDelegate.swift without the custom delegate: + +```bash +# Remove the iOS folder to force regeneration +rm -rf ios + +# Regenerate native code with updated plugin +yarn prebuild:clean + +# Or for specific environment +yarn prebuild:production # or staging, internal, development + +# Reinstall pods +cd ios && pod install && cd .. + +# Rebuild the app +yarn ios +``` + +### 3. Verify AppDelegate.swift + +After rebuild, verify that `ios/ResgridUnit/AppDelegate.swift` should: + +**❌ Should NOT have:** +- `, UNUserNotificationCenterDelegate` in class declaration +- `UNUserNotificationCenter.current().delegate = self` +- `userNotificationCenter(_:willPresent:)` method +- `userNotificationCenter(_:didReceive:)` method + +**✅ Should have:** +```swift +import FirebaseCore + +public class AppDelegate: ExpoAppDelegate { + // ... other code ... + + public override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + // ... setup code ... + + FirebaseApp.configure() + // NO delegate assignment here - Firebase handles it + + // ... rest of method ... + } +} +``` + +## How Firebase Messaging Works + +Once the custom delegate is removed: + +1. **Initialization**: When `FirebaseApp.configure()` is called, Firebase Messaging automatically sets itself as the UNUserNotificationCenter delegate + +2. **Foreground Notifications**: When a notification arrives while app is in foreground: + - Firebase receives it first + - Forwards to JavaScript via `messaging().onMessage(handleRemoteMessage)` + - Your `handleRemoteMessage` function processes it + - Shows in-app modal via `showNotificationModal()` + - Optionally displays native notification via Notifee + +3. **Background/Quit Notifications**: + - Handled by `messaging().setBackgroundMessageHandler()` + - Notification taps handled by `messaging().onNotificationOpenedApp()` + +## Expected Behavior After Fix + +✅ Push notification arrives while app is in foreground +✅ `handleRemoteMessage` is called with notification data +✅ In-app modal appears with notification details +✅ Sound plays (via notification sound service) +✅ User can interact with modal (dismiss or view call) + +## Testing + +After rebuilding: + +1. **Test Foreground Notification**: + - Open the app + - Send a test push notification from backend + - Verify in-app modal appears + - Verify sound plays + +2. **Test Background Notification**: + - Put app in background + - Send a test push notification + - Tap the notification + - Verify app opens and modal appears + +3. **Test Killed State**: + - Force quit the app + - Send a test push notification + - Tap the notification + - Verify app launches and modal appears + +## Alternative: Keep Native Banner + In-App Modal + +If you want BOTH the native iOS banner AND the in-app modal, you need to forward notifications to Firebase: + +```swift +public func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void +) { + // Display native banner + if #available(iOS 14.0, *) { + completionHandler([.banner, .sound, .badge]) + } else { + completionHandler([.alert, .sound, .badge]) + } + + // IMPORTANT: Forward to Firebase Messaging + // This ensures the notification also reaches your onMessage() handler + if let messagingDelegate = Messaging.messaging().delegate as? FIRMessagingDelegate { + // Firebase will handle forwarding to JavaScript + } +} +``` + +However, the **recommended approach** is to: +- Let Firebase handle everything +- Use Notifee in JavaScript to display custom notifications if needed +- Keep the in-app modal for interactive notifications + +## Related Files + +- `plugins/withForegroundNotifications.js` - Plugin configuration +- `src/services/push-notification.ts` - Push notification service +- `src/stores/push-notification/store.ts` - Modal state management +- `src/components/push-notification/push-notification-modal.tsx` - Modal UI + +## References + +- [Firebase Messaging iOS Documentation](https://firebase.google.com/docs/cloud-messaging/ios/receive) +- [React Native Firebase Messaging](https://rnfirebase.io/messaging/usage) +- [Notifee Documentation](https://notifee.app/react-native/docs/overview) diff --git a/expo-env.d.ts b/expo-env.d.ts index bf3c1693..5411fdde 100644 --- a/expo-env.d.ts +++ b/expo-env.d.ts @@ -1,3 +1,3 @@ /// -// NOTE: This file should not be edited and should be in your git ignore +// NOTE: This file should not be edited and should be in your git ignore \ No newline at end of file diff --git a/plugins/withForegroundNotifications.js b/plugins/withForegroundNotifications.js index ae0e868d..871527ec 100644 --- a/plugins/withForegroundNotifications.js +++ b/plugins/withForegroundNotifications.js @@ -2,7 +2,14 @@ const { withAppDelegate, withEntitlementsPlist } = require('@expo/config-plugins /** * Adds UNUserNotificationCenterDelegate to AppDelegate to handle foreground notifications - * and adds necessary entitlements for push notifications + * and adds necessary entitlements for push notifications. + * + * IMPORTANT: We implement the delegate methods to display notifications in foreground, + * but we do NOT intercept the notification data. Firebase Messaging will still receive + * the notifications and forward them to the onMessage() listener in JavaScript. + * + * The key is that we're only implementing willPresent (to show the banner) and + * NOT preventing Firebase from doing its job of forwarding to JS. */ const withForegroundNotifications = (config) => { // Add push notification entitlements @@ -48,6 +55,7 @@ const withForegroundNotifications = (config) => { // @generated end @react-native-firebase/app-didFinishLaunchingWithOptions // Set the UNUserNotificationCenter delegate to handle foreground notifications + // This allows us to display notifications while Firebase Messaging also processes them UNUserNotificationCenter.current().delegate = self` ); } @@ -57,18 +65,27 @@ const withForegroundNotifications = (config) => { const linkingApiPattern = /(\s+)(\/\/ Linking API)/; const delegateMethod = ` - // Handle foreground notifications - tell iOS to show them + // MARK: - UNUserNotificationCenterDelegate + + // Handle foreground notifications - display them even when app is active + // This method runs BEFORE Firebase Messaging processes the notification + // Both this method AND Firebase's onMessage() will be called public func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void ) { - // Show notification with alert, sound, and badge even when app is in foreground + // Display notification banner, play sound, and update badge + // This shows the native iOS notification banner in foreground if #available(iOS 14.0, *) { completionHandler([.banner, .sound, .badge]) } else { completionHandler([.alert, .sound, .badge]) } + + // NOTE: We do NOT need to manually forward to Firebase here. + // Firebase Messaging automatically receives the notification and calls onMessage() + // This method just controls whether iOS displays the native notification UI } // Handle notification tap - when user taps on a notification @@ -77,10 +94,8 @@ const withForegroundNotifications = (config) => { didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void ) { - // Forward the notification response to React Native - // When using Notifee (v7+), it will handle notification taps automatically - // This method still needs to be implemented to receive the notification response - // The response will be handled by Notifee's onBackgroundEvent/onForegroundEvent + // Firebase Messaging will handle this via onNotificationOpenedApp() + // We just need to call the completion handler completionHandler() } diff --git a/src/services/app-initialization.service.ts b/src/services/app-initialization.service.ts index 94384644..2dd7c910 100644 --- a/src/services/app-initialization.service.ts +++ b/src/services/app-initialization.service.ts @@ -2,6 +2,7 @@ import { Platform } from 'react-native'; import { logger } from '../lib/logging'; import { callKeepService } from './callkeep.service.ios'; +import { notificationSoundService } from './notification-sound.service'; import { pushNotificationService } from './push-notification'; /** @@ -74,6 +75,9 @@ class AppInitializationService { // Initialize CallKeep for iOS background audio support await this._initializeCallKeep(); + // Initialize Notification Sound Service + await this._initializeNotificationSoundService(); + // Initialize Push Notification Service await this._initializePushNotifications(); @@ -115,6 +119,26 @@ class AppInitializationService { } } + /** + * Initialize Notification Sound Service + */ + private async _initializeNotificationSoundService(): Promise { + try { + await notificationSoundService.initialize(); + + logger.info({ + message: 'Notification Sound Service initialized successfully', + }); + } catch (error) { + logger.error({ + message: 'Failed to initialize Notification Sound Service', + context: { error }, + }); + // Don't throw here - sound service failure shouldn't prevent app startup + // but notification sounds may not play properly + } + } + /** * Initialize Push Notification Service */ diff --git a/src/stores/push-notification/__tests__/store.test.ts b/src/stores/push-notification/__tests__/store.test.ts index 5ab2f556..f2e2dd93 100644 --- a/src/stores/push-notification/__tests__/store.test.ts +++ b/src/stores/push-notification/__tests__/store.test.ts @@ -36,7 +36,7 @@ describe('usePushNotificationModalStore', () => { }); describe('showNotificationModal', () => { - it('should show modal with call notification', () => { + it('should show modal with call notification', async () => { const callData = { eventCode: 'C1234', title: 'Emergency Call', @@ -44,7 +44,7 @@ describe('usePushNotificationModalStore', () => { }; const store = usePushNotificationModalStore.getState(); - store.showNotificationModal(callData); + await store.showNotificationModal(callData); const state = usePushNotificationModalStore.getState(); expect(state.isOpen).toBe(true); @@ -58,7 +58,7 @@ describe('usePushNotificationModalStore', () => { }); }); - it('should show modal with message notification', () => { + it('should show modal with message notification', async () => { const messageData = { eventCode: 'M5678', title: 'New Message', @@ -66,7 +66,7 @@ describe('usePushNotificationModalStore', () => { }; const store = usePushNotificationModalStore.getState(); - store.showNotificationModal(messageData); + await store.showNotificationModal(messageData); const state = usePushNotificationModalStore.getState(); expect(state.isOpen).toBe(true); @@ -80,7 +80,7 @@ describe('usePushNotificationModalStore', () => { }); }); - it('should show modal with chat notification', () => { + it('should show modal with chat notification', async () => { const chatData = { eventCode: 'T9101', title: 'Chat Message', @@ -88,7 +88,7 @@ describe('usePushNotificationModalStore', () => { }; const store = usePushNotificationModalStore.getState(); - store.showNotificationModal(chatData); + await store.showNotificationModal(chatData); const state = usePushNotificationModalStore.getState(); expect(state.isOpen).toBe(true); @@ -102,7 +102,7 @@ describe('usePushNotificationModalStore', () => { }); }); - it('should show modal with group chat notification', () => { + it('should show modal with group chat notification', async () => { const groupChatData = { eventCode: 'G1121', title: 'Group Chat', @@ -110,7 +110,7 @@ describe('usePushNotificationModalStore', () => { }; const store = usePushNotificationModalStore.getState(); - store.showNotificationModal(groupChatData); + await store.showNotificationModal(groupChatData); const state = usePushNotificationModalStore.getState(); expect(state.isOpen).toBe(true); @@ -124,7 +124,7 @@ describe('usePushNotificationModalStore', () => { }); }); - it('should handle unknown notification type', () => { + it('should handle unknown notification type', async () => { const unknownData = { eventCode: 'X9999', title: 'Unknown', @@ -132,7 +132,7 @@ describe('usePushNotificationModalStore', () => { }; const store = usePushNotificationModalStore.getState(); - store.showNotificationModal(unknownData); + await store.showNotificationModal(unknownData); const state = usePushNotificationModalStore.getState(); expect(state.isOpen).toBe(true); @@ -146,7 +146,7 @@ describe('usePushNotificationModalStore', () => { }); }); - it('should handle notification without valid eventCode', () => { + it('should handle notification without valid eventCode', async () => { const dataWithInvalidEventCode = { eventCode: 'I', title: 'Invalid Event Code', @@ -154,7 +154,7 @@ describe('usePushNotificationModalStore', () => { }; const store = usePushNotificationModalStore.getState(); - store.showNotificationModal(dataWithInvalidEventCode); + await store.showNotificationModal(dataWithInvalidEventCode); const state = usePushNotificationModalStore.getState(); expect(state.isOpen).toBe(true); @@ -168,7 +168,7 @@ describe('usePushNotificationModalStore', () => { }); }); - it('should log info message when showing notification', () => { + it('should log info message when showing notification', async () => { const callData = { eventCode: 'C1234', title: 'Emergency Call', @@ -176,7 +176,7 @@ describe('usePushNotificationModalStore', () => { }; const store = usePushNotificationModalStore.getState(); - store.showNotificationModal(callData); + await store.showNotificationModal(callData); expect(logger.info).toHaveBeenCalledWith({ message: 'Showing push notification modal', @@ -188,7 +188,7 @@ describe('usePushNotificationModalStore', () => { }); }); - it('should play notification sound when showing modal', () => { + it('should play notification sound when showing modal', async () => { const callData = { eventCode: 'C1234', title: 'Emergency Call', @@ -196,7 +196,7 @@ describe('usePushNotificationModalStore', () => { }; const store = usePushNotificationModalStore.getState(); - store.showNotificationModal(callData); + await store.showNotificationModal(callData); expect(notificationSoundService.playNotificationSound).toHaveBeenCalledWith('call'); }); @@ -212,19 +212,22 @@ describe('usePushNotificationModalStore', () => { }; const store = usePushNotificationModalStore.getState(); - store.showNotificationModal(callData); - - // Wait for async error handling - await new Promise((resolve) => setTimeout(resolve, 0)); + await store.showNotificationModal(callData); // Modal should still be shown even if sound fails const state = usePushNotificationModalStore.getState(); expect(state.isOpen).toBe(true); + + // Verify error was logged + expect(logger.error).toHaveBeenCalledWith({ + message: 'Failed to play notification sound', + context: { error: mockError, type: 'call' }, + }); }); }); describe('hideNotificationModal', () => { - it('should hide modal and clear notification', () => { + it('should hide modal and clear notification', async () => { // First show a notification const callData = { eventCode: 'C1234', @@ -233,7 +236,7 @@ describe('usePushNotificationModalStore', () => { }; const store = usePushNotificationModalStore.getState(); - store.showNotificationModal(callData); + await store.showNotificationModal(callData); // Verify it's shown let state = usePushNotificationModalStore.getState(); diff --git a/src/stores/push-notification/store.ts b/src/stores/push-notification/store.ts index a4bc6fde..50dd9a73 100644 --- a/src/stores/push-notification/store.ts +++ b/src/stores/push-notification/store.ts @@ -24,7 +24,7 @@ export interface ParsedNotification { interface PushNotificationModalState { isOpen: boolean; notification: ParsedNotification | null; - showNotificationModal: (notificationData: PushNotificationData) => void; + showNotificationModal: (notificationData: PushNotificationData) => Promise; hideNotificationModal: () => void; parseNotification: (notificationData: PushNotificationData) => ParsedNotification; } @@ -65,7 +65,7 @@ export const usePushNotificationModalStore = create( }; }, - showNotificationModal: (notificationData: PushNotificationData) => { + showNotificationModal: async (notificationData: PushNotificationData) => { const parsedNotification = get().parseNotification(notificationData); logger.info({ @@ -77,13 +77,16 @@ export const usePushNotificationModalStore = create( }, }); - // Play the appropriate sound for this notification type - notificationSoundService.playNotificationSound(parsedNotification.type).catch((error) => { + // Play the appropriate sound for this notification type and await it + // This ensures the sound starts playing before the modal appears + try { + await notificationSoundService.playNotificationSound(parsedNotification.type); + } catch (error) { logger.error({ message: 'Failed to play notification sound', context: { error, type: parsedNotification.type }, }); - }); + } set({ isOpen: true, From 52d3f7a97c121d52e0ab410d37c20037d22dfb85 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Tue, 11 Nov 2025 17:06:56 -0800 Subject: [PATCH 2/2] RU-T39 PR#187 fixes. --- src/services/__tests__/push-notification.test.ts | 8 ++++++-- src/services/push-notification.ts | 14 +++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/services/__tests__/push-notification.test.ts b/src/services/__tests__/push-notification.test.ts index de734a8a..4b7baa45 100644 --- a/src/services/__tests__/push-notification.test.ts +++ b/src/services/__tests__/push-notification.test.ts @@ -120,7 +120,7 @@ jest.mock('@notifee/react-native', () => ({ })); describe('Push Notification Service Integration', () => { - const mockShowNotificationModal = jest.fn(); + const mockShowNotificationModal = jest.fn().mockResolvedValue(undefined); const mockGetState = usePushNotificationModalStore.getState as jest.Mock; beforeAll(() => { @@ -131,6 +131,7 @@ describe('Push Notification Service Integration', () => { beforeEach(() => { jest.clearAllMocks(); + mockShowNotificationModal.mockResolvedValue(undefined); mockGetState.mockReturnValue({ showNotificationModal: mockShowNotificationModal, }); @@ -161,7 +162,10 @@ describe('Push Notification Service Integration', () => { }; // Show the notification modal using the store - usePushNotificationModalStore.getState().showNotificationModal(notificationData); + usePushNotificationModalStore.getState().showNotificationModal(notificationData).catch((err) => { + // Handle error in test environment + console.error('Error showing notification modal:', err); + }); } }; diff --git a/src/services/push-notification.ts b/src/services/push-notification.ts index 51590363..71a84923 100644 --- a/src/services/push-notification.ts +++ b/src/services/push-notification.ts @@ -166,7 +166,7 @@ class PushNotificationService { }; // Show the notification modal using the store - usePushNotificationModalStore.getState().showNotificationModal(notificationData); + await usePushNotificationModalStore.getState().showNotificationModal(notificationData); } }; @@ -201,7 +201,7 @@ class PushNotificationService { context: { eventCode, title }, }); - usePushNotificationModalStore.getState().showNotificationModal(notificationData); + await usePushNotificationModalStore.getState().showNotificationModal(notificationData); } } }); @@ -231,7 +231,7 @@ class PushNotificationService { context: { eventCode, title }, }); - usePushNotificationModalStore.getState().showNotificationModal(notificationData); + await usePushNotificationModalStore.getState().showNotificationModal(notificationData); } } }); @@ -294,7 +294,7 @@ class PushNotificationService { // Handle notification tap // Use a small delay to ensure the app is fully initialized and the store is ready - setTimeout(() => { + setTimeout(async () => { if (eventCode && typeof eventCode === 'string') { const notificationData = { eventCode: eventCode, @@ -309,7 +309,7 @@ class PushNotificationService { }); // Show the notification modal using the store - usePushNotificationModalStore.getState().showNotificationModal(notificationData); + await usePushNotificationModalStore.getState().showNotificationModal(notificationData); } }, 300); }); @@ -336,7 +336,7 @@ class PushNotificationService { // Handle the initial notification // Use a delay to ensure the app is fully loaded and the store is ready - setTimeout(() => { + setTimeout(async () => { if (eventCode && typeof eventCode === 'string') { const notificationData = { eventCode: eventCode, @@ -351,7 +351,7 @@ class PushNotificationService { }); // Show the notification modal using the store - usePushNotificationModalStore.getState().showNotificationModal(notificationData); + await usePushNotificationModalStore.getState().showNotificationModal(notificationData); } }, 500); }