@cappitolian/http-local-server-swifter 0.0.19 → 0.0.20

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.
@@ -9,10 +9,12 @@ import UIKit
9
9
  /// server.start() will always succeed regardless of permission status.
10
10
  ///
11
11
  /// Detection strategy:
12
- /// - Trigger the system dialog by starting a NWBrowser.
13
- /// - Listen to the browser's state change callback to detect denial.
14
- /// - Surface the result via a completion handler so the plugin can
15
- /// reject the Capacitor call with LOCAL_NETWORK_PERMISSION_DENIED.
12
+ /// - Send a UDP probe to the mDNS multicast address (224.0.0.251:5353).
13
+ /// - This triggers the system permission dialog without requiring any
14
+ /// NSBonjourServices entry in Info.plist.
15
+ /// - NWBrowser was avoided because it requires the browsed service type
16
+ /// to be declared in NSBonjourServices, causing false NoAuth(-65555)
17
+ /// failures even when the permission is actually granted.
16
18
  @objc public class LocalNetworkPermission: NSObject {
17
19
 
18
20
  // MARK: - Types
@@ -25,76 +27,83 @@ import UIKit
25
27
 
26
28
  // MARK: - Private state
27
29
 
28
- private var browser: NWBrowser?
30
+ private var connection: NWConnection?
29
31
 
30
32
  // MARK: - Permission check
31
33
 
32
- /// Triggers the Local Network permission dialog and returns the result
33
- /// asynchronously via the completion handler.
34
+ /// Probes the local network to trigger the permission dialog and detect the result.
35
+ ///
36
+ /// Sends a UDP packet to the mDNS multicast address. iOS intercepts this at the
37
+ /// network layer and either allows it (granted) or blocks it with a policy error (denied).
34
38
  ///
35
39
  /// - Parameter completion: Called on the main thread with the permission status.
36
- /// Will be called with `.granted` or `.denied` once iOS responds,
37
- /// or `.unknown` if the result could not be determined within the timeout.
40
+ /// `.granted` / `.denied` once iOS responds, or `.unknown` on timeout
41
+ /// (simulator, permission already granted and cached by the OS, etc.).
38
42
  ///
39
- /// - Note: iOS shows the dialog only once. On subsequent calls this method
40
- /// will return the previously stored decision immediately (no dialog shown).
43
+ /// - Note: iOS shows the dialog only once. Subsequent calls return the cached decision.
41
44
  public func checkPermission(completion: @escaping (PermissionStatus) -> Void) {
42
- let parameters = NWParameters()
43
- parameters.includePeerToPeer = true
45
+ // mDNS multicast address — probing this triggers the Local Network permission dialog.
46
+ // This approach does NOT require NSBonjourServices in Info.plist.
47
+ let host = NWEndpoint.Host("224.0.0.251")
48
+ let port = NWEndpoint.Port(rawValue: 5353)!
44
49
 
45
- let browser = NWBrowser(
46
- for: .bonjour(type: "_http._tcp", domain: "local."),
47
- using: parameters
48
- )
49
- self.browser = browser
50
+ let conn = NWConnection(host: host, port: port, using: .udp)
51
+ self.connection = conn
50
52
 
51
- // Timeout fallback — if iOS doesn't respond in 3 seconds, assume unknown
52
- // (e.g. on simulator or when the user has already granted permission)
53
53
  var completed = false
54
- let timeout = DispatchWorkItem {
54
+
55
+ // Timeout fallback — treat unresolved state as unknown and allow server startup.
56
+ // This covers: simulator, permission already granted (OS may skip the callback),
57
+ // and edge cases where iOS doesn't fire stateUpdateHandler promptly.
58
+ let timeout = DispatchWorkItem { [weak self] in
55
59
  guard !completed else { return }
56
60
  completed = true
57
- browser.cancel()
61
+ self?.connection?.cancel()
62
+ self?.connection = nil
58
63
  DispatchQueue.main.async { completion(.unknown) }
59
64
  }
60
65
  DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: timeout)
61
66
 
62
- browser.stateUpdateHandler = { state in
67
+ conn.stateUpdateHandler = { [weak self] state in
63
68
  guard !completed else { return }
64
69
 
65
70
  switch state {
66
71
  case .ready:
67
- // Browser started successfully — permission is granted
72
+ // UDP connection reached the network layer — permission is granted.
68
73
  completed = true
69
74
  timeout.cancel()
70
- browser.cancel()
75
+ self?.connection?.cancel()
76
+ self?.connection = nil
71
77
  DispatchQueue.main.async { completion(.granted) }
72
78
 
73
- case .failed(let error):
74
- // Check for policy denial errors:
75
- // kDNSServiceErr_PolicyDenied = -65570
76
- // kDNSServiceErr_NoAuth = -65555
77
- let nsError = error as NSError
78
- let isDenied = nsError.code == -65570
79
- || nsError.code == -65555
80
- || error.localizedDescription.lowercased().contains("policy")
79
+ case .waiting(let error):
80
+ // .waiting with a policy error = permission denied.
81
+ // Other .waiting errors (no route, offline) are transient — keep waiting.
82
+ if Self.isPolicyDenied(error) {
83
+ completed = true
84
+ timeout.cancel()
85
+ self?.connection?.cancel()
86
+ self?.connection = nil
87
+ DispatchQueue.main.async { completion(.denied) }
88
+ }
81
89
 
90
+ case .failed(let error):
91
+ let status: PermissionStatus = Self.isPolicyDenied(error) ? .denied : .unknown
82
92
  completed = true
83
93
  timeout.cancel()
84
- browser.cancel()
85
- DispatchQueue.main.async {
86
- completion(isDenied ? .denied : .unknown)
87
- }
94
+ self?.connection?.cancel()
95
+ self?.connection = nil
96
+ DispatchQueue.main.async { completion(status) }
88
97
 
89
98
  case .cancelled:
90
- break // Expected when we cancel it ourselves
99
+ break // Expected we cancelled it ourselves above
91
100
 
92
101
  default:
93
- break // .setup, .waiting — keep waiting
102
+ break // .setup, .preparing — keep waiting
94
103
  }
95
104
  }
96
105
 
97
- browser.start(queue: .main)
106
+ conn.start(queue: .main)
98
107
  }
99
108
 
100
109
  // MARK: - Recovery
@@ -111,4 +120,19 @@ import UIKit
111
120
  UIApplication.shared.open(url)
112
121
  }
113
122
  }
123
+
124
+ // MARK: - Private helpers
125
+
126
+ /// Returns true if the NWError indicates a Local Network policy denial.
127
+ ///
128
+ /// Known denial error codes:
129
+ /// - kDNSServiceErr_PolicyDenied = -65570
130
+ /// - kDNSServiceErr_NoAuth = -65555
131
+ private static func isPolicyDenied(_ error: NWError) -> Bool {
132
+ let nsError = error as NSError
133
+ if nsError.code == -65570 || nsError.code == -65555 { return true }
134
+
135
+ let desc = error.localizedDescription.lowercased()
136
+ return desc.contains("policy") || desc.contains("denied") || desc.contains("noauth")
137
+ }
114
138
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cappitolian/http-local-server-swifter",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "description": "Runs a local HTTP server on your device, accessible over LAN. Supports connect, disconnect, GET, and POST methods with IP and port discovery.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",