react-native-zeroconf 0.16.1 → 0.17.1

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 (45) hide show
  1. package/NOTICE +0 -3
  2. package/README.md +3 -3
  3. package/android/build.gradle +0 -3
  4. package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroConfImplFactory.java +1 -1
  5. package/android/src/main/java/com/balthazargronon/RCTZeroconf/Zeroconf.java +12 -2
  6. package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfModule.java +129 -4
  7. package/android/src/main/java/com/balthazargronon/RCTZeroconf/ZeroconfOptions.java +77 -0
  8. package/android/src/main/java/com/balthazargronon/RCTZeroconf/dnssd/DnssdImpl.java +654 -0
  9. package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/NsdServiceImpl.java +393 -39
  10. package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/ServiceInfoCallbacks.java +50 -13
  11. package/android/src/main/java/com/balthazargronon/RCTZeroconf/nsd/ServiceTypesQuery.java +296 -0
  12. package/android/src/main/java/com/github/druk/dnssd/DNSSDEmbedded.java +1 -1
  13. package/android/src/main/jni/mdnsresponder/mDNSShared/dnssd_clientshim.c +59 -22
  14. package/dist/index.js +235 -48
  15. package/index.d.ts +105 -7
  16. package/ios/RNZeroconf/RNZeroconf.m +285 -22
  17. package/package.json +28 -4
  18. package/src/index.js +214 -44
  19. package/windows/ExperimentalFeatures.props +33 -0
  20. package/windows/NuGet.config +11 -0
  21. package/windows/RNZeroconf/RNZeroconf.cpp +228 -0
  22. package/windows/RNZeroconf/RNZeroconf.def +3 -0
  23. package/windows/RNZeroconf/RNZeroconf.h +70 -0
  24. package/windows/RNZeroconf/RNZeroconf.rc +0 -0
  25. package/windows/RNZeroconf/RNZeroconf.vcxproj +150 -0
  26. package/windows/RNZeroconf/RNZeroconf.vcxproj.filters +50 -0
  27. package/windows/RNZeroconf/ReactPackageProvider.cpp +20 -0
  28. package/windows/RNZeroconf/ReactPackageProvider.h +24 -0
  29. package/windows/RNZeroconf/ReactPackageProvider.idl +9 -0
  30. package/windows/RNZeroconf/ZeroconfCore.cpp +1095 -0
  31. package/windows/RNZeroconf/ZeroconfCore.h +167 -0
  32. package/windows/RNZeroconf/pch.cpp +1 -0
  33. package/windows/RNZeroconf/pch.h +30 -0
  34. package/windows/RNZeroconf/resource.h +5 -0
  35. package/windows/RNZeroconf/targetver.h +8 -0
  36. package/windows/RNZeroconf.sln +43 -0
  37. package/android/src/main/java/com/balthazargronon/RCTZeroconf/rx2dnssd/DnssdImpl.java +0 -305
  38. package/android/src/main/java/com/github/druk/rx2dnssd/BonjourService.java +0 -400
  39. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2BrowseListener.java +0 -55
  40. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2Dnssd.java +0 -127
  41. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2DnssdCommon.java +0 -280
  42. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2DnssdEmbedded.java +0 -31
  43. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2QueryListener.java +0 -70
  44. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2RegisterListener.java +0 -47
  45. package/android/src/main/java/com/github/druk/rx2dnssd/Rx2ResolveListener.java +0 -51
package/src/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react'
2
- import { Platform, NativeModules, DeviceEventEmitter } from 'react-native'
2
+ import { Platform, NativeModules, DeviceEventEmitter, PermissionsAndroid } from 'react-native'
3
3
  import { EventEmitter } from 'events'
4
4
 
5
5
  const RNZeroconf = NativeModules.RNZeroconf
