@dawidzawada/bonjour-zeroconf 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/BonjourZeroconf.podspec +30 -0
  2. package/LICENSE +20 -0
  3. package/README.md +35 -0
  4. package/android/CMakeLists.txt +24 -0
  5. package/android/build.gradle +128 -0
  6. package/android/gradle.properties +5 -0
  7. package/android/src/main/AndroidManifest.xml +2 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf+AddressResolver.kt +182 -0
  10. package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf+Listeners.kt +45 -0
  11. package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf.kt +183 -0
  12. package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconfPackage.kt +22 -0
  13. package/ios/AddressResolverError.swift +20 -0
  14. package/ios/BonjourZeroconf+AddressResolver.swift +133 -0
  15. package/ios/BonjourZeroconf+Listeners.swift +26 -0
  16. package/ios/BonjourZeroconf.swift +201 -0
  17. package/ios/LocalNetworkAuthorization.swift +66 -0
  18. package/ios/LocalNetworkPermission+Listeners.swift +14 -0
  19. package/ios/LocalNetworkPermission.swift +41 -0
  20. package/ios/ServiceCache.swift +30 -0
  21. package/ios/Utils/Loggy.swift +37 -0
  22. package/lib/module/index.js +9 -0
  23. package/lib/module/index.js.map +1 -0
  24. package/lib/module/package.json +1 -0
  25. package/lib/module/permissions.ios.js +23 -0
  26. package/lib/module/permissions.ios.js.map +1 -0
  27. package/lib/module/permissions.js +9 -0
  28. package/lib/module/permissions.js.map +1 -0
  29. package/lib/module/specs/BonjourFail.js +9 -0
  30. package/lib/module/specs/BonjourFail.js.map +1 -0
  31. package/lib/module/specs/BonjourListener.js +2 -0
  32. package/lib/module/specs/BonjourListener.js.map +1 -0
  33. package/lib/module/specs/BonjourZeroconf.nitro.js +4 -0
  34. package/lib/module/specs/BonjourZeroconf.nitro.js.map +1 -0
  35. package/lib/module/specs/LocalNetworkPermission.nitro.js +4 -0
  36. package/lib/module/specs/LocalNetworkPermission.nitro.js.map +1 -0
  37. package/lib/module/specs/ScanResult.js +2 -0
  38. package/lib/module/specs/ScanResult.js.map +1 -0
  39. package/lib/module/useIsScanning.js +19 -0
  40. package/lib/module/useIsScanning.js.map +1 -0
  41. package/lib/typescript/package.json +1 -0
  42. package/lib/typescript/src/index.d.ts +9 -0
  43. package/lib/typescript/src/index.d.ts.map +1 -0
  44. package/lib/typescript/src/permissions.d.ts +3 -0
  45. package/lib/typescript/src/permissions.d.ts.map +1 -0
  46. package/lib/typescript/src/permissions.ios.d.ts +3 -0
  47. package/lib/typescript/src/permissions.ios.d.ts.map +1 -0
  48. package/lib/typescript/src/specs/BonjourFail.d.ts +6 -0
  49. package/lib/typescript/src/specs/BonjourFail.d.ts.map +1 -0
  50. package/lib/typescript/src/specs/BonjourListener.d.ts +4 -0
  51. package/lib/typescript/src/specs/BonjourListener.d.ts.map +1 -0
  52. package/lib/typescript/src/specs/BonjourZeroconf.nitro.d.ts +19 -0
  53. package/lib/typescript/src/specs/BonjourZeroconf.nitro.d.ts.map +1 -0
  54. package/lib/typescript/src/specs/LocalNetworkPermission.nitro.d.ts +9 -0
  55. package/lib/typescript/src/specs/LocalNetworkPermission.nitro.d.ts.map +1 -0
  56. package/lib/typescript/src/specs/ScanResult.d.ts +8 -0
  57. package/lib/typescript/src/specs/ScanResult.d.ts.map +1 -0
  58. package/lib/typescript/src/useIsScanning.d.ts +2 -0
  59. package/lib/typescript/src/useIsScanning.d.ts.map +1 -0
  60. package/nitro.json +20 -0
  61. package/nitrogen/generated/android/c++/JBonjourFail.hpp +62 -0
  62. package/nitrogen/generated/android/c++/JBonjourListener.hpp +68 -0
  63. package/nitrogen/generated/android/c++/JFunc_void.hpp +74 -0
  64. package/nitrogen/generated/android/c++/JFunc_void_BonjourFail.hpp +76 -0
  65. package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +74 -0
  66. package/nitrogen/generated/android/c++/JFunc_void_std__vector_ScanResult_.hpp +97 -0
  67. package/nitrogen/generated/android/c++/JHybridBonjourZeroconfSpec.cpp +96 -0
  68. package/nitrogen/generated/android/c++/JHybridBonjourZeroconfSpec.hpp +69 -0
  69. package/nitrogen/generated/android/c++/JScanOptions.hpp +57 -0
  70. package/nitrogen/generated/android/c++/JScanResult.hpp +74 -0
  71. package/nitrogen/generated/android/dawidzawada_bonjourzeroconf+autolinking.cmake +82 -0
  72. package/nitrogen/generated/android/dawidzawada_bonjourzeroconf+autolinking.gradle +27 -0
  73. package/nitrogen/generated/android/dawidzawada_bonjourzeroconfOnLoad.cpp +52 -0
  74. package/nitrogen/generated/android/dawidzawada_bonjourzeroconfOnLoad.hpp +25 -0
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourFail.kt +22 -0
  76. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourListener.kt +42 -0
  77. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/Func_void.kt +80 -0
  78. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/Func_void_BonjourFail.kt +80 -0
  79. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/Func_void_bool.kt +80 -0
  80. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/Func_void_std__vector_ScanResult_.kt +80 -0
  81. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/HybridBonjourZeroconfSpec.kt +90 -0
  82. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/ScanOptions.kt +38 -0
  83. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/ScanResult.kt +50 -0
  84. package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/dawidzawada_bonjourzeroconfOnLoad.kt +35 -0
  85. package/nitrogen/generated/ios/BonjourZeroconf+autolinking.rb +60 -0
  86. package/nitrogen/generated/ios/BonjourZeroconf-Swift-Cxx-Bridge.cpp +90 -0
  87. package/nitrogen/generated/ios/BonjourZeroconf-Swift-Cxx-Bridge.hpp +282 -0
  88. package/nitrogen/generated/ios/BonjourZeroconf-Swift-Cxx-Umbrella.hpp +65 -0
  89. package/nitrogen/generated/ios/BonjourZeroconfAutolinking.mm +41 -0
  90. package/nitrogen/generated/ios/BonjourZeroconfAutolinking.swift +40 -0
  91. package/nitrogen/generated/ios/c++/HybridBonjourZeroconfSpecSwift.cpp +11 -0
  92. package/nitrogen/generated/ios/c++/HybridBonjourZeroconfSpecSwift.hpp +120 -0
  93. package/nitrogen/generated/ios/c++/HybridLocalNetworkPermissionSpecSwift.cpp +11 -0
  94. package/nitrogen/generated/ios/c++/HybridLocalNetworkPermissionSpecSwift.hpp +87 -0
  95. package/nitrogen/generated/ios/swift/BonjourFail.swift +44 -0
  96. package/nitrogen/generated/ios/swift/BonjourListener.swift +47 -0
  97. package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
  98. package/nitrogen/generated/ios/swift/Func_void_BonjourFail.swift +47 -0
  99. package/nitrogen/generated/ios/swift/Func_void_bool.swift +47 -0
  100. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  101. package/nitrogen/generated/ios/swift/Func_void_std__vector_ScanResult_.swift +47 -0
  102. package/nitrogen/generated/ios/swift/HybridBonjourZeroconfSpec.swift +60 -0
  103. package/nitrogen/generated/ios/swift/HybridBonjourZeroconfSpec_cxx.swift +203 -0
  104. package/nitrogen/generated/ios/swift/HybridLocalNetworkPermissionSpec.swift +57 -0
  105. package/nitrogen/generated/ios/swift/HybridLocalNetworkPermissionSpec_cxx.swift +155 -0
  106. package/nitrogen/generated/ios/swift/ScanOptions.swift +48 -0
  107. package/nitrogen/generated/ios/swift/ScanResult.swift +149 -0
  108. package/nitrogen/generated/shared/c++/BonjourFail.hpp +63 -0
  109. package/nitrogen/generated/shared/c++/BonjourListener.hpp +75 -0
  110. package/nitrogen/generated/shared/c++/HybridBonjourZeroconfSpec.cpp +26 -0
  111. package/nitrogen/generated/shared/c++/HybridBonjourZeroconfSpec.hpp +80 -0
  112. package/nitrogen/generated/shared/c++/HybridLocalNetworkPermissionSpec.cpp +22 -0
  113. package/nitrogen/generated/shared/c++/HybridLocalNetworkPermissionSpec.hpp +66 -0
  114. package/nitrogen/generated/shared/c++/ScanOptions.hpp +75 -0
  115. package/nitrogen/generated/shared/c++/ScanResult.hpp +92 -0
  116. package/package.json +169 -0
  117. package/src/index.ts +22 -0
  118. package/src/permissions.ios.ts +27 -0
  119. package/src/permissions.ts +7 -0
  120. package/src/specs/BonjourFail.ts +5 -0
  121. package/src/specs/BonjourListener.ts +3 -0
  122. package/src/specs/BonjourZeroconf.nitro.ts +20 -0
  123. package/src/specs/LocalNetworkPermission.nitro.ts +7 -0
  124. package/src/specs/ScanResult.ts +7 -0
  125. package/src/useIsScanning.ts +17 -0
