@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.
- package/BonjourZeroconf.podspec +30 -0
- package/LICENSE +20 -0
- package/README.md +35 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +128 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf+AddressResolver.kt +182 -0
- package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf+Listeners.kt +45 -0
- package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconf.kt +183 -0
- package/android/src/main/java/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourZeroconfPackage.kt +22 -0
- package/ios/AddressResolverError.swift +20 -0
- package/ios/BonjourZeroconf+AddressResolver.swift +133 -0
- package/ios/BonjourZeroconf+Listeners.swift +26 -0
- package/ios/BonjourZeroconf.swift +201 -0
- package/ios/LocalNetworkAuthorization.swift +66 -0
- package/ios/LocalNetworkPermission+Listeners.swift +14 -0
- package/ios/LocalNetworkPermission.swift +41 -0
- package/ios/ServiceCache.swift +30 -0
- package/ios/Utils/Loggy.swift +37 -0
- package/lib/module/index.js +9 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/permissions.ios.js +23 -0
- package/lib/module/permissions.ios.js.map +1 -0
- package/lib/module/permissions.js +9 -0
- package/lib/module/permissions.js.map +1 -0
- package/lib/module/specs/BonjourFail.js +9 -0
- package/lib/module/specs/BonjourFail.js.map +1 -0
- package/lib/module/specs/BonjourListener.js +2 -0
- package/lib/module/specs/BonjourListener.js.map +1 -0
- package/lib/module/specs/BonjourZeroconf.nitro.js +4 -0
- package/lib/module/specs/BonjourZeroconf.nitro.js.map +1 -0
- package/lib/module/specs/LocalNetworkPermission.nitro.js +4 -0
- package/lib/module/specs/LocalNetworkPermission.nitro.js.map +1 -0
- package/lib/module/specs/ScanResult.js +2 -0
- package/lib/module/specs/ScanResult.js.map +1 -0
- package/lib/module/useIsScanning.js +19 -0
- package/lib/module/useIsScanning.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/index.d.ts +9 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/permissions.d.ts +3 -0
- package/lib/typescript/src/permissions.d.ts.map +1 -0
- package/lib/typescript/src/permissions.ios.d.ts +3 -0
- package/lib/typescript/src/permissions.ios.d.ts.map +1 -0
- package/lib/typescript/src/specs/BonjourFail.d.ts +6 -0
- package/lib/typescript/src/specs/BonjourFail.d.ts.map +1 -0
- package/lib/typescript/src/specs/BonjourListener.d.ts +4 -0
- package/lib/typescript/src/specs/BonjourListener.d.ts.map +1 -0
- package/lib/typescript/src/specs/BonjourZeroconf.nitro.d.ts +19 -0
- package/lib/typescript/src/specs/BonjourZeroconf.nitro.d.ts.map +1 -0
- package/lib/typescript/src/specs/LocalNetworkPermission.nitro.d.ts +9 -0
- package/lib/typescript/src/specs/LocalNetworkPermission.nitro.d.ts.map +1 -0
- package/lib/typescript/src/specs/ScanResult.d.ts +8 -0
- package/lib/typescript/src/specs/ScanResult.d.ts.map +1 -0
- package/lib/typescript/src/useIsScanning.d.ts +2 -0
- package/lib/typescript/src/useIsScanning.d.ts.map +1 -0
- package/nitro.json +20 -0
- package/nitrogen/generated/android/c++/JBonjourFail.hpp +62 -0
- package/nitrogen/generated/android/c++/JBonjourListener.hpp +68 -0
- package/nitrogen/generated/android/c++/JFunc_void.hpp +74 -0
- package/nitrogen/generated/android/c++/JFunc_void_BonjourFail.hpp +76 -0
- package/nitrogen/generated/android/c++/JFunc_void_bool.hpp +74 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_ScanResult_.hpp +97 -0
- package/nitrogen/generated/android/c++/JHybridBonjourZeroconfSpec.cpp +96 -0
- package/nitrogen/generated/android/c++/JHybridBonjourZeroconfSpec.hpp +69 -0
- package/nitrogen/generated/android/c++/JScanOptions.hpp +57 -0
- package/nitrogen/generated/android/c++/JScanResult.hpp +74 -0
- package/nitrogen/generated/android/dawidzawada_bonjourzeroconf+autolinking.cmake +82 -0
- package/nitrogen/generated/android/dawidzawada_bonjourzeroconf+autolinking.gradle +27 -0
- package/nitrogen/generated/android/dawidzawada_bonjourzeroconfOnLoad.cpp +52 -0
- package/nitrogen/generated/android/dawidzawada_bonjourzeroconfOnLoad.hpp +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourFail.kt +22 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/BonjourListener.kt +42 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/Func_void.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/Func_void_BonjourFail.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/Func_void_bool.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/Func_void_std__vector_ScanResult_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/HybridBonjourZeroconfSpec.kt +90 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/ScanOptions.kt +38 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/ScanResult.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/dawidzawada/bonjourzeroconf/dawidzawada_bonjourzeroconfOnLoad.kt +35 -0
- package/nitrogen/generated/ios/BonjourZeroconf+autolinking.rb +60 -0
- package/nitrogen/generated/ios/BonjourZeroconf-Swift-Cxx-Bridge.cpp +90 -0
- package/nitrogen/generated/ios/BonjourZeroconf-Swift-Cxx-Bridge.hpp +282 -0
- package/nitrogen/generated/ios/BonjourZeroconf-Swift-Cxx-Umbrella.hpp +65 -0
- package/nitrogen/generated/ios/BonjourZeroconfAutolinking.mm +41 -0
- package/nitrogen/generated/ios/BonjourZeroconfAutolinking.swift +40 -0
- package/nitrogen/generated/ios/c++/HybridBonjourZeroconfSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridBonjourZeroconfSpecSwift.hpp +120 -0
- package/nitrogen/generated/ios/c++/HybridLocalNetworkPermissionSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridLocalNetworkPermissionSpecSwift.hpp +87 -0
- package/nitrogen/generated/ios/swift/BonjourFail.swift +44 -0
- package/nitrogen/generated/ios/swift/BonjourListener.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_BonjourFail.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_ScanResult_.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridBonjourZeroconfSpec.swift +60 -0
- package/nitrogen/generated/ios/swift/HybridBonjourZeroconfSpec_cxx.swift +203 -0
- package/nitrogen/generated/ios/swift/HybridLocalNetworkPermissionSpec.swift +57 -0
- package/nitrogen/generated/ios/swift/HybridLocalNetworkPermissionSpec_cxx.swift +155 -0
- package/nitrogen/generated/ios/swift/ScanOptions.swift +48 -0
- package/nitrogen/generated/ios/swift/ScanResult.swift +149 -0
- package/nitrogen/generated/shared/c++/BonjourFail.hpp +63 -0
- package/nitrogen/generated/shared/c++/BonjourListener.hpp +75 -0
- package/nitrogen/generated/shared/c++/HybridBonjourZeroconfSpec.cpp +26 -0
- package/nitrogen/generated/shared/c++/HybridBonjourZeroconfSpec.hpp +80 -0
- package/nitrogen/generated/shared/c++/HybridLocalNetworkPermissionSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridLocalNetworkPermissionSpec.hpp +66 -0
- package/nitrogen/generated/shared/c++/ScanOptions.hpp +75 -0
- package/nitrogen/generated/shared/c++/ScanResult.hpp +92 -0
- package/package.json +169 -0
- package/src/index.ts +22 -0
- package/src/permissions.ios.ts +27 -0
- package/src/permissions.ts +7 -0
- package/src/specs/BonjourFail.ts +5 -0
- package/src/specs/BonjourListener.ts +3 -0
- package/src/specs/BonjourZeroconf.nitro.ts +20 -0
- package/src/specs/LocalNetworkPermission.nitro.ts +7 -0
- package/src/specs/ScanResult.ts +7 -0
- 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 @@
|
|
|
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 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["specs/BonjourListener.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["specs/BonjourZeroconf.nitro.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../../src","sources":["specs/LocalNetworkPermission.nitro.ts"],"mappings":"","ignoreList":[]}
|