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/__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/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/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);
}
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,