@@ -0,0 +1,20 @@
1
+ //
2
+ // BonjourError.swift
3
+ // Pods
4
+ //
5
+ // Created by Dawid Zawada on 06/11/2025.
6
+ //
7
+
8
+ enum AddressResolverError: Error {
9
+ case timeout
10
+ case extractionFailed
11
+
12
+ var localizedDescription: String {
13
+ switch self {
14
+ case .timeout:
15
+ return "Connection timed out"
16
+ case .extractionFailed:
17
+ return "Failed to extract IP and port information"
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,133 @@
1
+ //
2
+ // BonjourZeroconf+AddressResolver.swift
3
+ // Pods
4
+ //
5
+ // Created by Dawid Zawada on 05/11/2025.
6
+ //
7
+ import Network
8
+
9
+ extension BonjourZeroconf {
10
+ /// Resolve a service to get its IP address and port using async/await
11
+ internal func resolveService(result: NWBrowser.Result, name: String, timeout: TimeInterval) async -> ScanResult? {
12
+ do {
13
+ return try await withCheckedThrowingContinuation { continuation in
14
+ let connection = NWConnection(to: result.endpoint, using: .tcp)
15
+
16
+ final class ResumeBox {
17
+ var hasResumed = false
18
+ }
19
+ let box = ResumeBox()
20
+
21
+ let timeoutTask = DispatchWorkItem {
22
+ guard !box.hasResumed else { return }
23
+ box.hasResumed = true
24
+ Loggy.log(.debug, message: "Timeout resolving \(name)")
25
+ continuation.resume(throwing: AddressResolverError.timeout)
26
+ connection.cancel()
27
+ }
28
+ networkQueue.asyncAfter(deadline: .now() + timeout, execute: timeoutTask)
29
+
30
+ connection.stateUpdateHandler = { [weak self] state in
31
+ switch state {
32
+ case .ready:
33
+ timeoutTask.cancel()
34
+ guard !box.hasResumed else { return }
35
+ box.hasResumed = true
36
+
37
+ if let remoteEndpoint = connection.currentPath?.remoteEndpoint,
38
+ let scanResult = self?.extractIPAndPort(from: remoteEndpoint, serviceName: name) {
39
+ continuation.resume(returning: scanResult)
40
+ } else {
41
+ continuation.resume(throwing: AddressResolverError.extractionFailed)
42
+ }
43
+ connection.cancel()
44
+
45
+ case .failed(let error):
46
+ timeoutTask.cancel()
47
+ guard !box.hasResumed else { return }
48
+ box.hasResumed = true
49
+ Loggy.log(.error, message: "Failed to resolve service \(name): \(error.localizedDescription)")
50
+ continuation.resume(throwing: error)
51
+ connection.cancel()
52
+
53
+ case .waiting(let error):
54
+ Loggy.log(.debug, message: "Connection waiting for \(name): \(error.localizedDescription)")
55
+
56
+ case .cancelled:
57
+ break
58
+
59
+ default:
60
+ break
61
+ }
62
+ }
63
+
64
+ connection.start(queue: networkQueue)
65
+ }
66
+ } catch let error as AddressResolverError {
67
+ switch error {
68
+ case .timeout:
69
+ notifyScanFailListeners(with: BonjourFail.resolveFailed)
70
+ break;
71
+ case .extractionFailed:
72
+ notifyScanFailListeners(with: BonjourFail.extractionFailed)
73
+ break;
74
+ }
75
+ return nil
76
+ } catch {
77
+ return nil
78
+ }
79
+ }
80
+
81
+ /// Extract IP address and port from an endpoint
82
+ internal func extractIPAndPort(from endpoint: NWEndpoint, serviceName: String) -> ScanResult? {
83
+ switch endpoint {
84
+ case .hostPort(let host, let port):
85
+ var ipv4: String?
86
+ var ipv6: String?
87
+ var hostname: String?
88
+ let portNumber = Int(port.rawValue)
89
+
90
+ switch host {
91
+ case .ipv4(let address):
92
+ ipv4 = address.rawValue.map(String.init).joined(separator: ".")
93
+ Loggy.log(.debug, message: "Resolved \(serviceName) -> IPv4: \(ipv4!), Port: \(portNumber)")
94
+
95
+ case .ipv6(let address):
96
+ let formatted = stride(from: 0, to: address.rawValue.count, by: 2).map { i in
97
+ String(format: "%02x%02x", address.rawValue[i], address.rawValue[i + 1])
98
+ }.joined(separator: ":")
99
+
100
+ if formatted.hasPrefix("fe80:") {
101
+ if let interface = endpoint.interface?.name {
102
+ ipv6 = "\(formatted)%\(interface)"
103
+ } else {
104
+ ipv6 = "\(formatted)%en0" // fallback
105
+ }
106
+ } else {
107
+ ipv6 = formatted
108
+ }
109
+ Loggy.log(.debug, message: "Resolved \(serviceName) -> IPv6: \(ipv6!), Port: \(portNumber)")
110
+
111
+ case .name(let name, _):
112
+ hostname = name
113
+ Loggy.log(.debug, message: "Resolved \(serviceName) -> Hostname: \(hostname ?? "nil"), Port: \(portNumber)")
114
+
115
+ @unknown default:
116
+ Loggy.log(.debug, message: "Unknown host type for \(serviceName)")
117
+ return nil
118
+ }
119
+
120
+ return ScanResult(
121
+ name: serviceName,
122
+ ipv4: ipv4,
123
+ ipv6: ipv6,
124
+ hostname: hostname,
125
+ port: Double(portNumber)
126
+ )
127
+
128
+ default:
129
+ Loggy.log(.warning, message: "Unexpected endpoint format for \(serviceName)")
130
+ return nil
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,26 @@
1
+ //
2
+ // BonjourZeroconf+Listeners.swift
3
+ // Pods
4
+ //
5
+ // Created by Dawid Zawada on 05/11/2025.
6
+ //
7
+
8
+ extension BonjourZeroconf {
9
+ internal func notifyScanResultsListeners(with results: [ScanResult]) {
10
+ for listener in scanResultsListeners.values {
11
+ listener(results)
12
+ }
13
+ }
14
+
15
+ internal func notifyScanStateListeners(with isScanningState: Bool) {
16
+ for listener in scanStateListeners.values {
17
+ listener(isScanningState)
18
+ }
19
+ }
20
+
21
+ internal func notifyScanFailListeners(with fail: BonjourFail) {
22
+ for listener in scanFailListeners.values {
23
+ listener(fail)
24
+ }
25
+ }
26
+ }
@@ -0,0 +1,201 @@
1
+ import NitroModules
2
+ import Foundation
3
+ import Network
4
+
5
+ class BonjourZeroconf: HybridBonjourZeroconfSpec {
6
+ private var _browser: NWBrowser?
7
+ private let serviceCache = ServiceCache()
8
+
9
+ private let DEFAULT_RESOLVE_TIMEOUT = 10.0
10
+
11
+ internal var scanResultsListeners: [UUID: ([ScanResult]) -> Void] = [:]
12
+ internal var scanStateListeners: [UUID: (Bool) -> Void] = [:]
13
+ internal var scanFailListeners: [UUID: (BonjourFail) -> Void] = [:]
14
+ internal let networkQueue = DispatchQueue(label: "com.bonjour-zeroconf.network", qos: .userInitiated)
15
+
16
+ var isScanning: Bool {
17
+ return _isScanning
18
+ }
19
+
20
+ internal var _isScanning = false {
21
+ didSet {
22
+ notifyScanStateListeners(with: _isScanning)
23
+ }
24
+ }
25
+
26
+
27
+ func scan(type: String, domain: String, options: ScanOptions?) {
28
+ if _isScanning {
29
+ Loggy.log(.warning, message: "Cannot start scanning, already scanning")
30
+ return
31
+ }
32
+
33
+ let resolveTimeout = options?.addressResolveTimeout ?? DEFAULT_RESOLVE_TIMEOUT
34
+
35
+ let browser = NWBrowser(for: .bonjour(type: type, domain: domain), using: .tcp)
36
+ self._browser = browser
37
+ self._isScanning = true
38
+
39
+ browser.stateUpdateHandler = { [weak self] state in
40
+ guard let self = self else { return }
41
+
42
+ switch state {
43
+ case .failed(let error):
44
+ notifyScanFailListeners(with: BonjourFail.discoveryFailed)
45
+ Loggy.log(.error, message: "Browser failed, reason: \(error.localizedDescription)")
46
+ browser.cancel()
47
+ self._isScanning = false
48
+
49
+ case .ready:
50
+ Loggy.log(.info, message: "Browser ready")
51
+
52
+ case .cancelled:
53
+ Loggy.log(.info, message: "Browser cancelled")
54
+ self._isScanning = false
55
+
56
+ default:
57
+ break
58
+ }
59
+ }
60
+
61
+ browser.browseResultsChangedHandler = { [weak self] results, changes in
62
+ guard let self = self else { return }
63
+
64
+ Loggy.log(.info, message: "Found \(results.count) service(s)")
65
+
66
+ Task {
67
+ await self.processChanges(changes, until: resolveTimeout)
68
+ }
69
+ }
70
+
71
+ Loggy.log(.info, message: "Starting browser for service type: \(type)")
72
+ browser.start(queue: networkQueue)
73
+ }
74
+
75
+ func listenForScanResults(onResult: @escaping ([ScanResult]) -> Void) -> BonjourListener {
76
+ let listenerId = UUID()
77
+ self.scanResultsListeners[listenerId] = onResult
78
+
79
+ Task {
80
+ let cachedResults = await serviceCache.getAll()
81
+ onResult(cachedResults)
82
+ }
83
+
84
+ return BonjourListener { [weak self] in
85
+ self?.scanResultsListeners.removeValue(forKey: listenerId)
86
+ }
87
+ }
88
+
89
+ func listenForScanState(onChange: @escaping (Bool) -> Void) -> BonjourListener {
90
+ let listenerId = UUID()
91
+ self.scanStateListeners[listenerId] = onChange
92
+
93
+ onChange(_isScanning)
94
+
95
+ return BonjourListener { [weak self] in
96
+ self?.scanStateListeners.removeValue(forKey: listenerId)
97
+ }
98
+ }
99
+
100
+ func listenForScanFail(onFail: @escaping (BonjourFail) -> Void) -> BonjourListener {
101
+ let listenerId = UUID()
102
+ self.scanFailListeners[listenerId] = onFail
103
+
104
+ return BonjourListener { [weak self] in
105
+ self?.scanFailListeners.removeValue(forKey: listenerId)
106
+ }
107
+ }
108
+
109
+
110
+ func stop() {
111
+ if let browser = self._browser {
112
+ browser.cancel()
113
+ }
114
+
115
+ self._isScanning = false
116
+ Task {
117
+ await serviceCache.clear()
118
+ }
119
+ Loggy.log(.info, message: "Stopped scanning")
120
+ }
121
+
122
+ /// Process all discovered services and resolve their IP addresses
123
+ private func processChanges(_ changes: Set<NWBrowser.Result.Change>, until resolveTimeout: Double) async {
124
+ var hasChanges = false
125
+
126
+ var servicesToResolve: [(key: String, result: NWBrowser.Result, name: String)] = []
127
+
128
+ for change in changes {
129
+ switch change {
130
+ case .added(let result):
131
+ guard let key = serviceKey(for: result) else { continue }
132
+
133
+ if await serviceCache.get(key) == nil {
134
+ guard case .service(let name, _, _, _) = result.endpoint else { continue }
135
+ Loggy.log(.info, message: "New service detected: \(name)")
136
+ servicesToResolve.append((key: key, result: result, name: name))
137
+ hasChanges = true
138
+ } else {
139
+ Loggy.log(.debug, message: "Service already cached: \(key)")
140
+ }
141
+
142
+ case .removed(let result):
143
+ guard let key = serviceKey(for: result) else { continue }
144
+
145
+ if await serviceCache.remove(key) != nil {
146
+ Loggy.log(.debug, message: "Service removed")
147
+ hasChanges = true
148
+ }
149
+
150
+ case .changed(let _old, let new, let _flags):
151
+ guard let key = serviceKey(for: new) else { continue }
152
+ guard case .service(let name, _, _, _) = new.endpoint else { continue }
153
+
154
+ Loggy.log(.debug, message: "Service changed: \(name)")
155
+ servicesToResolve.append((key: key, result: new, name: name))
156
+ hasChanges = true
157
+
158
+ case .identical:
159
+ break
160
+
161
+ @unknown default:
162
+ break
163
+ }
164
+ }
165
+
166
+ // Resolve concurrently
167
+ if !servicesToResolve.isEmpty {
168
+ await withTaskGroup(of: (String, ScanResult?).self) { group in
169
+ for service in servicesToResolve {
170
+ group.addTask {
171
+ let scanResult = await self.resolveService(
172
+ result: service.result,
173
+ name: service.name,
174
+ timeout: resolveTimeout
175
+ )
176
+ return (service.key, scanResult)
177
+ }
178
+ }
179
+
180
+ for await (key, scanResult) in group {
181
+ if let scanResult = scanResult {
182
+ await serviceCache.set(key, value: scanResult)
183
+ Loggy.log(.debug, message: "Resolved and cached service: \(scanResult.name)")
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ if hasChanges {
190
+ let allResolvedServices = await serviceCache.getAll()
191
+ notifyScanResultsListeners(with: allResolvedServices)
192
+ }
193
+ }
194
+
195
+ private func serviceKey(for result: NWBrowser.Result) -> String? {
196
+ guard case .service(let name, let type, let domain, _) = result.endpoint else {
197
+ return nil
198
+ }
199
+ return "\(name).\(type).\(domain)"
200
+ }
201
+ }
@@ -0,0 +1,66 @@
1
+ //
2
+ // LocalNetworkAuthorization.swift
3
+ // Pods
4
+ //
5
+ // Created by Dawid Zawada on 16/11/2025.
6
+ //
7
+ import Foundation
8
+ import Network
9
+
10
+ // This code is based on https://stackoverflow.com/a/67758105/2618437
11
+ // And: https://github.com/neurio/react-native-local-network-permission
12
+ public class LocalNetworkAuthorization: NSObject {
13
+ private var browser: NWBrowser?
14
+ private var netService: NetService?
15
+ private var completion: ((Bool) -> Void)?
16
+
17
+ public func requestAuthorization(completion: @escaping (Bool) -> Void) {
18
+ self.completion = completion
19
+
20
+ // Create parameters, and allow browsing over peer-to-peer link.
21
+ let parameters = NWParameters()
22
+ parameters.includePeerToPeer = true
23
+
24
+ // Browse for a custom service type.
25
+ let browser = NWBrowser(for: .bonjour(type: "_bonjour._tcp", domain: nil), using: parameters)
26
+ self.browser = browser
27
+ browser.stateUpdateHandler = { newState in
28
+ switch newState {
29
+ case .failed(let error):
30
+ print(error.localizedDescription)
31
+ case .ready, .cancelled:
32
+ break
33
+ case let .waiting(error):
34
+ Loggy.log(.warning, message: "Local network permission has been denied: \(error)")
35
+ self.reset()
36
+ self.completion?(false)
37
+ default:
38
+ break
39
+ }
40
+ }
41
+
42
+ self.netService = NetService(domain: "local.", type:"_lnp._tcp.", name: "LocalNetworkPrivacy", port: 1100)
43
+ self.netService?.delegate = self
44
+
45
+ self.browser?.start(queue: .main)
46
+ self.netService?.publish()
47
+ // the netService needs to be scheduled on a run loop, in this case the main runloop
48
+ self.netService?.schedule(in: .main, forMode: .common)
49
+ }
50
+
51
+ private func reset() {
52
+ print("resetting")
53
+ self.browser?.cancel()
54
+ self.browser = nil
55
+ self.netService?.stop()
56
+ self.netService = nil
57
+ }
58
+ }
59
+
60
+ extension LocalNetworkAuthorization : NetServiceDelegate {
61
+ public func netServiceDidPublish(_ sender: NetService) {
62
+ self.reset()
63
+ Loggy.log(.info, message: "Local Network permission granted")
64
+ completion?(true)
65
+ }
66
+ }
@@ -0,0 +1,14 @@
1
+ //
2
+ // LocalNetworkPermission+Listeners.swift
3
+ // Pods
4
+ //
5
+ // Created by Dawid Zawada on 16/11/2025.
6
+ //
7
+
8
+ extension LocalNetworkPermission {
9
+ internal func notifyPermissionListeners(with granted: Bool) {
10
+ for listener in permissionListeners.values {
11
+ listener(granted)
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,41 @@
1
+ //
2
+ // LocalNetworkPermission.swift
3
+ // Pods
4
+ //
5
+ // Created by Dawid Zawada on 16/11/2025.
6
+ //
7
+ import NitroModules
8
+ import Network
9
+
10
+ class LocalNetworkPermission: HybridLocalNetworkPermissionSpec {
11
+ internal var permissionListeners: [UUID: (Bool) -> Void] = [:]
12
+
13
+ func requestPermission() throws -> Promise<Bool> {
14
+ return Promise.async {
15
+ if #available(iOS 14.0, *) {
16
+ return try await self.requestAuthorizationAsync()
17
+ } else {
18
+ return true
19
+ }
20
+ }
21
+ }
22
+
23
+ func listenForPermission(onChange: @escaping (Bool) -> Void) -> BonjourListener {
24
+ let listenerId = UUID()
25
+ self.permissionListeners[listenerId] = onChange
26
+
27
+ return BonjourListener { [weak self] in
28
+ self?.permissionListeners.removeValue(forKey: listenerId)
29
+ }
30
+ }
31
+
32
+ private func requestAuthorizationAsync() async throws -> Bool {
33
+ return await withCheckedContinuation { continuation in
34
+ let authorizationInstance = LocalNetworkAuthorization()
35
+ authorizationInstance.requestAuthorization { granted in
36
+ self.notifyPermissionListeners(with: granted)
37
+ continuation.resume(returning: granted)
38
+ }
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,30 @@
1
+ //
2
+ // ServiceCache.swift
3
+ // Pods
4
+ //
5
+ // Created by Dawid Zawada on 05/11/2025.
6
+ //
7
+
8
+ actor ServiceCache {
9
+ private var cache: [String: ScanResult] = [:]
10
+
11
+ func get(_ key: String) -> ScanResult? {
12
+ return cache[key]
13
+ }
14
+
15
+ func set(_ key: String, value: ScanResult) {
16
+ cache[key] = value
17
+ }
18
+
19
+ func remove(_ key: String) -> ScanResult? {
20
+ return cache.removeValue(forKey: key)
21
+ }
22
+
23
+ func getAll() -> [ScanResult] {
24
+ return Array(cache.values)
25
+ }
26
+
27
+ func clear() {
28
+ cache.removeAll()
29
+ }
30
+ }
@@ -0,0 +1,37 @@
1
+ //
2
+ // Logger.swift
3
+ // Pods
4
+ //
5
+ // Created by Dawid Zawada on 17/11/2025.
6
+ //
7
+ enum LogLevel: String {
8
+ case debug
9
+ case info
10
+ case warning
11
+ case error
12
+ }
13
+
14
+ enum Loggy {
15
+ static var staticFormatter: DateFormatter?
16
+ static var formatter: DateFormatter {
17
+ guard let staticFormatter else {
18
+ let formatter = DateFormatter()
19
+ formatter.dateFormat = "HH:mm:ss.SSS"
20
+ self.staticFormatter = formatter
21
+ return formatter
22
+ }
23
+ return staticFormatter
24
+ }
25
+
26
+ /**
27
+ * Log a message to the console`
28
+ */
29
+ @inlinable
30
+ static func log(_ level: LogLevel,
31
+ message: String,
32
+ _ function: String = #function) {
33
+ let now = Date()
34
+ let time = formatter.string(from: now)
35
+ print("\(time): [\(level.rawValue)] 🌐 BonjourZeroconf.\(function): \(message)")
36
+ }
37
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+
3
+ import { NitroModules } from 'react-native-nitro-modules';
4
+ import { useIsScanning } from "./useIsScanning.js";
5
+ import { BonjourFail } from "./specs/BonjourFail.js";
6
+ import { requestLocalNetworkPermission, useLocalNetworkPermission } from './permissions';
7
+ export const Scanner = NitroModules.createHybridObject('BonjourZeroconf');
8
+ export { useIsScanning, requestLocalNetworkPermission, useLocalNetworkPermission, BonjourFail };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["NitroModules","useIsScanning","BonjourFail","requestLocalNetworkPermission","useLocalNetworkPermission","Scanner","createHybridObject"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,4BAA4B;AAEzD,SAASC,aAAa,QAAQ,oBAAiB;AAG/C,SAASC,WAAW,QAAQ,wBAAqB;AACjD,SACEC,6BAA6B,EAC7BC,yBAAyB,QACpB,eAAe;AAEtB,OAAO,MAAMC,OAAO,GAClBL,YAAY,CAACM,kBAAkB,CAAkB,iBAAiB,CAAC;AAErE,SACEL,aAAa,EACbE,6BAA6B,EAC7BC,yBAAyB,EACzBF,WAAW","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+
3
+ import { NitroModules } from 'react-native-nitro-modules';
4
+ import { useEffect, useState } from 'react';
5
+ const LocalNetworkPermission = NitroModules.createHybridObject('LocalNetworkPermission');
6
+ export const requestLocalNetworkPermission = async () => {
7
+ return await LocalNetworkPermission.requestPermission();
8
+ };
9
+ export const useLocalNetworkPermission = () => {
10
+ const [permissionGranted, setPermissionGranted] = useState(false);
11
+ useEffect(() => {
12
+ const {
13
+ remove
14
+ } = LocalNetworkPermission.listenForPermission(granted => {
15
+ setPermissionGranted(granted);
16
+ });
17
+ return () => {
18
+ remove();
19
+ };
20
+ }, []);
21
+ return [permissionGranted, requestLocalNetworkPermission];
22
+ };
23
+ //# sourceMappingURL=permissions.ios.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["NitroModules","useEffect","useState","LocalNetworkPermission","createHybridObject","requestLocalNetworkPermission","requestPermission","useLocalNetworkPermission","permissionGranted","setPermissionGranted","remove","listenForPermission","granted"],"sourceRoot":"../../src","sources":["permissions.ios.ts"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,4BAA4B;AAEzD,SAASC,SAAS,EAAEC,QAAQ,QAAQ,OAAO;AAE3C,MAAMC,sBAAsB,GAC1BH,YAAY,CAACI,kBAAkB,CAC7B,wBACF,CAAC;AAEH,OAAO,MAAMC,6BAA6B,GAAG,MAAAA,CAAA,KAAY;EACvD,OAAO,MAAMF,sBAAsB,CAACG,iBAAiB,CAAC,CAAC;AACzD,CAAC;AAED,OAAO,MAAMC,yBAAyB,GAAGA,CAAA,KAAM;EAC7C,MAAM,CAACC,iBAAiB,EAAEC,oBAAoB,CAAC,GAAGP,QAAQ,CAAC,KAAK,CAAC;EAEjED,SAAS,CAAC,MAAM;IACd,MAAM;MAAES;IAAO,CAAC,GAAGP,sBAAsB,CAACQ,mBAAmB,CAAEC,OAAO,IAAK;MACzEH,oBAAoB,CAACG,OAAO,CAAC;IAC/B,CAAC,CAAC;IACF,OAAO,MAAM;MACXF,MAAM,CAAC,CAAC;IACV,CAAC;EACH,CAAC,EAAE,EAAE,CAAC;EAEN,OAAO,CAACF,iBAAiB,EAAEH,6BAA6B,CAAC;AAC3D,CAAC","ignoreList":[]}
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+
3
+ export const requestLocalNetworkPermission = async () => {
4
+ return true;
5
+ };
6
+ export const useLocalNetworkPermission = () => {
7
+ return [true, requestLocalNetworkPermission];
8
+ };
9
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["requestLocalNetworkPermission","useLocalNetworkPermission"],"sourceRoot":"../../src","sources":["permissions.ts"],"mappings":";;AAAA,OAAO,MAAMA,6BAA6B,GAAG,MAAAA,CAAA,KAAY;EACvD,OAAO,IAAI;AACb,CAAC;AAED,OAAO,MAAMC,yBAAyB,GAAGA,CAAA,KAAM;EAC7C,OAAO,CAAC,IAAI,EAAED,6BAA6B,CAAC;AAC9C,CAAC","ignoreList":[]}
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+
3
+ export let BonjourFail = /*#__PURE__*/function (BonjourFail) {
4
+ BonjourFail[BonjourFail["RESOLVE_FAILED"] = 0] = "RESOLVE_FAILED";
5
+ BonjourFail[BonjourFail["EXTRACTION_FAILED"] = 1] = "EXTRACTION_FAILED";
6
+ BonjourFail[BonjourFail["DISCOVERY_FAILED"] = 2] = "DISCOVERY_FAILED";
7
+ return BonjourFail;
8
+ }({});
9
+ //# sourceMappingURL=BonjourFail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["BonjourFail"],"sourceRoot":"../../../src","sources":["specs/BonjourFail.ts"],"mappings":";;AAAA,WAAYA,WAAW,0BAAXA,WAAW;EAAXA,WAAW,CAAXA,WAAW;EAAXA,WAAW,CAAXA,WAAW;EAAXA,WAAW,CAAXA,WAAW;EAAA,OAAXA,WAAW;AAAA","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ //# sourceMappingURL=BonjourListener.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../../src","sources":["specs/BonjourListener.ts"],"mappings":"","ignoreList":[]}
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ export {};
4
+ //# sourceMappingURL=BonjourZeroconf.nitro.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../../src","sources":["specs/BonjourZeroconf.nitro.ts"],"mappings":"","ignoreList":[]}
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ export {};
4
+ //# sourceMappingURL=LocalNetworkPermission.nitro.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../../src","sources":["specs/LocalNetworkPermission.nitro.ts"],"mappings":"","ignoreList":[]}