From c524862c05002d1bc0479eb209598b756eb382ed Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Wed, 26 Aug 2026 11:11:53 -0700 Subject: [PATCH] fix(push-demo): return notification completion on main thread Signed-off-by: Tom Brow --- mobile/ios/Runner/PushNativeState.swift | 9 ++++++++- .../BuzzCommunicationNotificationTests.swift | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/mobile/ios/Runner/PushNativeState.swift b/mobile/ios/Runner/PushNativeState.swift index 6cb7609cb8..13a18961d3 100644 --- a/mobile/ios/Runner/PushNativeState.swift +++ b/mobile/ios/Runner/PushNativeState.swift @@ -16,7 +16,14 @@ final class BuzzOneShotCompletion { let completion = completion self.completion = nil lock.unlock() - completion?() + guard let completion else { return } + if Thread.isMainThread { + completion() + } else { + // Reply publication finishes on a URLSession callback queue, while the + // notification response completion crosses back into UIKit. + DispatchQueue.main.async(execute: completion) + } } } diff --git a/mobile/ios/RunnerTests/BuzzCommunicationNotificationTests.swift b/mobile/ios/RunnerTests/BuzzCommunicationNotificationTests.swift index 624b50e64b..9b2d28d5a3 100644 --- a/mobile/ios/RunnerTests/BuzzCommunicationNotificationTests.swift +++ b/mobile/ios/RunnerTests/BuzzCommunicationNotificationTests.swift @@ -217,6 +217,24 @@ final class BuzzPushSnapshotEnrichmentTests: XCTestCase { } final class BuzzPushNotificationResponseTests: XCTestCase { + func testBackgroundCompletionReturnsToMainThreadExactlyOnce() { + let completed = expectation(description: "completion returned on main thread") + var completionCount = 0 + let completion = BuzzOneShotCompletion { + XCTAssertTrue(Thread.isMainThread) + completionCount += 1 + completed.fulfill() + } + + DispatchQueue.global().async { + completion.call() + completion.call() + } + + wait(for: [completed], timeout: 1) + XCTAssertEqual(completionCount, 1) + } + func testValidReminderActionRoutesContextWithoutOpeningFlutter() { let context = BuzzPushReplyContext( eventID: "message-id",