@@ -38,6 +38,13 @@ const withDefaults = (defaults, options) => {
38
38
 
39
39
  const isOptionsObject = value => value !== null && typeof value === 'object'
40
40
 
41
+ // Options passed to native code, without the undefined ones
42
+ const nativeOptions = options =>
43
+ Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined))
44
+
45
+ // Android 17 (API 37) runtime permission, enforced for apps targeting API 37 or declaring it
46
+ const ACCESS_LOCAL_NETWORK = 'android.permission.ACCESS_LOCAL_NETWORK'
47
+
41
48
  // Each instance runs its own scan, identified by this id in native events
42
49
  let instanceCount = 0
43
50
 
@@ -90,6 +97,15 @@ const withAddressFamilies = service => {
90
97
  /**
91
98
  * TXT records as ordered [key, value] pairs, from an object or an array of pairs
92
99
  */
100
+ // Browsing this type lists the service types on the network instead of services
101
+ const SERVICE_TYPES = { type: 'services._dns-sd', protocol: 'udp' }
102
+
103
+ // "_http._tcp" -> { type: 'http', protocol: 'tcp' }
104
+ const parseServiceType = name => {
105
+ const match = /^_([^.]+)\._(tcp|udp)$/.exec(name)
106
+ return match ? { type: match[1], protocol: match[2] } : null
107
+ }
108
+
93
109
  const toTxtPairs = txt => {
94
110
  const entries = Array.isArray(txt) ? txt : Object.entries(txt || {})
95
111
  return entries.map(([key, value]) => [String(key), String(value)])
@@ -106,6 +122,8 @@ export default class Zeroconf extends EventEmitter {
106
122
  }
107
123
 
108
124
  this._services = {}
125
+ this._serviceTypes = {}
126
+ this._scanningTypes = false
109
127
  this._publishedServices = {}
110
128
  this._publishedImplTypes = {}
111
129
  this._scanImplType = null
@@ -148,6 +166,16 @@ export default class Zeroconf extends EventEmitter {
148
166
  }
149
167
  const { name } = service
150
168
 
169
+ if (this._scanningTypes) {
170
+ const serviceType = parseServiceType(name)
171
+ if (serviceType && !this._serviceTypes[name]) {
172
+ this._serviceTypes[name] = serviceType
173
+ this.emit('typeFound', serviceType)
174
+ this.emit('update')
175
+ }
176
+ return
177
+ }
178
+
151
179
  this._services[name] = { name }
152
180
  this.emit('found', name)
153
181
  this.emit('update')
@@ -159,6 +187,16 @@ export default class Zeroconf extends EventEmitter {
159
187
  }
160
188
  const { name } = service
161
189
 
190
+ if (this._scanningTypes) {
191
+ const serviceType = this._serviceTypes[name]
192
+ if (serviceType) {
193
+ delete this._serviceTypes[name]
194
+ this.emit('typeRemove', serviceType)
195
+ this.emit('update')
196
+ }
197
+ return
198
+ }
199
+
162
200
  delete this._services[name]
163
201
 
164
202
  this.emit('remove', name)
@@ -237,6 +275,24 @@ export default class Zeroconf extends EventEmitter {
237
275
  return this._services
238
276
  }
239
277
 
278
+ /**
279
+ * Get the service types found by scanServiceTypes, as [{ type, protocol }]
280
+ */
281
+ getServiceTypes() {
282
+ return Object.values(this._serviceTypes)
283
+ }
284
+
285
+ /**
286
+ * Scan for the service types advertised on the network (_http._tcp, _ipp._tcp...), emits typeFound and typeRemove.
287
+ * On iOS, only types declared in NSBonjourServices can then be scanned.
288
+ * On iOS it requires the multicast entitlement. On Android with NSD and on Windows, services other apps publish on the same device are left out.
289
+ *
290
+ * scanServiceTypes({ domain, implType })
291
+ */
292
+ scanServiceTypes({ domain, implType, networkInterface } = {}) {
293
+ this._startScan({ ...SERVICE_TYPES, domain, implType, networkInterface }, true)
294
+ }
295
+
240
296
  /**
241
297
  * Scan for Zeroconf services, defaults to _http._tcp. on the local. domain
242
298
  *
@@ -244,14 +300,21 @@ export default class Zeroconf extends EventEmitter {
244
300
  * scan(type, protocol, domain, implType) is deprecated
245
301
  */
246
302
  scan(options, protocolArg, domainArg, implTypeArg) {
247
- const { type, protocol, domain, implType, resolveTimeout } = withDefaults(
248
- SCAN_DEFAULTS,
303
+ this._startScan(
249
304
  isOptionsObject(options)
250
305
  ? options
251
306
  : { type: options, protocol: protocolArg, domain: domainArg, implType: implTypeArg },
307
+ false,
252
308
  )
309
+ }
310
+
311
+ _startScan(options, scanningTypes) {
312
+ const { type, protocol, domain, implType, resolveTimeout, subtype, networkInterface } =
313
+ withDefaults(SCAN_DEFAULTS, options)
253
314
 
254
315
  this._services = {}
316
+ this._serviceTypes = {}
317
+ this._scanningTypes = scanningTypes
255
318
  this._hasScanned = true
256
319
  this.emit('update')
257
320
  if (Platform.OS === 'android') {
@@ -260,9 +323,22 @@ export default class Zeroconf extends EventEmitter {
260
323
  RNZeroconf.stop(this._scanId, this._scanImplType)
261
324
  }
262
325
  this._scanImplType = implType
263
- RNZeroconf.scan(this._scanId, type, protocol, domain, implType)
326
+ RNZeroconf.scan(
327
+ this._scanId,
328
+ type,
329
+ protocol,
330
+ domain,
331
+ implType,
332
+ nativeOptions({ resolveTimeout, subtype, networkInterface }),
333
+ )
264
334
  } else {
265
- RNZeroconf.scan(this._scanId, type, protocol, domain, resolveTimeout)
335
+ RNZeroconf.scan(
336
+ this._scanId,
337
+ type,
338
+ protocol,
339
+ domain,
340
+ nativeOptions({ resolveTimeout, subtype, networkInterface }),
341
+ )
266
342
  }
267
343
  }
268
344
 
@@ -279,13 +355,22 @@ export default class Zeroconf extends EventEmitter {
279
355
  }
280
356
 
281
357
  /**
282
- * Checks the iOS Local Network permission, resolves 'granted', 'denied' or 'unknown'.
283
- * Uses a service type from NSBonjourServices (the first one by default) and can show the permission prompt.
284
- * Android has no equivalent permission to check and resolves 'unknown'.
358
+ * Checks the Local Network permission, resolves 'granted', 'denied' or 'unknown'.
359
+ * iOS: uses a service type from NSBonjourServices (the first one by default) and can show the permission prompt.
360
+ * Android: apps targeting Android 17 (API 37) need ACCESS_LOCAL_NETWORK on Android 17 devices, it is requested
361
+ * when missing unless request is false. 'granted' when there is nothing to grant.
285
362
  */
286
- checkLocalNetworkAccess({ type, protocol = 'tcp', timeout = 5 } = {}) {
363
+ async checkLocalNetworkAccess({ type, protocol = 'tcp', timeout = 5, request = true } = {}) {
287
364
  if (Platform.OS === 'android') {
288
- return Promise.resolve('unknown')
365
+ if (!RNZeroconf.checkLocalNetworkAccess) {
366
+ return 'unknown'
367
+ }
368
+ const status = await asPromise(RNZeroconf.checkLocalNetworkAccess())
369
+ if (status !== 'denied' || !request) {
370
+ return status
371
+ }
372
+ const result = await PermissionsAndroid.request(ACCESS_LOCAL_NETWORK)
373
+ return result === PermissionsAndroid.RESULTS.GRANTED ? 'granted' : 'denied'
289
374
  }
290
375
  const serviceType = type ? `_${type}._${protocol}` : null
291
376
  return asPromise(RNZeroconf.checkLocalNetworkAccess(serviceType, timeout))
@@ -299,28 +384,68 @@ export default class Zeroconf extends EventEmitter {
299
384
  * publishService(type, protocol, domain, name, port, txt, implType) is deprecated
300
385
  */
301
386
  publishService(options, protocolArg, domainArg, nameArg, portArg, txtArg, implTypeArg) {
302
- const { type, protocol, domain, name, port, txt, implType } = withDefaults(
303
- PUBLISH_DEFAULTS,
304
- isOptionsObject(options)
305
- ? options
306
- : {
307
- type: options,
308
- protocol: protocolArg,
309
- domain: domainArg,
310
- name: nameArg,
311
- port: portArg,
312
- txt: txtArg,
313
- implType: implTypeArg,
314
- },
315
- )
387
+ const { type, protocol, domain, name, port, txt, implType, subtypes, networkInterface } =
388
+ withDefaults(
389
+ PUBLISH_DEFAULTS,
390
+ isOptionsObject(options)
391
+ ? options
392
+ : {
393
+ type: options,
394
+ protocol: protocolArg,
395
+ domain: domainArg,
396
+ name: nameArg,
397
+ port: portArg,
398
+ txt: txtArg,
399
+ implType: implTypeArg,
400
+ },
401
+ )
316
402
  const txtRecord = toTxtPairs(txt)
403
+ const native = nativeOptions({ subtypes, networkInterface })
317
404
  if (Platform.OS === 'android') {
318
405
  this._publishedImplTypes[name] = implType
319
406
  return asPromise(
320
- RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, implType),
407
+ RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, implType, native),
321
408
  )
322
409
  }
323
- return asPromise(RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord))
410
+ return asPromise(
411
+ RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, native),
412
+ )
413
+ }
414
+
415
+ /**
416
+ * Replace the TXT record of a published service, resolves with the updated service.
417
+ * Android NSD has no update, the service is published again under the same name.
418
+ *
419
+ * updateService(name, { txt })
420
+ */
421
+ updateService(
422
+ name,
423
+ { txt = {}, implType = this._publishedImplTypes[name] || ImplType.NSD } = {},
424
+ ) {
425
+ const txtRecord = toTxtPairs(txt)
426
+ if (Platform.OS === 'android') {
427
+ return asPromise(RNZeroconf.updateService(name, txtRecord, implType))
428
+ }
429
+ return asPromise(RNZeroconf.updateService(name, txtRecord))
430
+ }
431
+
432
+ /**
433
+ * Resolve one service by name without scanning, for a device found before.
434
+ * Resolves with the service, rejects with code 'TIMEOUT' when it doesn't answer in time.
435
+ *
436
+ * resolveService({ name, type, protocol, domain, implType, timeout, networkInterface })
437
+ */
438
+ resolveService(options) {
439
+ const { name, type, protocol, domain, implType, timeout, networkInterface } = withDefaults(
440
+ { ...SCAN_DEFAULTS, timeout: 5 },
441
+ options,
442
+ )
443
+ const native = nativeOptions({ timeout, networkInterface })
444
+ const promise =
445
+ Platform.OS === 'android'
446
+ ? RNZeroconf.resolveService(name, type, protocol, domain, implType, native)
447
+ : RNZeroconf.resolveService(name, type, protocol, domain, native)
448
+ return asPromise(asPromise(promise).then(service => service && withAddressFamilies(service)))
324
449
  }
325
450
 
326
451
  /**
@@ -336,31 +461,23 @@ export default class Zeroconf extends EventEmitter {
336
461
  }
337
462
  }
338
463
 
339
- /**
340
- * Scans while mounted and returns the resolved services.
341
- * Scans again when the options change, stops and cleans up on unmount.
342
- * Each hook runs its own scan, several can run at once.
343
- *
344
- * const { services, isScanning, error, stop, restart } = useZeroconf({ type: 'http' })
345
- */
346
- export function useZeroconf(options = {}) {
347
- const { type, protocol, domain, implType, resolveTimeout, enabled = true } = options
464
+ // Runs a scan while mounted and enabled, and again when deps change or on restart
465
+ function useScan(enabled, startScan, read, deps) {
348
466
  const zeroconfRef = useRef(null)
349
- const [services, setServices] = useState([])
467
+ const [items, setItems] = useState([])
350
468
  const [isScanning, setIsScanning] = useState(false)
351
469
  const [error, setError] = useState(null)
352
470
  const [scanCount, setScanCount] = useState(0)
471
+ const readRef = useRef(read)
472
+ readRef.current = read
353
473
 
354
474
  useEffect(() => {
355
475
  const zeroconf = new Zeroconf()
356
476
  zeroconfRef.current = zeroconf
357
- // Found services only have a name until they are resolved
358
- const updateServices = () =>
359
- setServices(Object.values(zeroconf.getServices()).filter(service => service.addresses))
360
477
  const unsubscribes = [
361
478
  zeroconf.subscribe('start', () => setIsScanning(true)),
362
479
  zeroconf.subscribe('stop', () => setIsScanning(false)),
363
- zeroconf.subscribe('update', updateServices),
480
+ zeroconf.subscribe('update', () => setItems(readRef.current(zeroconf))),
364
481
  zeroconf.subscribe('error', setError),
365
482
  ]
366
483
  return () => {
@@ -376,11 +493,11 @@ export function useZeroconf(options = {}) {
376
493
  if (!zeroconf || !enabled) {
377
494
  return undefined
378
495
  }
379
- setServices([])
496
+ setItems([])
380
497
  setError(null)
381
- zeroconf.scan({ type, protocol, domain, implType, resolveTimeout })
498
+ startScan(zeroconf)
382
499
  return () => zeroconf.stop()
383
- }, [enabled, type, protocol, domain, implType, resolveTimeout, scanCount])
500
+ }, [enabled, scanCount, ...deps])
384
501
 
385
502
  const stop = useCallback(() => {
386
503
  if (zeroconfRef.current) {
@@ -389,5 +506,58 @@ export function useZeroconf(options = {}) {
389
506
  }, [])
390
507
  const restart = useCallback(() => setScanCount(count => count + 1), [])
391
508
 
392
- return { services, isScanning, error, stop, restart }
509
+ return { items, isScanning, error, stop, restart }
510
+ }
511
+
512
+ /**
513
+ * Scans while mounted and returns the resolved services.
514
+ * Scans again when the options change, stops and cleans up on unmount.
515
+ * Each hook runs its own scan, several can run at once.
516
+ *
517
+ * const { services, isScanning, error, stop, restart } = useZeroconf({ type: 'http' })
518
+ */
519
+ export function useZeroconf(options = {}) {
520
+ const {
521
+ type,
522
+ protocol,
523
+ domain,
524
+ implType,
525
+ resolveTimeout,
526
+ subtype,
527
+ networkInterface,
528
+ enabled = true,
529
+ } = options
530
+ const { items, ...scan } = useScan(
531
+ enabled,
532
+ zeroconf =>
533
+ zeroconf.scan({
534
+ type,
535
+ protocol,
536
+ domain,
537
+ implType,
538
+ resolveTimeout,
539
+ subtype,
540
+ networkInterface,
541
+ }),
542
+ // Found services only have a name until they are resolved
543
+ zeroconf => Object.values(zeroconf.getServices()).filter(service => service.addresses),
544
+ [type, protocol, domain, implType, resolveTimeout, subtype, networkInterface],
545
+ )
546
+ return { services: items, ...scan }
547
+ }
548
+
549
+ /**
550
+ * Lists the service types advertised on the network while mounted.
551
+ *
552
+ * const { serviceTypes, isScanning, error, stop, restart } = useServiceTypes()
553
+ */
554
+ export function useServiceTypes(options = {}) {
555
+ const { domain, implType, networkInterface, enabled = true } = options
556
+ const { items, ...scan } = useScan(
557
+ enabled,
558
+ zeroconf => zeroconf.scanServiceTypes({ domain, implType, networkInterface }),
559
+ zeroconf => zeroconf.getServiceTypes(),
560
+ [domain, implType, networkInterface],
561
+ )
562
+ return { serviceTypes: items, ...scan }
393
563
  }
@@ -0,0 +1,33 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3
+
4
+ <PropertyGroup Label="Microsoft.ReactNative Experimental Features">
5
+ <!--
6
+ Required for building a New Architecture project.
7
+
8
+ Library projects can change this value to test against Old and New
9
+ Architectures when building the library project's local sln file.
10
+
11
+ Otherwise this value will be decided by the consuming RNW app.
12
+
13
+ See https://microsoft.github.io/react-native-windows/docs/new-architecture
14
+ -->
15
+ <RnwNewArch>true</RnwNewArch>
16
+
17
+ <!--
18
+ Changes compilation to assume use of Microsoft.ReactNative NuGet packages
19
+ instead of building the framework from source. Defaults to true.
20
+
21
+ This is set during library project creation and is used when building
22
+ the library project's local sln file.
23
+
24
+ Otherwise this value will be decided by the consuming RNW app.
25
+
26
+ See https://microsoft.github.io/react-native-windows/docs/nuget
27
+ -->
28
+ <UseExperimentalNuget>true</UseExperimentalNuget>
29
+
30
+ <ReactExperimentalFeaturesSet>true</ReactExperimentalFeaturesSet>
31
+ </PropertyGroup>
32
+
33
+ </Project>
@@ -0,0 +1,11 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <configuration>
3
+ <packageSources>
4
+ <clear />
5
+ <add key="react-native" value="https://pkgs.dev.azure.com/ms/react-native/_packaging/react-native-public/nuget/v3/index.json" />
6
+ <add key="Nuget.org" value="https://api.nuget.org/v3/index.json" />
7
+ </packageSources>
8
+ <disabledPackageSources>
9
+ <clear />
10
+ </disabledPackageSources>
11
+ </configuration>
@@ -0,0 +1,228 @@
1
+ #include "pch.h"
2
+
3
+ #include "RNZeroconf.h"
4
+
5
+ using namespace rnzeroconf;
6
+
7
+ namespace winrt::RNZeroconf
8
+ {
9
+
10
+ namespace {
11
+
12
+ std::wstring Wide(std::string const &value) {
13
+ return Widen(value);
14
+ }
15
+
16
+ std::wstring StringOption(React::JSValue const &options, char const *key) {
17
+ if (options.Type() != React::JSValueType::Object) {
18
+ return L"";
19
+ }
20
+ auto const &value = options.AsObject();
21
+ auto it = value.find(key);
22
+ return it != value.end() && it->second.Type() == React::JSValueType::String ? Wide(it->second.AsString()) : L"";
23
+ }
24
+
25
+ double NumberOption(React::JSValue const &options, char const *key) {
26
+ if (options.Type() != React::JSValueType::Object) {
27
+ return 0;
28
+ }
29
+ auto const &value = options.AsObject();
30
+ auto it = value.find(key);
31
+ return it != value.end() && (it->second.Type() == React::JSValueType::Double || it->second.Type() == React::JSValueType::Int64)
32
+ ? it->second.AsDouble()
33
+ : 0;
34
+ }
35
+
36
+ // [[key, value], ...] from JavaScript
37
+ TxtPairs ReadTxt(React::JSValue const &txt) {
38
+ TxtPairs pairs;
39
+ if (txt.Type() != React::JSValueType::Array) {
40
+ return pairs;
41
+ }
42
+ for (auto const &pair : txt.AsArray()) {
43
+ if (pair.Type() == React::JSValueType::Array && pair.AsArray().size() == 2) {
44
+ pairs.emplace_back(Wide(pair.AsArray()[0].AsString()), Wide(pair.AsArray()[1].AsString()));
45
+ }
46
+ }
47
+ return pairs;
48
+ }
49
+
50
+ React::JSValueObject ServiceObject(Service const &service) {
51
+ React::JSValueArray addresses;
52
+ for (auto const &address : service.addresses) {
53
+ addresses.push_back(Narrow(address));
54
+ }
55
+ React::JSValueObject txt;
56
+ for (auto const &pair : service.txt) {
57
+ txt[Narrow(pair.first)] = Narrow(pair.second);
58
+ }
59
+ React::JSValueObject object;
60
+ object["name"] = Narrow(service.name);
61
+ object["fullName"] = Narrow(service.fullName);
62
+ object["host"] = Narrow(service.host);
63
+ object["port"] = static_cast<int64_t>(service.port);
64
+ object["addresses"] = std::move(addresses);
65
+ object["txt"] = std::move(txt);
66
+ return object;
67
+ }
68
+
69
+ React::JSValueObject ErrorObject(Error const &error) {
70
+ React::JSValueObject object;
71
+ object["message"] = Narrow(error.message);
72
+ if (error.stringCode.empty()) {
73
+ object["code"] = error.code;
74
+ } else {
75
+ object["code"] = Narrow(error.stringCode);
76
+ }
77
+ object["domain"] = Narrow(error.domain);
78
+ if (!error.serviceName.empty()) {
79
+ object["serviceName"] = Narrow(error.serviceName);
80
+ }
81
+ return object;
82
+ }
83
+
84
+ // Rejects with the same { message, code, domain, serviceName } in userInfo as iOS and Android
85
+ void Reject(React::ReactPromise<React::JSValue> const &promise, Error const &error) {
86
+ React::ReactError reactError;
87
+ reactError.Code = error.stringCode.empty() ? std::to_string(error.code) : Narrow(error.stringCode);
88
+ reactError.Message = Narrow(error.message);
89
+ reactError.UserInfo = ErrorObject(error);
90
+ promise.Reject(std::move(reactError));
91
+ }
92
+
93
+ } // namespace
94
+
95
+ void RNZeroconf::Initialize(React::ReactContext const &reactContext) noexcept {
96
+ m_context = reactContext;
97
+
98
+ Events events;
99
+ events.start = [this](std::string const &scanId) {
100
+ React::JSValueObject body;
101
+ body["scanId"] = scanId;
102
+ Emit(L"RNZeroconfStart", std::move(body));
103
+ };
104
+ events.stop = [this](std::string const &scanId) {
105
+ React::JSValueObject body;
106
+ body["scanId"] = scanId;
107
+ Emit(L"RNZeroconfStop", std::move(body));
108
+ };
109
+ events.found = [this](std::string const &scanId, std::wstring const &name) {
110
+ React::JSValueObject body;
111
+ body["name"] = Narrow(name);
112
+ body["scanId"] = scanId;
113
+ Emit(L"RNZeroconfFound", std::move(body));
114
+ };
115
+ events.remove = [this](std::string const &scanId, std::wstring const &name) {
116
+ React::JSValueObject body;
117
+ body["name"] = Narrow(name);
118
+ body["scanId"] = scanId;
119
+ Emit(L"RNZeroconfRemove", std::move(body));
120
+ };
121
+ events.resolved = [this](std::string const &scanId, Service const &service) {
122
+ React::JSValueObject body = ServiceObject(service);
123
+ body["scanId"] = scanId;
124
+ Emit(L"RNZeroconfResolved", std::move(body));
125
+ };
126
+ events.published = [this](Service const &service) { Emit(L"RNZeroconfServiceRegistered", ServiceObject(service)); };
127
+ events.unpublished = [this](Service const &service) { Emit(L"RNZeroconfServiceUnregistered", ServiceObject(service)); };
128
+ events.error = [this](std::string const &scanId, Error const &error) {
129
+ React::JSValueObject body = ErrorObject(error);
130
+ if (!scanId.empty()) {
131
+ body["scanId"] = scanId;
132
+ }
133
+ Emit(L"RNZeroconfError", std::move(body));
134
+ };
135
+ m_zeroconf = std::make_unique<Zeroconf>(std::move(events));
136
+ }
137
+
138
+ void RNZeroconf::Emit(std::wstring_view eventName, React::JSValueObject body) noexcept {
139
+ m_context.EmitJSEvent(L"RCTDeviceEventEmitter", eventName, std::move(body));
140
+ }
141
+
142
+ void RNZeroconf::scan(std::string scanId, std::string type, std::string protocol, std::string domain, React::JSValue options) noexcept {
143
+ m_zeroconf->Scan(scanId, Wide(type), Wide(protocol), Wide(domain), StringOption(options, "subtype"), StringOption(options, "networkInterface"),
144
+ NumberOption(options, "resolveTimeout"));
145
+ }
146
+
147
+ void RNZeroconf::stop(React::JSValue scanId) noexcept {
148
+ if (scanId.Type() == React::JSValueType::String && !scanId.AsString().empty()) {
149
+ m_zeroconf->Stop(scanId.AsString());
150
+ } else {
151
+ m_zeroconf->StopAll();
152
+ }
153
+ }
154
+
155
+ void RNZeroconf::registerService(
156
+ std::string type,
157
+ std::string protocol,
158
+ std::string domain,
159
+ std::string name,
160
+ int port,
161
+ React::JSValue txt,
162
+ React::JSValue options,
163
+ React::ReactPromise<React::JSValue> promise) noexcept {
164
+ // windns.h can't register subtypes
165
+ if (options.Type() == React::JSValueType::Object) {
166
+ auto const &object = options.AsObject();
167
+ auto subtypes = object.find("subtypes");
168
+ if (subtypes != object.end() && subtypes->second.Type() == React::JSValueType::Array && !subtypes->second.AsArray().empty()) {
169
+ Error error;
170
+ error.domain = L"RNZeroconf";
171
+ error.stringCode = L"UNSUPPORTED";
172
+ error.message = L"Publishing subtypes is not supported on Windows";
173
+ error.serviceName = Wide(name);
174
+ Reject(promise, error);
175
+ return;
176
+ }
177
+ }
178
+ m_zeroconf->Publish(
179
+ Wide(type),
180
+ Wide(protocol),
181
+ Wide(domain),
182
+ Wide(name),
183
+ static_cast<uint16_t>(port),
184
+ ReadTxt(txt),
185
+ StringOption(options, "networkInterface"),
186
+ [promise](Service const &service) mutable { promise.Resolve(React::JSValue(ServiceObject(service))); },
187
+ [promise](Error const &error) mutable { Reject(promise, error); });
188
+ }
189
+
190
+ void RNZeroconf::unregisterService(std::string name, React::ReactPromise<React::JSValue> promise) noexcept {
191
+ m_zeroconf->Unpublish(
192
+ Wide(name),
193
+ [promise](Service const &service) mutable { promise.Resolve(React::JSValue(ServiceObject(service))); },
194
+ [promise](Error const &error) mutable { Reject(promise, error); });
195
+ }
196
+
197
+ void RNZeroconf::updateService(std::string name, React::JSValue txt, React::ReactPromise<React::JSValue> promise) noexcept {
198
+ m_zeroconf->Update(
199
+ Wide(name),
200
+ ReadTxt(txt),
201
+ [promise](Service const &service) mutable { promise.Resolve(React::JSValue(ServiceObject(service))); },
202
+ [promise](Error const &error) mutable { Reject(promise, error); });
203
+ }
204
+
205
+ void RNZeroconf::resolveService(
206
+ std::string name,
207
+ std::string type,
208
+ std::string protocol,
209
+ std::string domain,
210
+ React::JSValue options,
211
+ React::ReactPromise<React::JSValue> promise) noexcept {
212
+ m_zeroconf->ResolveService(
213
+ Wide(name),
214
+ Wide(type),
215
+ Wide(protocol),
216
+ Wide(domain),
217
+ StringOption(options, "networkInterface"),
218
+ NumberOption(options, "timeout"),
219
+ [promise](Service const &service) mutable { promise.Resolve(React::JSValue(ServiceObject(service))); },
220
+ [promise](Error const &error) mutable { Reject(promise, error); });
221
+ }
222
+
223
+ void RNZeroconf::checkLocalNetworkAccess(React::JSValue, React::JSValue, React::ReactPromise<React::JSValue> promise) noexcept {
224
+ // Nothing to grant on Windows, as on Android before Android 17
225
+ promise.Resolve(React::JSValue("granted"));
226
+ }
227
+
228
+ } // namespace winrt::RNZeroconf
@@ -0,0 +1,3 @@
1
+ EXPORTS
2
+ DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE
3
+ DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE