| author | |
| committer | |
| log | 4c61e3d7ecc4d9957c243677d35dd3313a37c067 |
| tree | e431abc0b6ace60f1769c42ad0f2827780c31488 |
| parent | dcdf5e6a013607b42836a270165286181da48296 |
| signature | Signed by PGP key B5690EEEBB952194 |
6 files changed, 379 insertions(+), 4 deletions(-)
apps/mobile/package.json+1-1| ... | ... | @@ -96,7 +96,7 @@ |
| 96 | 96 | "expo-keep-awake": "~57.0.2", |
| 97 | 97 | "expo-linking": "~57.0.8", |
| 98 | 98 | "expo-network": "~57.0.1", |
| 99 | "expo-notifications": "~57.0.15", | |
| 99 | "expo-notifications": "57.0.15", | |
| 100 | 100 | "expo-paste-input": "^0.1.15", |
| 101 | 101 | "expo-quick-actions": "^6.0.2", |
| 102 | 102 | "expo-secure-store": "~57.0.2", |
apps/mobile/scripts/fixtures/NotificationCenterManagerRegression.swift created+189| ... | ... | @@ -0,0 +1,189 @@ |
| 1 | import Foundation | |
| 2 | ||
| 3 | // The registry runs on macOS without starting a simulator. Only the OS-facing | |
| 4 | // types are replaced; the tests compile the dependency's actual Swift source. | |
| 5 | public enum UIBackgroundFetchResult { | |
| 6 | case noData | |
| 7 | } | |
| 8 | ||
| 9 | public struct UNNotificationPresentationOptions: OptionSet { | |
| 10 | public let rawValue: Int | |
| 11 | public init(rawValue: Int) { self.rawValue = rawValue } | |
| 12 | } | |
| 13 | ||
| 14 | public final class UNNotification: NSObject {} | |
| 15 | ||
| 16 | public final class UNNotificationResponse: NSObject { | |
| 17 | let identifier: String | |
| 18 | init(_ identifier: String) { self.identifier = identifier } | |
| 19 | } | |
| 20 | ||
| 21 | public protocol UNUserNotificationCenterDelegate: AnyObject {} | |
| 22 | ||
| 23 | public final class UNUserNotificationCenter: NSObject { | |
| 24 | private static let instance = UNUserNotificationCenter() | |
| 25 | public weak var delegate: UNUserNotificationCenterDelegate? | |
| 26 | public static func current() -> UNUserNotificationCenter { instance } | |
| 27 | } | |
| 28 | ||
| 29 | private final class TestDelegate: NotificationDelegate { | |
| 30 | private let lock = NSLock() | |
| 31 | private var recordedEvents: [String] = [] | |
| 32 | var onEvent: ((String) -> Void)? | |
| 33 | var onResponse: ((UNNotificationResponse) -> Bool)? | |
| 34 | ||
| 35 | var events: [String] { lock.withLock { recordedEvents } } | |
| 36 | ||
| 37 | private func record(_ event: String) { | |
| 38 | lock.withLock { recordedEvents.append(event) } | |
| 39 | onEvent?(event) | |
| 40 | } | |
| 41 | ||
| 42 | func didRegister(_ deviceToken: String) { record("registered") } | |
| 43 | func didFailRegistration(_ error: Error) { record("failed") } | |
| 44 | func openSettings(_ notification: UNNotification?) { record("settings") } | |
| 45 | ||
| 46 | func willPresent(_ notification: UNNotification, completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) -> Bool { | |
| 47 | record("present") | |
| 48 | return false | |
| 49 | } | |
| 50 | ||
| 51 | func didReceive(_ userInfo: [AnyHashable: Any], completionHandler: @escaping (UIBackgroundFetchResult) -> Void) -> Bool { | |
| 52 | record("background") | |
| 53 | return false | |
| 54 | } | |
| 55 | ||
| 56 | func didReceive(_ response: UNNotificationResponse, completionHandler: @escaping () -> Void) -> Bool { | |
| 57 | record(response.identifier) | |
| 58 | return onResponse?(response) ?? false | |
| 59 | } | |
| 60 | } | |
| 61 | ||
| 62 | private func require(_ condition: @autoclosure () -> Bool, _ message: String) throws { | |
| 63 | if !condition() { | |
| 64 | throw NSError(domain: "NotificationCenterManagerRegression", code: 1, userInfo: [NSLocalizedDescriptionKey: message]) | |
| 65 | } | |
| 66 | } | |
| 67 | ||
| 68 | private func deliver(_ identifier: String) { | |
| 69 | NotificationCenterManager.shared.userNotificationCenter( | |
| 70 | UNUserNotificationCenter.current(), | |
| 71 | didReceive: UNNotificationResponse(identifier), | |
| 72 | withCompletionHandler: {} | |
| 73 | ) | |
| 74 | } | |
| 75 | ||
| 76 | private func reentrantCallbacks() throws { | |
| 77 | let manager = NotificationCenterManager.shared | |
| 78 | let center = UNUserNotificationCenter.current() | |
| 79 | let callbacks: [(String, () -> Void)] = [ | |
| 80 | ("registered", { manager.didRegister("token") }), | |
| 81 | ("failed", { manager.didFailRegistration(NSError(domain: "test", code: 1)) }), | |
| 82 | ("settings", { manager.userNotificationCenter(center, openSettingsFor: nil) }), | |
| 83 | ("present", { manager.userNotificationCenter(center, willPresent: UNNotification(), withCompletionHandler: { _ in }) }), | |
| 84 | ("background", { manager.didReceive([:], completionHandler: { _ in }) }) | |
| 85 | ] | |
| 86 | ||
| 87 | for (event, callback) in callbacks { | |
| 88 | let original = TestDelegate() | |
| 89 | let replacement = TestDelegate() | |
| 90 | original.onEvent = { [weak original] _ in | |
| 91 | guard let original else { return } | |
| 92 | manager.removeDelegate(original) | |
| 93 | manager.addDelegate(replacement) | |
| 94 | } | |
| 95 | manager.addDelegate(original) | |
| 96 | callback() | |
| 97 | try require(original.events == [event], "The original delegate must receive the callback once") | |
| 98 | try require(replacement.events.isEmpty, "New delegates must not join an in-flight callback snapshot") | |
| 99 | callback() | |
| 100 | try require(replacement.events == [event], "Reentrant registration must take effect for the next callback") | |
| 101 | manager.removeDelegate(replacement) | |
| 102 | } | |
| 103 | } | |
| 104 | ||
| 105 | private func registrationDuringDelivery() throws { | |
| 106 | let manager = NotificationCenterManager.shared | |
| 107 | let original = TestDelegate() | |
| 108 | let receiver = TestDelegate() | |
| 109 | receiver.onResponse = { _ in true } | |
| 110 | original.onResponse = { _ in | |
| 111 | manager.addDelegate(receiver) | |
| 112 | return false | |
| 113 | } | |
| 114 | manager.addDelegate(original) | |
| 115 | deliver("handoff") | |
| 116 | try require(receiver.events == ["handoff"], "A delegate registering during delivery must not miss the pending response") | |
| 117 | manager.removeDelegate(original) | |
| 118 | manager.removeDelegate(receiver) | |
| 119 | } | |
| 120 | ||
| 121 | private func responseDuringReplay() throws { | |
| 122 | let manager = NotificationCenterManager.shared | |
| 123 | deliver("first") | |
| 124 | let original = TestDelegate() | |
| 125 | original.onResponse = { response in | |
| 126 | if response.identifier == "first" { | |
| 127 | deliver("second") | |
| 128 | return true | |
| 129 | } | |
| 130 | return false | |
| 131 | } | |
| 132 | manager.addDelegate(original) | |
| 133 | let receiver = TestDelegate() | |
| 134 | receiver.onResponse = { _ in true } | |
| 135 | manager.addDelegate(receiver) | |
| 136 | try require(receiver.events == ["second"], "Finishing a replay must retain responses received during callbacks") | |
| 137 | manager.removeDelegate(original) | |
| 138 | manager.removeDelegate(receiver) | |
| 139 | } | |
| 140 | ||
| 141 | private func concurrentRegistrations() throws { | |
| 142 | let manager = NotificationCenterManager.shared | |
| 143 | let delegates = (0..<512).map { _ in | |
| 144 | let delegate = TestDelegate() | |
| 145 | delegate.onResponse = { _ in true } | |
| 146 | return delegate | |
| 147 | } | |
| 148 | DispatchQueue.concurrentPerform(iterations: delegates.count) { index in | |
| 149 | manager.addDelegate(delegates[index]) | |
| 150 | if index.isMultiple(of: 16) { | |
| 151 | manager.didRegister("during-add") | |
| 152 | deliver("during-add") | |
| 153 | } | |
| 154 | } | |
| 155 | let before = delegates.map { $0.events.count } | |
| 156 | manager.didRegister("after-add") | |
| 157 | for (index, delegate) in delegates.enumerated() { | |
| 158 | try require(delegate.events.count == before[index] + 1, "Concurrent registration must retain every delegate exactly once") | |
| 159 | } | |
| 160 | DispatchQueue.concurrentPerform(iterations: delegates.count) { index in | |
| 161 | manager.removeDelegate(delegates[index]) | |
| 162 | if index.isMultiple(of: 16) { | |
| 163 | manager.didRegister("during-remove") | |
| 164 | deliver("during-remove") | |
| 165 | } | |
| 166 | } | |
| 167 | let removed = delegates.map { $0.events.count } | |
| 168 | manager.didRegister("after-remove") | |
| 169 | try require(delegates.map { $0.events.count } == removed, "Removed delegates must not receive new callbacks") | |
| 170 | } | |
| 171 | ||
| 172 | @main | |
| 173 | private enum RegressionTests { | |
| 174 | static func main() { | |
| 175 | do { | |
| 176 | switch CommandLine.arguments.last { | |
| 177 | case "reentrant": try reentrantCallbacks() | |
| 178 | case "handoff": try registrationDuringDelivery() | |
| 179 | case "pending": try responseDuringReplay() | |
| 180 | case "concurrent": try concurrentRegistrations() | |
| 181 | default: throw NSError(domain: "NotificationCenterManagerRegression", code: 2) | |
| 182 | } | |
| 183 | print("passed") | |
| 184 | } catch { | |
| 185 | FileHandle.standardError.write(Data("\(error.localizedDescription)\n".utf8)) | |
| 186 | exit(1) | |
| 187 | } | |
| 188 | } | |
| 189 | } |
apps/mobile/scripts/notification-center-manager.test.ts created+65| ... | ... | @@ -0,0 +1,65 @@ |
| 1 | // @effect-diagnostics nodeBuiltinImport:off - Compiles and runs the native dependency regression directly. | |
| 2 | import * as NodeChildProcess from "node:child_process"; | |
| 3 | import * as NodeFS from "node:fs"; | |
| 4 | import * as NodeOS from "node:os"; | |
| 5 | import * as NodePath from "node:path"; | |
| 6 | import * as NodeURL from "node:url"; | |
| 7 | import { afterAll, beforeAll, describe, expect, it } from "vite-plus/test"; | |
| 8 | ||
| 9 | // oxlint-disable-next-line t3code/no-global-process-runtime -- This native integration test uses the host Swift compiler. | |
| 10 | describe.skipIf(NodeOS.platform() !== "darwin")( | |
| 11 | "NotificationCenterManager native concurrency", | |
| 12 | () => { | |
| 13 | let directory: string; | |
| 14 | let executable: string; | |
| 15 | ||
| 16 | beforeAll(() => { | |
| 17 | directory = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-notifications-test-")); | |
| 18 | executable = NodePath.join(directory, "notification-regression"); | |
| 19 | const source = NodeFS.readFileSync( | |
| 20 | new URL( | |
| 21 | "../node_modules/expo-notifications/ios/ExpoNotifications/Notifications/NotificationCenterManager.swift", | |
| 22 | import.meta.url, | |
| 23 | ), | |
| 24 | "utf8", | |
| 25 | ); | |
| 26 | const manager = NodePath.join(directory, "NotificationCenterManager.swift"); | |
| 27 | NodeFS.writeFileSync( | |
| 28 | manager, | |
| 29 | source.replace(/^import (ExpoModulesCore|UserNotifications)\n/gm, ""), | |
| 30 | ); | |
| 31 | NodeChildProcess.execFileSync( | |
| 32 | "swiftc", | |
| 33 | [ | |
| 34 | "-swift-version", | |
| 35 | "5", | |
| 36 | "-sanitize=thread", | |
| 37 | manager, | |
| 38 | NodeURL.fileURLToPath( | |
| 39 | new URL("./fixtures/NotificationCenterManagerRegression.swift", import.meta.url), | |
| 40 | ), | |
| 41 | "-o", | |
| 42 | executable, | |
| 43 | ], | |
| 44 | { timeout: 30_000, encoding: "utf8" }, | |
| 45 | ); | |
| 46 | }); | |
| 47 | ||
| 48 | afterAll(() => { | |
| 49 | if (directory) NodeFS.rmSync(directory, { recursive: true, force: true }); | |
| 50 | }); | |
| 51 | ||
| 52 | it.each([ | |
| 53 | ["reentrant", "allows callbacks to replace delegates without deadlocking"], | |
| 54 | ["handoff", "delivers responses to delegates registering during delivery"], | |
| 55 | ["pending", "retains new responses received while replaying pending responses"], | |
| 56 | ["concurrent", "registers, removes, and broadcasts concurrently without data races"], | |
| 57 | ])("%s: %s", (name) => { | |
| 58 | const output = NodeChildProcess.execFileSync(executable, [name], { | |
| 59 | encoding: "utf8", | |
| 60 | timeout: 15_000, | |
| 61 | }); | |
| 62 | expect(output.trim()).toBe("passed"); | |
| 63 | }); | |
| 64 | }, | |
| 65 | ); |
patches/expo-notifications@57.0.15.patch created+119| ... | ... | @@ -0,0 +1,119 @@ |
| 1 | diff --git a/ios/ExpoNotifications/Notifications/NotificationCenterManager.swift b/ios/ExpoNotifications/Notifications/NotificationCenterManager.swift | |
| 2 | --- a/ios/ExpoNotifications/Notifications/NotificationCenterManager.swift | |
| 3 | +++ b/ios/ExpoNotifications/Notifications/NotificationCenterManager.swift | |
| 4 | @@ -42,8 +42,10 @@ | |
| 5 | @objc | |
| 6 | public static let shared = NotificationCenterManager() | |
| 7 | ||
| 8 | - var delegates: [NotificationDelegate] = [] | |
| 9 | - var pendingResponses: [UNNotificationResponse] = [] | |
| 10 | + // Module creation and destruction can overlap across React runtimes. | |
| 11 | + private let stateLock = NSLock() | |
| 12 | + private var delegates: [NotificationDelegate] = [] | |
| 13 | + private var pendingResponses: [UNNotificationResponse] = [] | |
| 14 | let userNotificationCenter: UNUserNotificationCenter = UNUserNotificationCenter.current() | |
| 15 | ||
| 16 | private override init() { | |
| 17 | @@ -62,32 +64,49 @@ | |
| 18 | } | |
| 19 | ||
| 20 | public func addDelegate(_ delegate: NotificationDelegate) { | |
| 21 | - delegates.append(delegate) | |
| 22 | + let responses = stateLock.withLock { | |
| 23 | + delegates.append(delegate) | |
| 24 | + return pendingResponses | |
| 25 | + } | |
| 26 | var handled = false | |
| 27 | - for pendingResponse in pendingResponses { | |
| 28 | + for pendingResponse in responses { | |
| 29 | handled = delegate.didReceive(pendingResponse, completionHandler: {}) || handled | |
| 30 | } | |
| 31 | if handled { | |
| 32 | - pendingResponses.removeAll() | |
| 33 | + removePendingResponses(responses) | |
| 34 | } | |
| 35 | } | |
| 36 | ||
| 37 | public func removeDelegate(_ delegate: AnyObject) { | |
| 38 | - if let index = delegates.firstIndex(where: { $0 === delegate }) { | |
| 39 | - delegates.remove(at: index) | |
| 40 | + stateLock.withLock { | |
| 41 | + if let index = delegates.firstIndex(where: { $0 === delegate }) { | |
| 42 | + delegates.remove(at: index) | |
| 43 | + } | |
| 44 | + } | |
| 45 | + } | |
| 46 | + | |
| 47 | + private func snapshotDelegates() -> [NotificationDelegate] { | |
| 48 | + stateLock.withLock { delegates } | |
| 49 | + } | |
| 50 | + | |
| 51 | + private func removePendingResponses(_ responses: [UNNotificationResponse]) { | |
| 52 | + let identifiers = Set(responses.map(ObjectIdentifier.init)) | |
| 53 | + stateLock.withLock { | |
| 54 | + // Keep responses received while delegate callbacks were running. | |
| 55 | + pendingResponses.removeAll { identifiers.contains(ObjectIdentifier($0)) } | |
| 56 | } | |
| 57 | } | |
| 58 | ||
| 59 | // MARK: - Called by PushTokenAppDelegateSubscriber | |
| 60 | ||
| 61 | public func didFailRegistration(_ error: any Error) { | |
| 62 | - for delegate in delegates { | |
| 63 | + for delegate in snapshotDelegates() { | |
| 64 | delegate.didFailRegistration(error) | |
| 65 | } | |
| 66 | } | |
| 67 | ||
| 68 | public func didRegister(_ deviceToken: String) { | |
| 69 | - for delegate in delegates { | |
| 70 | + for delegate in snapshotDelegates() { | |
| 71 | delegate.didRegister(deviceToken) | |
| 72 | } | |
| 73 | } | |
| 74 | @@ -100,7 +119,7 @@ | |
| 75 | withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void | |
| 76 | ) { | |
| 77 | var handled = false | |
| 78 | - for delegate in delegates { | |
| 79 | + for delegate in snapshotDelegates() { | |
| 80 | handled = delegate.willPresent(notification, completionHandler: completionHandler) || handled | |
| 81 | } | |
| 82 | if !handled { | |
| 83 | @@ -113,18 +132,23 @@ | |
| 84 | didReceive response: UNNotificationResponse, | |
| 85 | withCompletionHandler completionHandler: @escaping () -> Void | |
| 86 | ) { | |
| 87 | + let currentDelegates = stateLock.withLock { | |
| 88 | + // A delegate registering during delivery must see this response too. | |
| 89 | + pendingResponses.append(response) | |
| 90 | + return delegates | |
| 91 | + } | |
| 92 | var handled = false | |
| 93 | - for delegate in delegates { | |
| 94 | + for delegate in currentDelegates { | |
| 95 | handled = delegate.didReceive(response, completionHandler: completionHandler) || handled | |
| 96 | } | |
| 97 | - if !handled { | |
| 98 | - pendingResponses.append(response) | |
| 99 | + if handled { | |
| 100 | + removePendingResponses([response]) | |
| 101 | } | |
| 102 | completionHandler() | |
| 103 | } | |
| 104 | ||
| 105 | public func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) { | |
| 106 | - for delegate in delegates { | |
| 107 | + for delegate in snapshotDelegates() { | |
| 108 | delegate.openSettings(notification) | |
| 109 | } | |
| 110 | } | |
| 111 | @@ -132,7 +156,7 @@ | |
| 112 | // MARK: - Called from NotificationsAppDelegateSubscriber | |
| 113 | public func didReceive(_ userInfo: [AnyHashable: Any], completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { | |
| 114 | var handled = false | |
| 115 | - for delegate in delegates { | |
| 116 | + for delegate in snapshotDelegates() { | |
| 117 | handled = delegate.didReceive(userInfo, completionHandler: completionHandler) || handled | |
| 118 | } | |
| 119 | if !handled { |
pnpm-lock.yaml+4-3| ... | ... | @@ -103,6 +103,7 @@ patchedDependencies: |
| 103 | 103 | effect@4.0.0-rc.115: 0dfc4bb8ebd80fb3e06b91ef61346f5259517ab0f2437644fe95ae531084b1f5 |
| 104 | 104 | expo-audio@57.0.4: fa9a3e0442ed395d4071bb406e08c3a471c9a84700bdfa0b9ad7ff144c96041a |
| 105 | 105 | expo-modules-core@57.0.14: 6be7092f8c6f03a57b75c797d98b5f19ed62d2dd205bfc4c8adf4cef45655d7e |
| 106 | expo-notifications@57.0.15: f89252c2c08dc7b3acb8415f319ebbe15fff8c9f88a64ebe1830cd5ac5c9ac40 | |
| 106 | 107 | expo-sharing@57.0.17: 8d2e3b10eb3f52036a9a086800180ec6cebf3b75bccc5b1775117a7244d4ac45 |
| 107 | 108 | expo-widgets@57.0.15: 319a9ded5db49c5b5215c511a138b33f44c7ea2972eb418192e8d5342fe75ce6 |
| 108 | 109 | react-native-gesture-handler@2.32.0: 0579f8e4dad02bf3183d95b02620358412983c36f9bda7425dc8bcb9643b5ce2 |
| ... | ... | @@ -389,8 +390,8 @@ importers: |
| 389 | 390 | specifier: ~57.0.1 |
| 390 | 391 | version: 57.0.1(expo@57.0.18)(react@19.2.3) |
| 391 | 392 | expo-notifications: |
| 392 | specifier: ~57.0.15 | |
| 393 | version: 57.0.15(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@7.0.2) | |
| 393 | specifier: 57.0.15 | |
| 394 | version: 57.0.15(patch_hash=f89252c2c08dc7b3acb8415f319ebbe15fff8c9f88a64ebe1830cd5ac5c9ac40)(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@7.0.2) | |
| 394 | 395 | expo-paste-input: |
| 395 | 396 | specifier: ^0.1.15 |
| 396 | 397 | version: 0.1.15(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) |
| ... | ... | @@ -17449,7 +17450,7 @@ snapshots: |
| 17449 | 17450 | expo: 57.0.18(41fd11498a34454c91128cdad32f22f8) |
| 17450 | 17451 | react: 19.2.3 |
| 17451 | 17452 | |
| 17452 | expo-notifications@57.0.15(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@7.0.2): | |
| 17453 | expo-notifications@57.0.15(patch_hash=f89252c2c08dc7b3acb8415f319ebbe15fff8c9f88a64ebe1830cd5ac5c9ac40)(expo@57.0.18)(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@7.0.2): | |
| 17453 | 17454 | dependencies: |
| 17454 | 17455 | '@expo/image-utils': 0.11.5(typescript@7.0.2) |
| 17455 | 17456 | abort-controller: 3.0.0 |
pnpm-workspace.yaml+1| ... | ... | @@ -190,6 +190,7 @@ patchedDependencies: |
| 190 | 190 | effect@4.0.0-rc.115: patches/effect@4.0.0-rc.115.patch |
| 191 | 191 | expo-audio@57.0.4: patches/expo-audio@57.0.4.patch |
| 192 | 192 | expo-modules-core@57.0.14: patches/expo-modules-core@57.0.14.patch |
| 193 | expo-notifications@57.0.15: patches/expo-notifications@57.0.15.patch | |
| 193 | 194 | expo-sharing@57.0.17: patches/expo-sharing@57.0.17.patch |
| 194 | 195 | expo-widgets@57.0.15: patches/expo-widgets@57.0.15.patch |
| 195 | 196 | react-native-gesture-handler@2.32.0: patches/react-native-gesture-handler@2.32.0.patch |