From 70560f2db251a372ecea6073583857564fea330a Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Mon, 27 Jul 2026 13:48:45 -0700 Subject: [PATCH] Fix main-thread watchdog hang in PushNotificationIOS.setApplicationIconBadgeNumber (#57707) Summary: Adopts the non-blocking `-[UNUserNotificationCenter setBadgeCount:withCompletionHandler:]` API (iOS 16+) to fix a chronic main-thread hang in `PushNotificationIOS.setApplicationIconBadgeNumber`. The deprecated `applicationIconBadgeNumber` setter performs a synchronous XPC round-trip to `usernotificationsd` on the calling thread, which triggers `0x8badf00d` watchdog kills when the daemon is slow. The async API returns immediately and invokes a completion handler later, keeping the main thread responsive. Also adds an RNTester example screen with a "Rapid-fire 50x set" stress test to validate the fix. API Documentation say: ``` property(nonatomic) NSInteger applicationIconBadgeNumber API_DEPRECATED("Use -[UNUserNotificationCenter setBadgeCount:withCompletionHandler:] instead.", ios(2.0, 17.0)) API_UNAVAILABLE(watchos); ``` Changelog: [iOS][Fixed] Fix main-thread watchdog hang in PushNotificationIOS.setApplicationIconBadgeNumber Reviewed By: fkgozali Differential Revision: D113810036 --- .../RCTPushNotificationManager.mm | 13 ++- .../PushNotificationIOSExample.js | 98 +++++++++++++++++++ .../rn-tester/js/utils/RNTesterList.ios.js | 5 + 3 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 packages/rn-tester/js/examples/PushNotificationIOS/PushNotificationIOSExample.js diff --git a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm index 8b3be6bccc33..d5c6bc7270a0 100644 --- a/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm +++ b/packages/react-native/Libraries/PushNotificationIOS/RCTPushNotificationManager.mm @@ -322,7 +322,18 @@ - (void)handleRemoteNotificationRegistrationError:(NSNotification *)notification */ RCT_EXPORT_METHOD(setApplicationIconBadgeNumber : (double)number) { - RCTSharedApplication().applicationIconBadgeNumber = number; + NSInteger badgeCount = (NSInteger)lround(number); + if (@available(iOS 16.0, *)) { + [UNUserNotificationCenter.currentNotificationCenter + setBadgeCount:badgeCount + withCompletionHandler:^(NSError *_Nullable error) { + if (error != nil) { + RCTLogWarn(@"Failed to set application icon badge number: %@", error); + } + }]; + } else { + RCTSharedApplication().applicationIconBadgeNumber = badgeCount; + } } /** diff --git a/packages/rn-tester/js/examples/PushNotificationIOS/PushNotificationIOSExample.js b/packages/rn-tester/js/examples/PushNotificationIOS/PushNotificationIOSExample.js new file mode 100644 index 000000000000..60d0565ea099 --- /dev/null +++ b/packages/rn-tester/js/examples/PushNotificationIOS/PushNotificationIOSExample.js @@ -0,0 +1,98 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import type {RNTesterModuleExample} from '../../types/RNTesterTypes'; + +import RNTesterText from '../../components/RNTesterText'; +import * as React from 'react'; +import {useCallback, useState} from 'react'; +import {Button, StyleSheet, View} from 'react-native'; +import PushNotificationIOS from 'react-native/Libraries/PushNotificationIOS/PushNotificationIOS'; + +// Fires setApplicationIconBadgeNumber many times back-to-back. +const RAPID_FIRE_COUNT = 50; + +function BadgeExample(): React.Node { + const [reported, setReported] = useState(null); + + const updateBadge = useCallback((n: number) => { + PushNotificationIOS.setApplicationIconBadgeNumber(n); + }, []); + + const readBadge = useCallback(() => { + PushNotificationIOS.getApplicationIconBadgeNumber((n: number) => { + setReported(n); + }); + }, []); + + const rapidFire = useCallback(() => { + for (let i = 1; i <= RAPID_FIRE_COUNT; i++) { + PushNotificationIOS.setApplicationIconBadgeNumber(i); + } + }, []); + + return ( + + + {reported == null + ? 'Tap "Read current badge" to query the badge number.' + : `Last read badge number: ${reported}`} + + +