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/dist/index.js CHANGED
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.default = exports.ImplType = void 0;
7
+ exports.useServiceTypes = useServiceTypes;
7
8
  exports.useZeroconf = useZeroconf;
8
9
  var _react = require("react");
9
10
  var _reactNative = require("react-native");
@@ -42,6 +43,12 @@ const withDefaults = (defaults, options) => {
42
43
  };
43
44
  const isOptionsObject = value => value !== null && typeof value === 'object';
44
45
 
46
+ // Options passed to native code, without the undefined ones
47
+ const nativeOptions = options => Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined));
48
+
49
+ // Android 17 (API 37) runtime permission, enforced for apps targeting API 37 or declaring it
50
+ const ACCESS_LOCAL_NETWORK = 'android.permission.ACCESS_LOCAL_NETWORK';
51
+
45
52
  // Each instance runs its own scan, identified by this id in native events
46
53
  let instanceCount = 0;
47
54
 
@@ -97,6 +104,20 @@ const withAddressFamilies = service => {
97
104
  /**
98
105
  * TXT records as ordered [key, value] pairs, from an object or an array of pairs
99
106
  */
107
+ // Browsing this type lists the service types on the network instead of services
108
+ const SERVICE_TYPES = {
109
+ type: 'services._dns-sd',
110
+ protocol: 'udp'
111
+ };
112
+
113
+ // "_http._tcp" -> { type: 'http', protocol: 'tcp' }
114
+ const parseServiceType = name => {
115
+ const match = /^_([^.]+)\._(tcp|udp)$/.exec(name);
116
+ return match ? {
117
+ type: match[1],
118
+ protocol: match[2]
119
+ } : null;
120
+ };
100
121
  const toTxtPairs = txt => {
101
122
  const entries = Array.isArray(txt) ? txt : Object.entries(txt || {});
102
123
  return entries.map(([key, value]) => [String(key), String(value)]);
@@ -108,6 +129,8 @@ class Zeroconf extends _events.EventEmitter {
108
129
  throw new Error('react-native-zeroconf: native module not found. Make sure the library is linked and the app rebuilt. Expo Go is not supported, use a development build instead.');
109
130
  }
110
131
  this._services = {};
132
+ this._serviceTypes = {};
133
+ this._scanningTypes = false;
111
134
  this._publishedServices = {};
112
135
  this._publishedImplTypes = {};
113
136
  this._scanImplType = null;
@@ -146,6 +169,15 @@ class Zeroconf extends _events.EventEmitter {
146
169
  const {
147
170
  name
148
171
  } = service;
172
+ if (this._scanningTypes) {
173
+ const serviceType = parseServiceType(name);
174
+ if (serviceType && !this._serviceTypes[name]) {
175
+ this._serviceTypes[name] = serviceType;
176
+ this.emit('typeFound', serviceType);
177
+ this.emit('update');
178
+ }
179
+ return;
180
+ }
149
181
  this._services[name] = {
150
182
  name
151
183
  };
@@ -159,6 +191,15 @@ class Zeroconf extends _events.EventEmitter {
159
191
  const {
160
192
  name
161
193
  } = service;
194
+ if (this._scanningTypes) {
195
+ const serviceType = this._serviceTypes[name];
196
+ if (serviceType) {
197
+ delete this._serviceTypes[name];
198
+ this.emit('typeRemove', serviceType);
199
+ this.emit('update');
200
+ }
201
+ return;
202
+ }
162
203
  delete this._services[name];
163
204
  this.emit('remove', name);
164
205
  this.emit('update');
@@ -226,6 +267,33 @@ class Zeroconf extends _events.EventEmitter {
226
267
  return this._services;
227
268
  }
228
269
 
270
+ /**
271
+ * Get the service types found by scanServiceTypes, as [{ type, protocol }]
272
+ */
273
+ getServiceTypes() {
274
+ return Object.values(this._serviceTypes);
275
+ }
276
+
277
+ /**
278
+ * Scan for the service types advertised on the network (_http._tcp, _ipp._tcp...), emits typeFound and typeRemove.
279
+ * On iOS, only types declared in NSBonjourServices can then be scanned.
280
+ * 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.
281
+ *
282
+ * scanServiceTypes({ domain, implType })
283
+ */
284
+ scanServiceTypes({
285
+ domain,
286
+ implType,
287
+ networkInterface
288
+ } = {}) {
289
+ this._startScan({
290
+ ...SERVICE_TYPES,
291
+ domain,
292
+ implType,
293
+ networkInterface
294
+ }, true);
295
+ }
296
+
229
297
  /**
230
298
  * Scan for Zeroconf services, defaults to _http._tcp. on the local. domain
231
299
  *
@@ -233,19 +301,26 @@ class Zeroconf extends _events.EventEmitter {
233
301
  * scan(type, protocol, domain, implType) is deprecated
234
302
  */
235
303
  scan(options, protocolArg, domainArg, implTypeArg) {
304
+ this._startScan(isOptionsObject(options) ? options : {
305
+ type: options,
306
+ protocol: protocolArg,
307
+ domain: domainArg,
308
+ implType: implTypeArg
309
+ }, false);
310
+ }
311
+ _startScan(options, scanningTypes) {
236
312
  const {
237
313
  type,
238
314
  protocol,
239
315
  domain,
240
316
  implType,
241
- resolveTimeout
242
- } = withDefaults(SCAN_DEFAULTS, isOptionsObject(options) ? options : {
243
- type: options,
244
- protocol: protocolArg,
245
- domain: domainArg,
246
- implType: implTypeArg
247
- });
317
+ resolveTimeout,
318
+ subtype,
319
+ networkInterface
320
+ } = withDefaults(SCAN_DEFAULTS, options);
248
321
  this._services = {};
322
+ this._serviceTypes = {};
323
+ this._scanningTypes = scanningTypes;
249
324
  this._hasScanned = true;
250
325
  this.emit('update');
251
326
  if (_reactNative.Platform.OS === 'android') {
@@ -254,9 +329,17 @@ class Zeroconf extends _events.EventEmitter {
254
329
  RNZeroconf.stop(this._scanId, this._scanImplType);
255
330
  }
256
331
  this._scanImplType = implType;
257
- RNZeroconf.scan(this._scanId, type, protocol, domain, implType);
332
+ RNZeroconf.scan(this._scanId, type, protocol, domain, implType, nativeOptions({
333
+ resolveTimeout,
334
+ subtype,
335
+ networkInterface
336
+ }));
258
337
  } else {
259
- RNZeroconf.scan(this._scanId, type, protocol, domain, resolveTimeout);
338
+ RNZeroconf.scan(this._scanId, type, protocol, domain, nativeOptions({
339
+ resolveTimeout,
340
+ subtype,
341
+ networkInterface
342
+ }));
260
343
  }
261
344
  }
262
345
 
@@ -273,17 +356,27 @@ class Zeroconf extends _events.EventEmitter {
273
356
  }
274
357
 
275
358
  /**
276
- * Checks the iOS Local Network permission, resolves 'granted', 'denied' or 'unknown'.
277
- * Uses a service type from NSBonjourServices (the first one by default) and can show the permission prompt.
278
- * Android has no equivalent permission to check and resolves 'unknown'.
359
+ * Checks the Local Network permission, resolves 'granted', 'denied' or 'unknown'.
360
+ * iOS: uses a service type from NSBonjourServices (the first one by default) and can show the permission prompt.
361
+ * Android: apps targeting Android 17 (API 37) need ACCESS_LOCAL_NETWORK on Android 17 devices, it is requested
362
+ * when missing unless request is false. 'granted' when there is nothing to grant.
279
363
  */
280
- checkLocalNetworkAccess({
364
+ async checkLocalNetworkAccess({
281
365
  type,
282
366
  protocol = 'tcp',
283
- timeout = 5
367
+ timeout = 5,
368
+ request = true
284
369
  } = {}) {
285
370
  if (_reactNative.Platform.OS === 'android') {
286
- return Promise.resolve('unknown');
371
+ if (!RNZeroconf.checkLocalNetworkAccess) {
372
+ return 'unknown';
373
+ }
374
+ const status = await asPromise(RNZeroconf.checkLocalNetworkAccess());
375
+ if (status !== 'denied' || !request) {
376
+ return status;
377
+ }
378
+ const result = await _reactNative.PermissionsAndroid.request(ACCESS_LOCAL_NETWORK);
379
+ return result === _reactNative.PermissionsAndroid.RESULTS.GRANTED ? 'granted' : 'denied';
287
380
  }
288
381
  const serviceType = type ? `_${type}._${protocol}` : null;
289
382
  return asPromise(RNZeroconf.checkLocalNetworkAccess(serviceType, timeout));
@@ -304,7 +397,9 @@ class Zeroconf extends _events.EventEmitter {
304
397
  name,
305
398
  port,
306
399
  txt,
307
- implType
400
+ implType,
401
+ subtypes,
402
+ networkInterface
308
403
  } = withDefaults(PUBLISH_DEFAULTS, isOptionsObject(options) ? options : {
309
404
  type: options,
310
405
  protocol: protocolArg,
@@ -315,11 +410,59 @@ class Zeroconf extends _events.EventEmitter {
315
410
  implType: implTypeArg
316
411
  });
317
412
  const txtRecord = toTxtPairs(txt);
413
+ const native = nativeOptions({
414
+ subtypes,
415
+ networkInterface
416
+ });
318
417
  if (_reactNative.Platform.OS === 'android') {
319
418
  this._publishedImplTypes[name] = implType;
320
- return asPromise(RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, implType));
419
+ return asPromise(RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, implType, native));
420
+ }
421
+ return asPromise(RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord, native));
422
+ }
423
+
424
+ /**
425
+ * Replace the TXT record of a published service, resolves with the updated service.
426
+ * Android NSD has no update, the service is published again under the same name.
427
+ *
428
+ * updateService(name, { txt })
429
+ */
430
+ updateService(name, {
431
+ txt = {},
432
+ implType = this._publishedImplTypes[name] || ImplType.NSD
433
+ } = {}) {
434
+ const txtRecord = toTxtPairs(txt);
435
+ if (_reactNative.Platform.OS === 'android') {
436
+ return asPromise(RNZeroconf.updateService(name, txtRecord, implType));
321
437
  }
322
- return asPromise(RNZeroconf.registerService(type, protocol, domain, name, port, txtRecord));
438
+ return asPromise(RNZeroconf.updateService(name, txtRecord));
439
+ }
440
+
441
+ /**
442
+ * Resolve one service by name without scanning, for a device found before.
443
+ * Resolves with the service, rejects with code 'TIMEOUT' when it doesn't answer in time.
444
+ *
445
+ * resolveService({ name, type, protocol, domain, implType, timeout, networkInterface })
446
+ */
447
+ resolveService(options) {
448
+ const {
449
+ name,
450
+ type,
451
+ protocol,
452
+ domain,
453
+ implType,
454
+ timeout,
455
+ networkInterface
456
+ } = withDefaults({
457
+ ...SCAN_DEFAULTS,
458
+ timeout: 5
459
+ }, options);
460
+ const native = nativeOptions({
461
+ timeout,
462
+ networkInterface
463
+ });
464
+ const promise = _reactNative.Platform.OS === 'android' ? RNZeroconf.resolveService(name, type, protocol, domain, implType, native) : RNZeroconf.resolveService(name, type, protocol, domain, native);
465
+ return asPromise(asPromise(promise).then(service => service && withAddressFamilies(service)));
323
466
  }
324
467
 
325
468
  /**
@@ -335,34 +478,20 @@ class Zeroconf extends _events.EventEmitter {
335
478
  }
336
479
  }
337
480
 
338
- /**
339
- * Scans while mounted and returns the resolved services.
340
- * Scans again when the options change, stops and cleans up on unmount.
341
- * Each hook runs its own scan, several can run at once.
342
- *
343
- * const { services, isScanning, error, stop, restart } = useZeroconf({ type: 'http' })
344
- */
481
+ // Runs a scan while mounted and enabled, and again when deps change or on restart
345
482
  exports.default = Zeroconf;
346
- function useZeroconf(options = {}) {
347
- const {
348
- type,
349
- protocol,
350
- domain,
351
- implType,
352
- resolveTimeout,
353
- enabled = true
354
- } = options;
483
+ function useScan(enabled, startScan, read, deps) {
355
484
  const zeroconfRef = (0, _react.useRef)(null);
356
- const [services, setServices] = (0, _react.useState)([]);
485
+ const [items, setItems] = (0, _react.useState)([]);
357
486
  const [isScanning, setIsScanning] = (0, _react.useState)(false);
358
487
  const [error, setError] = (0, _react.useState)(null);
359
488
  const [scanCount, setScanCount] = (0, _react.useState)(0);
489
+ const readRef = (0, _react.useRef)(read);
490
+ readRef.current = read;
360
491
  (0, _react.useEffect)(() => {
361
492
  const zeroconf = new Zeroconf();
362
493
  zeroconfRef.current = zeroconf;
363
- // Found services only have a name until they are resolved
364
- const updateServices = () => setServices(Object.values(zeroconf.getServices()).filter(service => service.addresses));
365
- const unsubscribes = [zeroconf.subscribe('start', () => setIsScanning(true)), zeroconf.subscribe('stop', () => setIsScanning(false)), zeroconf.subscribe('update', updateServices), zeroconf.subscribe('error', setError)];
494
+ const unsubscribes = [zeroconf.subscribe('start', () => setIsScanning(true)), zeroconf.subscribe('stop', () => setIsScanning(false)), zeroconf.subscribe('update', () => setItems(readRef.current(zeroconf))), zeroconf.subscribe('error', setError)];
366
495
  return () => {
367
496
  unsubscribes.forEach(unsubscribe => unsubscribe());
368
497
  zeroconf.stop();
@@ -375,17 +504,11 @@ function useZeroconf(options = {}) {
375
504
  if (!zeroconf || !enabled) {
376
505
  return undefined;
377
506
  }
378
- setServices([]);
507
+ setItems([]);
379
508
  setError(null);
380
- zeroconf.scan({
381
- type,
382
- protocol,
383
- domain,
384
- implType,
385
- resolveTimeout
386
- });
509
+ startScan(zeroconf);
387
510
  return () => zeroconf.stop();
388
- }, [enabled, type, protocol, domain, implType, resolveTimeout, scanCount]);
511
+ }, [enabled, scanCount, ...deps]);
389
512
  const stop = (0, _react.useCallback)(() => {
390
513
  if (zeroconfRef.current) {
391
514
  zeroconfRef.current.stop();
@@ -393,10 +516,74 @@ function useZeroconf(options = {}) {
393
516
  }, []);
394
517
  const restart = (0, _react.useCallback)(() => setScanCount(count => count + 1), []);
395
518
  return {
396
- services,
519
+ items,
397
520
  isScanning,
398
521
  error,
399
522
  stop,
400
523
  restart
401
524
  };
402
525
  }
526
+
527
+ /**
528
+ * Scans while mounted and returns the resolved services.
529
+ * Scans again when the options change, stops and cleans up on unmount.
530
+ * Each hook runs its own scan, several can run at once.
531
+ *
532
+ * const { services, isScanning, error, stop, restart } = useZeroconf({ type: 'http' })
533
+ */
534
+ function useZeroconf(options = {}) {
535
+ const {
536
+ type,
537
+ protocol,
538
+ domain,
539
+ implType,
540
+ resolveTimeout,
541
+ subtype,
542
+ networkInterface,
543
+ enabled = true
544
+ } = options;
545
+ const {
546
+ items,
547
+ ...scan
548
+ } = useScan(enabled, zeroconf => zeroconf.scan({
549
+ type,
550
+ protocol,
551
+ domain,
552
+ implType,
553
+ resolveTimeout,
554
+ subtype,
555
+ networkInterface
556
+ }),
557
+ // Found services only have a name until they are resolved
558
+ zeroconf => Object.values(zeroconf.getServices()).filter(service => service.addresses), [type, protocol, domain, implType, resolveTimeout, subtype, networkInterface]);
559
+ return {
560
+ services: items,
561
+ ...scan
562
+ };
563
+ }
564
+
565
+ /**
566
+ * Lists the service types advertised on the network while mounted.
567
+ *
568
+ * const { serviceTypes, isScanning, error, stop, restart } = useServiceTypes()
569
+ */
570
+ function useServiceTypes(options = {}) {
571
+ const {
572
+ domain,
573
+ implType,
574
+ networkInterface,
575
+ enabled = true
576
+ } = options;
577
+ const {
578
+ items,
579
+ ...scan
580
+ } = useScan(enabled, zeroconf => zeroconf.scanServiceTypes({
581
+ domain,
582
+ implType,
583
+ networkInterface
584
+ }), zeroconf => zeroconf.getServiceTypes(), [domain, implType, networkInterface]);
585
+ return {
586
+ serviceTypes: items,
587
+ ...scan
588
+ };
589
+ }
package/index.d.ts CHANGED
@@ -36,8 +36,18 @@ export interface ScanOptions {
36
36
  domain?: string
37
37
  /** Android only, defaults to `NSD` */
38
38
  implType?: ImplType
39
- /** iOS only, seconds to try resolving a service before retrying once and giving up. Defaults to `5` */
39
+ /**
40
+ * Seconds to try resolving each service before retrying once, then emitting an `error` with code `'TIMEOUT'`.
41
+ * Defaults to `5`. Android `NSD` before Android 14 can't retry a resolve, the error comes after twice this time
42
+ */
40
43
  resolveTimeout?: number
44
+ /** Only find services registered with this subtype, e.g. `'printer'` */
45
+ subtype?: string
46
+ /**
47
+ * Network interface to scan on, e.g. `'en0'` on iOS or `'wlan0'` on Android. All of them by default.
48
+ * Android `NSD` needs Android 13 or later for it
49
+ */
50
+ networkInterface?: string
41
51
  }
42
52
 
43
53
  export interface PublishOptions {
@@ -53,6 +63,34 @@ export interface PublishOptions {
53
63
  txt?: TxtRecord
54
64
  /** Android only, defaults to `NSD` */
55
65
  implType?: ImplType
66
+ /** Subtypes to register the service with, e.g. `['printer']` */
67
+ subtypes?: string[]
68
+ /** Network interface to publish on, all of them by default. Android `NSD` needs Android 13 or later for it */
69
+ networkInterface?: string
70
+ }
71
+
72
+ export interface UpdateOptions {
73
+ /** The new TXT record, replaces the current one */
74
+ txt?: TxtRecord
75
+ /** Android only, defaults to the implementation the service was published with */
76
+ implType?: ImplType
77
+ }
78
+
79
+ export interface ResolveOptions {
80
+ /** Name of the service */
81
+ name: string
82
+ /** Service type without underscore, e.g. `'http'`. Defaults to `'http'` */
83
+ type?: string
84
+ /** Defaults to `'tcp'` */
85
+ protocol?: string
86
+ /** Defaults to `'local.'` */
87
+ domain?: string
88
+ /** Android only, defaults to `NSD` */
89
+ implType?: ImplType
90
+ /** Seconds before rejecting with code `'TIMEOUT'`, defaults to `5` */
91
+ timeout?: number
92
+ /** Network interface to resolve on, all of them by default */
93
+ networkInterface?: string
56
94
  }
57
95
 
58
96
  /**
@@ -61,11 +99,12 @@ export interface PublishOptions {
61
99
  * - `DNSSD`: DNSServiceErrorType, on iOS and from Android's embedded mDNSResponder,
62
100
  * e.g. `-65555` type missing from NSBonjourServices, `-65570` Local Network access denied
63
101
  * - `NsdManager`: Android NsdManager failure code
64
- * - `RNZeroconf`: the library's own errors, e.g. `'EXCEPTION'`
102
+ * - `Windows`: Win32 / DNS_STATUS code from the Windows DNS-SD functions
103
+ * - `RNZeroconf`: the library's own errors, e.g. `'EXCEPTION'`, `'TIMEOUT'`, `'UNKNOWN_INTERFACE'`
65
104
  */
66
105
  export interface ZeroconfError extends Error {
67
106
  code: number | string
68
- domain: 'DNSSD' | 'NsdManager' | 'RNZeroconf'
107
+ domain: 'DNSSD' | 'NsdManager' | 'RNZeroconf' | 'Windows'
69
108
  /** The service the error is about, when there is one */
70
109
  serviceName?: string
71
110
  }
@@ -75,8 +114,10 @@ export interface LocalNetworkAccessOptions {
75
114
  type?: string
76
115
  /** Defaults to `'tcp'` */
77
116
  protocol?: string
78
- /** Seconds to wait for an answer, defaults to `5` */
117
+ /** iOS: seconds to wait for an answer, defaults to `5` */
79
118
  timeout?: number
119
+ /** Android: request `ACCESS_LOCAL_NETWORK` when it is missing, defaults to `true` */
120
+ request?: boolean
80
121
  }
81
122
 
82
123
  /** Published services don't carry the address family fields */
@@ -91,13 +132,32 @@ export interface ZeroconfEvents {
91
132
  resolved: (service: Service) => void
92
133
  /** Service removed from the network */
93
134
  remove: (name: string) => void
94
- /** Services list changed */
135
+ /** Services or service types list changed */
95
136
  update: () => void
137
+ /** Service type found by `scanServiceTypes` */
138
+ typeFound: (serviceType: ServiceType) => void
139
+ /** Service type no longer advertised, during `scanServiceTypes` */
140
+ typeRemove: (serviceType: ServiceType) => void
96
141
  error: (error: ZeroconfError) => void
97
142
  published: (service: PublishedService) => void
98
143
  unpublished: (service: PublishedService) => void
99
144
  }
100
145
 
146
+ /** A service type advertised on the network, e.g. `{ type: 'http', protocol: 'tcp' }` for `_http._tcp` */
147
+ export interface ServiceType {
148
+ type: string
149
+ protocol: 'tcp' | 'udp'
150
+ }
151
+
152
+ export interface ServiceTypesScanOptions {
153
+ /** Defaults to `local.` */
154
+ domain?: string
155
+ /** Android only, defaults to `ImplType.NSD`. With `NSD`, services other apps publish on the same phone are left out (this app's own are included), `DNSSD` includes them */
156
+ implType?: ImplType
157
+ /** Network interface to list the types on, all of them by default */
158
+ networkInterface?: string
159
+ }
160
+
101
161
  /** Extends the `events` package EventEmitter */
102
162
  export default class Zeroconf {
103
163
  constructor()
@@ -121,14 +181,24 @@ export default class Zeroconf {
121
181
  /** All the services found so far, keyed by name */
122
182
  getServices(): Record<string, Service>
123
183
 
184
+ /** Service types found by `scanServiceTypes` */
185
+ getServiceTypes(): ServiceType[]
186
+
187
+ /**
188
+ * Scan for the service types advertised on the network, emits `typeFound` and `typeRemove`.
189
+ * On iOS it requires the `com.apple.developer.networking.multicast` entitlement.
190
+ */
191
+ scanServiceTypes(options?: ServiceTypesScanOptions): void
192
+
124
193
  /** Scan for services, defaults to `_http._tcp.` on the `local.` domain */
125
194
  scan(options?: ScanOptions): void
126
195
  /** @deprecated Use `scan({ type, protocol, domain, implType })` */
127
196
  scan(type?: string, protocol?: string, domain?: string, implType?: ImplType): void
128
197
 
129
198
  /**
130
- * Checks the iOS Local Network permission. Can show the permission prompt when it hasn't been answered yet.
131
- * Resolves `'unknown'` on Android, which has no equivalent permission to check.
199
+ * Checks the Local Network permission. iOS can show the permission prompt when it hasn't been answered yet.
200
+ * Android 17 (API 37): apps targeting API 37 need `ACCESS_LOCAL_NETWORK`, requested when missing unless
201
+ * `request` is `false`. Resolves `'granted'` when there is nothing to grant.
132
202
  */
133
203
  checkLocalNetworkAccess(
134
204
  options?: LocalNetworkAccessOptions,
@@ -162,6 +232,15 @@ export default class Zeroconf {
162
232
  * @param implType Android only, defaults to the implementation the service was published with
163
233
  */
164
234
  unpublishService(name: string, implType?: ImplType): Promise<PublishedService | null>
235
+
236
+ /**
237
+ * Replace the TXT record of a published service, resolves with the updated service.
238
+ * Android `NSD` has no update: the service is published again under the same name.
239
+ */
240
+ updateService(name: string, options: UpdateOptions): Promise<PublishedService>
241
+
242
+ /** Resolve one service by name without scanning. Rejects with code `'TIMEOUT'` when it doesn't answer in time */
243
+ resolveService(options: ResolveOptions): Promise<Service>
165
244
  }
166
245
 
167
246
  export interface UseZeroconfOptions extends ScanOptions {
@@ -185,3 +264,22 @@ export interface UseZeroconfResult {
185
264
  * stops and cleans up on unmount. Each hook runs its own scan, several can run at once.
186
265
  */
187
266
  export function useZeroconf(options?: UseZeroconfOptions): UseZeroconfResult
267
+
268
+ export interface UseServiceTypesOptions extends ServiceTypesScanOptions {
269
+ /** Scan only while true, defaults to `true` */
270
+ enabled?: boolean
271
+ }
272
+
273
+ export interface UseServiceTypesResult {
274
+ /** Service types advertised on the network, updated as they appear and disappear */
275
+ serviceTypes: ServiceType[]
276
+ isScanning: boolean
277
+ /** Last error, reset when a new scan starts */
278
+ error: ZeroconfError | null
279
+ stop(): void
280
+ /** Clears the service types and scans again */
281
+ restart(): void
282
+ }
283
+
284
+ /** Lists the service types advertised on the network while mounted */
285
+ export function useServiceTypes(options?: UseServiceTypesOptions): UseServiceTypesResult