@cappitolian/http-local-server-swifter 0.0.24 → 0.0.26

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/dist/docs.json CHANGED
@@ -39,27 +39,12 @@
39
39
  ],
40
40
  "returns": "Promise<void>",
41
41
  "tags": [],
42
- "docs": "Sends a response back to the client.\nSupports status and headers to handle CORS Preflight correctly.",
42
+ "docs": "Sends a response back to the client.\nNow supports status and headers to handle CORS Preflight correctly.",
43
43
  "complexTypes": [
44
44
  "HttpSendResponseOptions"
45
45
  ],
46
46
  "slug": "sendresponse"
47
47
  },
48
- {
49
- "name": "openSettings",
50
- "signature": "() => Promise<void>",
51
- "parameters": [],
52
- "returns": "Promise<void>",
53
- "tags": [
54
- {
55
- "name": "example",
56
- "text": "try {\n await HttpLocalServerSwifter.connect();\n} catch (error) {\n if (error.message === 'LOCAL_NETWORK_PERMISSION_DENIED') {\n await HttpLocalServerSwifter.openSettings();\n }\n}"
57
- }
58
- ],
59
- "docs": "Opens the app's page in iOS Settings so the user can manually grant\nLocal Network permission after having denied it.\n\nOnly relevant on iOS — on Android this is a no-op.",
60
- "complexTypes": [],
61
- "slug": "opensettings"
62
- },
63
48
  {
64
49
  "name": "addListener",
65
50
  "signature": "(eventName: 'onRequest', listenerFunc: (data: HttpRequestData) => void | Promise<void>) => Promise<PluginListenerHandle>",
@@ -145,14 +130,14 @@
145
130
  {
146
131
  "name": "status",
147
132
  "tags": [],
148
- "docs": "HTTP Status code (e.g., 200, 204, 404).\nDefault is 200.",
133
+ "docs": "* NEW: HTTP Status code (e.g., 200, 204, 404). \nDefault is 200.",
149
134
  "complexTypes": [],
150
135
  "type": "number | undefined"
151
136
  },
152
137
  {
153
138
  "name": "headers",
154
139
  "tags": [],
155
- "docs": "Custom HTTP headers.\nCrucial for fixing CORS by providing 'Access-Control-Allow-Origin'.",
140
+ "docs": "* NEW: Custom HTTP headers.\nCrucial for fixing CORS by providing 'Access-Control-Allow-Origin'.",
156
141
  "complexTypes": [
157
142
  "Record"
158
143
  ],
@@ -20,13 +20,11 @@ export interface HttpSendResponseOptions {
20
20
  requestId: string;
21
21
  /** The response body (usually stringified JSON) */
22
22
  body: string;
23
- /**
24
- * HTTP Status code (e.g., 200, 204, 404).
23
+ /** * NEW: HTTP Status code (e.g., 200, 204, 404).
25
24
  * Default is 200.
26
25
  */
27
26
  status?: number;
28
- /**
29
- * Custom HTTP headers.
27
+ /** * NEW: Custom HTTP headers.
30
28
  * Crucial for fixing CORS by providing 'Access-Control-Allow-Origin'.
31
29
  */
32
30
  headers?: Record<string, string>;
@@ -36,25 +34,9 @@ export interface HttpLocalServerSwifterPlugin {
36
34
  disconnect(): Promise<void>;
37
35
  /**
38
36
  * Sends a response back to the client.
39
- * Supports status and headers to handle CORS Preflight correctly.
37
+ * Now supports status and headers to handle CORS Preflight correctly.
40
38
  */
41
39
  sendResponse(options: HttpSendResponseOptions): Promise<void>;
42
- /**
43
- * Opens the app's page in iOS Settings so the user can manually grant
44
- * Local Network permission after having denied it.
45
- *
46
- * Only relevant on iOS — on Android this is a no-op.
47
- *
48
- * @example
49
- * try {
50
- * await HttpLocalServerSwifter.connect();
51
- * } catch (error) {
52
- * if (error.message === 'LOCAL_NETWORK_PERMISSION_DENIED') {
53
- * await HttpLocalServerSwifter.openSettings();
54
- * }
55
- * }
56
- */
57
- openSettings(): Promise<void>;
58
40
  addListener(eventName: 'onRequest', listenerFunc: (data: HttpRequestData) => void | Promise<void>): Promise<PluginListenerHandle>;
59
41
  removeAllListeners(): Promise<void>;
60
42
  }
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\nexport interface HttpConnectResult {\n ip: string;\n port: number;\n}\n\nexport interface HttpRequestData {\n requestId: string;\n method: string;\n path: string;\n body?: string;\n headers?: Record<string, string>;\n query?: Record<string, string>;\n}\n\n/**\n * Options for sending an HTTP response.\n * Updated to support custom status codes and headers for CORS.\n */\nexport interface HttpSendResponseOptions {\n /** The ID received in the 'onRequest' event */\n requestId: string;\n\n /** The response body (usually stringified JSON) */\n body: string;\n\n /**\n * HTTP Status code (e.g., 200, 204, 404).\n * Default is 200.\n */\n status?: number;\n\n /**\n * Custom HTTP headers.\n * Crucial for fixing CORS by providing 'Access-Control-Allow-Origin'.\n */\n headers?: Record<string, string>;\n}\n\nexport interface HttpLocalServerSwifterPlugin {\n connect(): Promise<HttpConnectResult>;\n disconnect(): Promise<void>;\n\n /**\n * Sends a response back to the client.\n * Supports status and headers to handle CORS Preflight correctly.\n */\n sendResponse(options: HttpSendResponseOptions): Promise<void>;\n\n /**\n * Opens the app's page in iOS Settings so the user can manually grant\n * Local Network permission after having denied it.\n *\n * Only relevant on iOS — on Android this is a no-op.\n *\n * @example\n * try {\n * await HttpLocalServerSwifter.connect();\n * } catch (error) {\n * if (error.message === 'LOCAL_NETWORK_PERMISSION_DENIED') {\n * await HttpLocalServerSwifter.openSettings();\n * }\n * }\n */\n openSettings(): Promise<void>;\n\n addListener(\n eventName: 'onRequest',\n listenerFunc: (data: HttpRequestData) => void | Promise<void>\n ): Promise<PluginListenerHandle>;\n\n removeAllListeners(): Promise<void>;\n}"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\nexport interface HttpConnectResult {\n ip: string;\n port: number;\n}\n\nexport interface HttpRequestData {\n requestId: string;\n method: string;\n path: string;\n body?: string;\n headers?: Record<string, string>;\n query?: Record<string, string>;\n}\n\n/**\n * Options for sending an HTTP response.\n * Updated to support custom status codes and headers for CORS.\n */\nexport interface HttpSendResponseOptions {\n /** The ID received in the 'onRequest' event */\n requestId: string;\n\n /** The response body (usually stringified JSON) */\n body: string;\n\n /** * NEW: HTTP Status code (e.g., 200, 204, 404). \n * Default is 200.\n */\n status?: number;\n\n /** * NEW: Custom HTTP headers.\n * Crucial for fixing CORS by providing 'Access-Control-Allow-Origin'.\n */\n headers?: Record<string, string>;\n}\n\nexport interface HttpLocalServerSwifterPlugin {\n connect(): Promise<HttpConnectResult>;\n disconnect(): Promise<void>;\n\n /**\n * Sends a response back to the client.\n * Now supports status and headers to handle CORS Preflight correctly.\n */\n sendResponse(options: HttpSendResponseOptions): Promise<void>;\n\n addListener(\n eventName: 'onRequest',\n listenerFunc: (data: HttpRequestData) => void | Promise<void>\n ): Promise<PluginListenerHandle>;\n\n removeAllListeners(): Promise<void>;\n}"]}
package/dist/esm/web.d.ts CHANGED
@@ -5,8 +5,4 @@ export declare class HttpLocalServerSwifterWeb extends WebPlugin implements Http
5
5
  connect(): Promise<HttpConnectResult>;
6
6
  disconnect(): Promise<void>;
7
7
  sendResponse(options: HttpSendResponseOptions): Promise<void>;
8
- /**
9
- * No-op on web and Android — Local Network permission dialogs only exist on iOS.
10
- */
11
- openSettings(): Promise<void>;
12
8
  }
package/dist/esm/web.js CHANGED
@@ -17,13 +17,7 @@ export class HttpLocalServerSwifterWeb extends WebPlugin {
17
17
  if (!this.isRunning)
18
18
  throw new Error('Server not running');
19
19
  const { requestId, body, status, headers } = options;
20
- console.log('[HttpLocalServerSwifter Web] Mock Response:', { requestId, status: status !== null && status !== void 0 ? status : 200, headers: headers !== null && headers !== void 0 ? headers : {}, bodyLength: body.length });
21
- }
22
- /**
23
- * No-op on web and Android — Local Network permission dialogs only exist on iOS.
24
- */
25
- async openSettings() {
26
- console.warn('[HttpLocalServerSwifter Web] openSettings() is only available on iOS.');
20
+ console.log(`[HttpLocalServerSwifter Web] Mock Response:`, { requestId, status: status !== null && status !== void 0 ? status : 200, headers: headers !== null && headers !== void 0 ? headers : {}, bodyLength: body.length });
27
21
  }
28
22
  }
29
23
  //# sourceMappingURL=web.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAO5C,MAAM,OAAO,yBAA0B,SAAQ,SAAS;IAAxD;;QACU,cAAS,GAAG,KAAK,CAAC;IA4B5B,CAAC;IA1BC,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,OAAO,CAAC,IAAI,CAAC,oEAAoE,CAAC,CAAC;QACnF,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,UAAU;QACd,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC,CAAC;IACnE,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,OAAgC;QACjD,IAAI,CAAC,IAAI,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;QAC3D,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QACrD,OAAO,CAAC,GAAG,CACT,6CAA6C,EAC7C,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,aAAN,MAAM,cAAN,MAAM,GAAI,GAAG,EAAE,OAAO,EAAE,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,CACtF,CAAC;IACJ,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,YAAY;QAChB,OAAO,CAAC,IAAI,CAAC,uEAAuE,CAAC,CAAC;IACxF,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\nimport type {\n HttpLocalServerSwifterPlugin,\n HttpConnectResult,\n HttpSendResponseOptions\n} from './definitions';\n\nexport class HttpLocalServerSwifterWeb extends WebPlugin implements HttpLocalServerSwifterPlugin {\n private isRunning = false;\n\n async connect(): Promise<HttpConnectResult> {\n this.isRunning = true;\n console.warn('[HttpLocalServerSwifter Web] Mock server started at 127.0.0.1:8080');\n return { ip: '127.0.0.1', port: 8080 };\n }\n\n async disconnect(): Promise<void> {\n this.isRunning = false;\n console.log('[HttpLocalServerSwifter Web] Mock server stopped.');\n }\n\n async sendResponse(options: HttpSendResponseOptions): Promise<void> {\n if (!this.isRunning) throw new Error('Server not running');\n const { requestId, body, status, headers } = options;\n console.log(\n '[HttpLocalServerSwifter Web] Mock Response:',\n { requestId, status: status ?? 200, headers: headers ?? {}, bodyLength: body.length }\n );\n }\n\n /**\n * No-op on web and Android — Local Network permission dialogs only exist on iOS.\n */\n async openSettings(): Promise<void> {\n console.warn('[HttpLocalServerSwifter Web] openSettings() is only available on iOS.');\n }\n}"]}
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAO5C,MAAM,OAAO,yBAA0B,SAAQ,SAAS;IAAxD;;QACU,cAAS,GAAG,KAAK,CAAC;IAuB5B,CAAC;IArBC,KAAK,CAAC,OAAO;QACX,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,OAAO,CAAC,IAAI,CAAC,oEAAoE,CAAC,CAAC;QACnF,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,UAAU;QACd,IAAI,CAAC,SAAS,GAAG,KAAK,CAAC;QACvB,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC,CAAC;IACnE,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,OAAgC;QACjD,IAAI,CAAC,IAAI,CAAC,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;QAE3D,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAErD,OAAO,CAAC,GAAG,CACT,6CAA6C,EAC7C,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,aAAN,MAAM,cAAN,MAAM,GAAI,GAAG,EAAE,OAAO,EAAE,OAAO,aAAP,OAAO,cAAP,OAAO,GAAI,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,CACtF,CAAC;IACJ,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\nimport type {\n HttpLocalServerSwifterPlugin,\n HttpConnectResult,\n HttpSendResponseOptions\n} from './definitions';\n\nexport class HttpLocalServerSwifterWeb extends WebPlugin implements HttpLocalServerSwifterPlugin {\n private isRunning = false;\n\n async connect(): Promise<HttpConnectResult> {\n this.isRunning = true;\n console.warn('[HttpLocalServerSwifter Web] Mock server started at 127.0.0.1:8080');\n return { ip: '127.0.0.1', port: 8080 };\n }\n\n async disconnect(): Promise<void> {\n this.isRunning = false;\n console.log('[HttpLocalServerSwifter Web] Mock server stopped.');\n }\n\n async sendResponse(options: HttpSendResponseOptions): Promise<void> {\n if (!this.isRunning) throw new Error('Server not running');\n\n const { requestId, body, status, headers } = options;\n\n console.log(\n `[HttpLocalServerSwifter Web] Mock Response:`,\n { requestId, status: status ?? 200, headers: headers ?? {}, bodyLength: body.length }\n );\n }\n}"]}
@@ -31,13 +31,7 @@ class HttpLocalServerSwifterWeb extends core.WebPlugin {
31
31
  if (!this.isRunning)
32
32
  throw new Error('Server not running');
33
33
  const { requestId, body, status, headers } = options;
34
- console.log('[HttpLocalServerSwifter Web] Mock Response:', { requestId, status: status !== null && status !== void 0 ? status : 200, headers: headers !== null && headers !== void 0 ? headers : {}, bodyLength: body.length });
35
- }
36
- /**
37
- * No-op on web and Android — Local Network permission dialogs only exist on iOS.
38
- */
39
- async openSettings() {
40
- console.warn('[HttpLocalServerSwifter Web] openSettings() is only available on iOS.');
34
+ console.log(`[HttpLocalServerSwifter Web] Mock Response:`, { requestId, status: status !== null && status !== void 0 ? status : 200, headers: headers !== null && headers !== void 0 ? headers : {}, bodyLength: body.length });
41
35
  }
42
36
  }
43
37
 
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\n/**\n * Local HTTP server plugin for Android and iOS.\n * * Allows creating an HTTP server on the device that can receive\n * requests from other devices on the same local network or\n * from the app's own WebView (fixing CORS issues).\n */\nconst HttpLocalServerSwifter = registerPlugin('HttpLocalServerSwifter', {\n // We point to the web mock for browser development\n web: () => import('./web').then(m => new m.HttpLocalServerSwifterWeb()),\n});\n// Re-export everything from definitions so they are available to the app\nexport * from './definitions';\nexport { HttpLocalServerSwifter };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class HttpLocalServerSwifterWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.isRunning = false;\n }\n async connect() {\n this.isRunning = true;\n console.warn('[HttpLocalServerSwifter Web] Mock server started at 127.0.0.1:8080');\n return { ip: '127.0.0.1', port: 8080 };\n }\n async disconnect() {\n this.isRunning = false;\n console.log('[HttpLocalServerSwifter Web] Mock server stopped.');\n }\n async sendResponse(options) {\n if (!this.isRunning)\n throw new Error('Server not running');\n const { requestId, body, status, headers } = options;\n console.log('[HttpLocalServerSwifter Web] Mock Response:', { requestId, status: status !== null && status !== void 0 ? status : 200, headers: headers !== null && headers !== void 0 ? headers : {}, bodyLength: body.length });\n }\n /**\n * No-op on web and Android — Local Network permission dialogs only exist on iOS.\n */\n async openSettings() {\n console.warn('[HttpLocalServerSwifter Web] openSettings() is only available on iOS.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACA;AACA;AACA;AACA;AACA;AACA;AACK,MAAC,sBAAsB,GAAGA,mBAAc,CAAC,wBAAwB,EAAE;AACxE;AACA,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,yBAAyB,EAAE,CAAC;AAC3E,CAAC;;ACTM,MAAM,yBAAyB,SAASC,cAAS,CAAC;AACzD,IAAI,WAAW,GAAG;AAClB,QAAQ,KAAK,CAAC,GAAG,SAAS,CAAC;AAC3B,QAAQ,IAAI,CAAC,SAAS,GAAG,KAAK;AAC9B,IAAI;AACJ,IAAI,MAAM,OAAO,GAAG;AACpB,QAAQ,IAAI,CAAC,SAAS,GAAG,IAAI;AAC7B,QAAQ,OAAO,CAAC,IAAI,CAAC,oEAAoE,CAAC;AAC1F,QAAQ,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE;AAC9C,IAAI;AACJ,IAAI,MAAM,UAAU,GAAG;AACvB,QAAQ,IAAI,CAAC,SAAS,GAAG,KAAK;AAC9B,QAAQ,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC;AACxE,IAAI;AACJ,IAAI,MAAM,YAAY,CAAC,OAAO,EAAE;AAChC,QAAQ,IAAI,CAAC,IAAI,CAAC,SAAS;AAC3B,YAAY,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC;AACjD,QAAQ,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO;AAC5D,QAAQ,OAAO,CAAC,GAAG,CAAC,6CAA6C,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,MAAM,GAAG,MAAM,GAAG,GAAG,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,MAAM,GAAG,OAAO,GAAG,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;AACvO,IAAI;AACJ;AACA;AACA;AACA,IAAI,MAAM,YAAY,GAAG;AACzB,QAAQ,OAAO,CAAC,IAAI,CAAC,uEAAuE,CAAC;AAC7F,IAAI;AACJ;;;;;;;;;"}
1
+ {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\n/**\n * Local HTTP server plugin for Android and iOS.\n * * Allows creating an HTTP server on the device that can receive\n * requests from other devices on the same local network or\n * from the app's own WebView (fixing CORS issues).\n */\nconst HttpLocalServerSwifter = registerPlugin('HttpLocalServerSwifter', {\n // We point to the web mock for browser development\n web: () => import('./web').then(m => new m.HttpLocalServerSwifterWeb()),\n});\n// Re-export everything from definitions so they are available to the app\nexport * from './definitions';\nexport { HttpLocalServerSwifter };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class HttpLocalServerSwifterWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.isRunning = false;\n }\n async connect() {\n this.isRunning = true;\n console.warn('[HttpLocalServerSwifter Web] Mock server started at 127.0.0.1:8080');\n return { ip: '127.0.0.1', port: 8080 };\n }\n async disconnect() {\n this.isRunning = false;\n console.log('[HttpLocalServerSwifter Web] Mock server stopped.');\n }\n async sendResponse(options) {\n if (!this.isRunning)\n throw new Error('Server not running');\n const { requestId, body, status, headers } = options;\n console.log(`[HttpLocalServerSwifter Web] Mock Response:`, { requestId, status: status !== null && status !== void 0 ? status : 200, headers: headers !== null && headers !== void 0 ? headers : {}, bodyLength: body.length });\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACA;AACA;AACA;AACA;AACA;AACA;AACK,MAAC,sBAAsB,GAAGA,mBAAc,CAAC,wBAAwB,EAAE;AACxE;AACA,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,yBAAyB,EAAE,CAAC;AAC3E,CAAC;;ACTM,MAAM,yBAAyB,SAASC,cAAS,CAAC;AACzD,IAAI,WAAW,GAAG;AAClB,QAAQ,KAAK,CAAC,GAAG,SAAS,CAAC;AAC3B,QAAQ,IAAI,CAAC,SAAS,GAAG,KAAK;AAC9B,IAAI;AACJ,IAAI,MAAM,OAAO,GAAG;AACpB,QAAQ,IAAI,CAAC,SAAS,GAAG,IAAI;AAC7B,QAAQ,OAAO,CAAC,IAAI,CAAC,oEAAoE,CAAC;AAC1F,QAAQ,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE;AAC9C,IAAI;AACJ,IAAI,MAAM,UAAU,GAAG;AACvB,QAAQ,IAAI,CAAC,SAAS,GAAG,KAAK;AAC9B,QAAQ,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC;AACxE,IAAI;AACJ,IAAI,MAAM,YAAY,CAAC,OAAO,EAAE;AAChC,QAAQ,IAAI,CAAC,IAAI,CAAC,SAAS;AAC3B,YAAY,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC;AACjD,QAAQ,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO;AAC5D,QAAQ,OAAO,CAAC,GAAG,CAAC,CAAC,2CAA2C,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,MAAM,GAAG,MAAM,GAAG,GAAG,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,MAAM,GAAG,OAAO,GAAG,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;AACvO,IAAI;AACJ;;;;;;;;;"}
package/dist/plugin.js CHANGED
@@ -30,13 +30,7 @@ var capacitorHttpLocalServerSwifter = (function (exports, core) {
30
30
  if (!this.isRunning)
31
31
  throw new Error('Server not running');
32
32
  const { requestId, body, status, headers } = options;
33
- console.log('[HttpLocalServerSwifter Web] Mock Response:', { requestId, status: status !== null && status !== void 0 ? status : 200, headers: headers !== null && headers !== void 0 ? headers : {}, bodyLength: body.length });
34
- }
35
- /**
36
- * No-op on web and Android — Local Network permission dialogs only exist on iOS.
37
- */
38
- async openSettings() {
39
- console.warn('[HttpLocalServerSwifter Web] openSettings() is only available on iOS.');
33
+ console.log(`[HttpLocalServerSwifter Web] Mock Response:`, { requestId, status: status !== null && status !== void 0 ? status : 200, headers: headers !== null && headers !== void 0 ? headers : {}, bodyLength: body.length });
40
34
  }
41
35
  }
42
36
 
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\n/**\n * Local HTTP server plugin for Android and iOS.\n * * Allows creating an HTTP server on the device that can receive\n * requests from other devices on the same local network or\n * from the app's own WebView (fixing CORS issues).\n */\nconst HttpLocalServerSwifter = registerPlugin('HttpLocalServerSwifter', {\n // We point to the web mock for browser development\n web: () => import('./web').then(m => new m.HttpLocalServerSwifterWeb()),\n});\n// Re-export everything from definitions so they are available to the app\nexport * from './definitions';\nexport { HttpLocalServerSwifter };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class HttpLocalServerSwifterWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.isRunning = false;\n }\n async connect() {\n this.isRunning = true;\n console.warn('[HttpLocalServerSwifter Web] Mock server started at 127.0.0.1:8080');\n return { ip: '127.0.0.1', port: 8080 };\n }\n async disconnect() {\n this.isRunning = false;\n console.log('[HttpLocalServerSwifter Web] Mock server stopped.');\n }\n async sendResponse(options) {\n if (!this.isRunning)\n throw new Error('Server not running');\n const { requestId, body, status, headers } = options;\n console.log('[HttpLocalServerSwifter Web] Mock Response:', { requestId, status: status !== null && status !== void 0 ? status : 200, headers: headers !== null && headers !== void 0 ? headers : {}, bodyLength: body.length });\n }\n /**\n * No-op on web and Android — Local Network permission dialogs only exist on iOS.\n */\n async openSettings() {\n console.warn('[HttpLocalServerSwifter Web] openSettings() is only available on iOS.');\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;IACA;IACA;IACA;IACA;IACA;IACA;AACK,UAAC,sBAAsB,GAAGA,mBAAc,CAAC,wBAAwB,EAAE;IACxE;IACA,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,yBAAyB,EAAE,CAAC;IAC3E,CAAC;;ICTM,MAAM,yBAAyB,SAASC,cAAS,CAAC;IACzD,IAAI,WAAW,GAAG;IAClB,QAAQ,KAAK,CAAC,GAAG,SAAS,CAAC;IAC3B,QAAQ,IAAI,CAAC,SAAS,GAAG,KAAK;IAC9B,IAAI;IACJ,IAAI,MAAM,OAAO,GAAG;IACpB,QAAQ,IAAI,CAAC,SAAS,GAAG,IAAI;IAC7B,QAAQ,OAAO,CAAC,IAAI,CAAC,oEAAoE,CAAC;IAC1F,QAAQ,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE;IAC9C,IAAI;IACJ,IAAI,MAAM,UAAU,GAAG;IACvB,QAAQ,IAAI,CAAC,SAAS,GAAG,KAAK;IAC9B,QAAQ,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC;IACxE,IAAI;IACJ,IAAI,MAAM,YAAY,CAAC,OAAO,EAAE;IAChC,QAAQ,IAAI,CAAC,IAAI,CAAC,SAAS;IAC3B,YAAY,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC;IACjD,QAAQ,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO;IAC5D,QAAQ,OAAO,CAAC,GAAG,CAAC,6CAA6C,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,MAAM,GAAG,MAAM,GAAG,GAAG,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,MAAM,GAAG,OAAO,GAAG,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;IACvO,IAAI;IACJ;IACA;IACA;IACA,IAAI,MAAM,YAAY,GAAG;IACzB,QAAQ,OAAO,CAAC,IAAI,CAAC,uEAAuE,CAAC;IAC7F,IAAI;IACJ;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\n/**\n * Local HTTP server plugin for Android and iOS.\n * * Allows creating an HTTP server on the device that can receive\n * requests from other devices on the same local network or\n * from the app's own WebView (fixing CORS issues).\n */\nconst HttpLocalServerSwifter = registerPlugin('HttpLocalServerSwifter', {\n // We point to the web mock for browser development\n web: () => import('./web').then(m => new m.HttpLocalServerSwifterWeb()),\n});\n// Re-export everything from definitions so they are available to the app\nexport * from './definitions';\nexport { HttpLocalServerSwifter };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class HttpLocalServerSwifterWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.isRunning = false;\n }\n async connect() {\n this.isRunning = true;\n console.warn('[HttpLocalServerSwifter Web] Mock server started at 127.0.0.1:8080');\n return { ip: '127.0.0.1', port: 8080 };\n }\n async disconnect() {\n this.isRunning = false;\n console.log('[HttpLocalServerSwifter Web] Mock server stopped.');\n }\n async sendResponse(options) {\n if (!this.isRunning)\n throw new Error('Server not running');\n const { requestId, body, status, headers } = options;\n console.log(`[HttpLocalServerSwifter Web] Mock Response:`, { requestId, status: status !== null && status !== void 0 ? status : 200, headers: headers !== null && headers !== void 0 ? headers : {}, bodyLength: body.length });\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;IACA;IACA;IACA;IACA;IACA;IACA;AACK,UAAC,sBAAsB,GAAGA,mBAAc,CAAC,wBAAwB,EAAE;IACxE;IACA,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,yBAAyB,EAAE,CAAC;IAC3E,CAAC;;ICTM,MAAM,yBAAyB,SAASC,cAAS,CAAC;IACzD,IAAI,WAAW,GAAG;IAClB,QAAQ,KAAK,CAAC,GAAG,SAAS,CAAC;IAC3B,QAAQ,IAAI,CAAC,SAAS,GAAG,KAAK;IAC9B,IAAI;IACJ,IAAI,MAAM,OAAO,GAAG;IACpB,QAAQ,IAAI,CAAC,SAAS,GAAG,IAAI;IAC7B,QAAQ,OAAO,CAAC,IAAI,CAAC,oEAAoE,CAAC;IAC1F,QAAQ,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE;IAC9C,IAAI;IACJ,IAAI,MAAM,UAAU,GAAG;IACvB,QAAQ,IAAI,CAAC,SAAS,GAAG,KAAK;IAC9B,QAAQ,OAAO,CAAC,GAAG,CAAC,mDAAmD,CAAC;IACxE,IAAI;IACJ,IAAI,MAAM,YAAY,CAAC,OAAO,EAAE;IAChC,QAAQ,IAAI,CAAC,IAAI,CAAC,SAAS;IAC3B,YAAY,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC;IACjD,QAAQ,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO;IAC5D,QAAQ,OAAO,CAAC,GAAG,CAAC,CAAC,2CAA2C,CAAC,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,MAAM,GAAG,MAAM,GAAG,GAAG,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,MAAM,GAAG,OAAO,GAAG,EAAE,EAAE,UAAU,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;IACvO,IAAI;IACJ;;;;;;;;;;;;;;;"}
@@ -9,71 +9,55 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
9
9
  @objc public class HttpLocalServerSwifter: NSObject {
10
10
  private var webServer: HttpServer?
11
11
  private weak var delegate: HttpLocalServerSwifterDelegate?
12
-
12
+
13
13
  private static var pendingResponses = [String: (String) -> Void]()
14
14
  private static let queue = DispatchQueue(label: "com.cappitolian.HttpLocalServerSwifter.pendingResponses", qos: .userInitiated)
15
-
15
+
16
16
  private let defaultTimeout: TimeInterval = 10.0
17
17
  private let defaultPort: UInt16 = 8080
18
-
19
- // Handles Local Network permission dialog and denial detection.
20
- private let networkPermission = LocalNetworkPermission()
21
-
18
+
22
19
  public init(delegate: HttpLocalServerSwifterDelegate) {
23
20
  self.delegate = delegate
24
21
  super.init()
25
22
  }
26
-
23
+
27
24
  @objc public func connect(_ call: CAPPluginCall) {
28
- // Check Local Network permission first via NWBrowser callback.
29
- // iOS only shows the system dialog once — subsequent calls return the stored decision.
30
- // We must do this BEFORE starting the server because Swifter uses plain TCP sockets
31
- // which iOS never blocks — the denial only surfaces through Bonjour/mDNS errors,
32
- // not through server.start() failures.
33
- networkPermission.checkPermission { [weak self] status in
25
+ // IMPORTANT: Move execution to a background thread immediately
26
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
34
27
  guard let self = self else { return }
35
-
36
- if status == .denied {
37
- print("❌ SWIFTER: Local Network permission denied by user")
38
- call.reject("LOCAL_NETWORK_PERMISSION_DENIED")
39
- return
40
- }
41
-
42
- // .granted or .unknown — proceed with server startup.
43
- // .unknown means iOS didn't respond within the timeout (simulator, already granted, etc.)
44
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
45
- guard let self = self else { return }
46
-
47
- self.disconnect()
48
- let server = HttpServer()
49
- self.webServer = server
50
-
51
- // Use middleware to catch ALL requests and avoid route misses.
52
- server.middleware.append { [weak self] request in
53
- if request.method == "OPTIONS" {
54
- return self?.corsResponse() ?? .raw(204, "No Content", nil, nil)
55
- }
56
- return self?.processRequest(request) ?? .raw(500, "Internal Server Error", nil, nil)
57
- }
58
-
59
- do {
60
- try server.start(self.defaultPort, forceIPv4: true)
61
- let ip = Self.getWiFiAddress() ?? "127.0.0.1"
62
-
63
- print("🚀 SWIFTER: Server running on http://\(ip):\(self.defaultPort)")
64
-
65
- call.resolve([
66
- "ip": ip,
67
- "port": Int(self.defaultPort)
68
- ])
69
- } catch {
70
- print("❌ SWIFTER ERROR: \(error)")
71
- call.reject("Could not start server")
28
+
29
+ self.disconnect()
30
+ let server = HttpServer()
31
+ self.webServer = server
32
+
33
+ // Use middleware to catch ALL requests and avoid route misses
34
+ server.middleware.append { [weak self] request in
35
+ if request.method == "OPTIONS" {
36
+ return self?.corsResponse() ?? .raw(204, "No Content", nil, nil)
72
37
  }
38
+ return self?.processRequest(request) ?? .raw(500, "Internal Server Error", nil, nil)
39
+ }
40
+
41
+ do {
42
+ // We start the server. This call is non-blocking in Swifter but
43
+ // it's safer to do it here.
44
+ try server.start(self.defaultPort, forceIPv4: true)
45
+ let ip = Self.getWiFiAddress() ?? "127.0.0.1"
46
+
47
+ print("🚀 SWIFTER: Server running on http://\(ip):\(self.defaultPort)")
48
+
49
+ // Resolve back to Angular
50
+ call.resolve([
51
+ "ip": ip,
52
+ "port": Int(self.defaultPort)
53
+ ])
54
+ } catch {
55
+ print("❌ SWIFTER ERROR: \(error)")
56
+ call.reject("Could not start server")
73
57
  }
74
58
  }
75
59
  }
76
-
60
+
77
61
  @objc public func disconnect(_ call: CAPPluginCall? = nil) {
78
62
  webServer?.stop()
79
63
  webServer = nil
@@ -81,28 +65,18 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
81
65
  call?.resolve()
82
66
  }
83
67
 
84
- // MARK: - Settings redirect
85
-
86
- /// Opens the app's page in iOS Settings so the user can grant Local Network access.
87
- /// Called from the plugin when the JS layer requests it after a permission denial.
88
- @objc public func openSettings() {
89
- networkPermission.openAppSettings()
90
- }
91
-
92
- // MARK: - Request handling
93
-
94
68
  private func processRequest(_ request: HttpRequest) -> HttpResponse {
95
69
  let requestId = UUID().uuidString
96
70
  var responseString: String?
97
71
  let semaphore = DispatchSemaphore(value: 0)
98
-
72
+
99
73
  Self.queue.async {
100
74
  Self.pendingResponses[requestId] = { jsResponse in
101
75
  responseString = jsResponse
102
76
  semaphore.signal()
103
77
  }
104
78
  }
105
-
79
+
106
80
  let requestData: [String: Any] = [
107
81
  "requestId": requestId,
108
82
  "method": request.method,
@@ -111,25 +85,26 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
111
85
  "query": request.queryParams,
112
86
  "body": String(bytes: request.body, encoding: .utf8) ?? ""
113
87
  ]
114
-
115
- // notifyListeners MUST be called from the Main Thread.
88
+
89
+ // CRITICAL: notifyListeners MUST be called from the Main Thread
116
90
  DispatchQueue.main.async {
117
91
  self.delegate?.httpLocalServerSwifterDidReceiveRequest(requestData)
118
92
  }
119
-
93
+
120
94
  let result = semaphore.wait(timeout: .now() + defaultTimeout)
121
-
95
+
122
96
  if result == .timedOut {
123
97
  Self.queue.async { Self.pendingResponses.removeValue(forKey: requestId) }
124
98
  return .raw(408, "Request Timeout", nil, nil)
125
99
  }
126
-
100
+
127
101
  return createDynamicResponse(responseString ?? "")
128
102
  }
129
103
 
130
104
  static func handleJsResponse(requestId: String, responseData: [String: Any]) {
131
105
  queue.async {
132
106
  if let callback = pendingResponses[requestId] {
107
+ // Extract body, status and headers to pass as a JSON string to the semaphore
133
108
  if let jsonData = try? JSONSerialization.data(withJSONObject: responseData),
134
109
  let jsonString = String(data: jsonData, encoding: .utf8) {
135
110
  callback(jsonString)
@@ -139,8 +114,6 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
139
114
  }
140
115
  }
141
116
 
142
- // MARK: - Response helpers
143
-
144
117
  private func createDynamicResponse(_ jsonResponse: String) -> HttpResponse {
145
118
  var finalStatus = 200
146
119
  var finalBody = jsonResponse
@@ -150,7 +123,7 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
150
123
  "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
151
124
  "Access-Control-Allow-Headers": "Origin, Content-Type, Accept, Authorization, X-Requested-With"
152
125
  ]
153
-
126
+
154
127
  if let data = jsonResponse.data(using: .utf8),
155
128
  let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
156
129
  finalBody = dict["body"] as? String ?? ""
@@ -159,7 +132,7 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
159
132
  for (key, value) in customHeaders { headers[key] = value }
160
133
  }
161
134
  }
162
-
135
+
163
136
  return .raw(finalStatus, "OK", headers) { try $0.write([UInt8](finalBody.utf8)) }
164
137
  }
165
138
 
@@ -172,8 +145,6 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
172
145
  ], nil)
173
146
  }
174
147
 
175
- // MARK: - Network utilities
176
-
177
148
  static func getWiFiAddress() -> String? {
178
149
  var address: String?
179
150
  var ifaddr: UnsafeMutablePointer<ifaddrs>?
@@ -8,48 +8,37 @@ public class HttpLocalServerSwifterPlugin: CAPPlugin, CAPBridgedPlugin, HttpLoca
8
8
  public let pluginMethods: [CAPPluginMethod] = [
9
9
  CAPPluginMethod(name: "connect", returnType: CAPPluginReturnPromise),
10
10
  CAPPluginMethod(name: "disconnect", returnType: CAPPluginReturnPromise),
11
- CAPPluginMethod(name: "sendResponse", returnType: CAPPluginReturnPromise),
12
- // Redirects the user to Settings so they can grant Local Network permission manually.
13
- CAPPluginMethod(name: "openSettings", returnType: CAPPluginReturnPromise)
11
+ CAPPluginMethod(name: "sendResponse", returnType: CAPPluginReturnPromise)
14
12
  ]
15
-
13
+
16
14
  private var localServer: HttpLocalServerSwifter?
17
-
15
+
18
16
  @objc func connect(_ call: CAPPluginCall) {
19
17
  if localServer == nil { localServer = HttpLocalServerSwifter(delegate: self) }
20
18
  localServer?.connect(call)
21
19
  }
22
-
20
+
23
21
  @objc func disconnect(_ call: CAPPluginCall) {
24
22
  localServer?.disconnect(call)
25
23
  localServer = nil
26
24
  }
27
-
28
- @objc func sendResponse(_ call: CAPPluginCall) {
29
- guard let requestId = call.getString("requestId") else {
30
- call.reject("Missing requestId")
31
- return
32
- }
33
-
34
- if let responseData = call.dictionaryRepresentation as? [String: Any] {
35
- HttpLocalServerSwifter.handleJsResponse(requestId: requestId, responseData: responseData)
36
- call.resolve()
37
- } else {
38
- call.reject("Could not parse response data")
39
- }
40
- }
41
-
42
- /// Opens the app page in iOS Settings so the user can grant Local Network permission.
43
- ///
44
- /// Call this from JavaScript after catching a `LOCAL_NETWORK_PERMISSION_DENIED` rejection
45
- /// from `connect()`. There is no programmatic way to re-prompt the system dialog —
46
- /// redirecting to Settings is the only available recovery path.
47
- @objc func openSettings(_ call: CAPPluginCall) {
48
- localServer?.openSettings()
49
- call.resolve()
50
- }
51
-
25
+
26
+ @objc func sendResponse(_ call: CAPPluginCall) {
27
+ guard let requestId = call.getString("requestId") else {
28
+ call.reject("Missing requestId")
29
+ return
30
+ }
31
+
32
+ // Cast dictionaryRepresentation explicitly to [String: Any]
33
+ if let responseData = call.dictionaryRepresentation as? [String: Any] {
34
+ HttpLocalServerSwifter.handleJsResponse(requestId: requestId, responseData: responseData)
35
+ call.resolve()
36
+ } else {
37
+ call.reject("Could not parse response data")
38
+ }
39
+ }
40
+
52
41
  public func httpLocalServerSwifterDidReceiveRequest(_ data: [String: Any]) {
53
42
  notifyListeners("onRequest", data: data)
54
43
  }
55
- }
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cappitolian/http-local-server-swifter",
3
- "version": "0.0.24",
3
+ "version": "0.0.26",
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",
@@ -1,154 +0,0 @@
1
- import Foundation
2
- import Network
3
- import UIKit
4
-
5
- /// Handles Local Network permission lifecycle on iOS.
6
- ///
7
- /// IMPORTANT: iOS Local Network permission does NOT block TCP sockets.
8
- /// It only blocks mDNS/Bonjour service discovery. This means Swifter's
9
- /// server.start() will always succeed regardless of permission status.
10
- ///
11
- /// Detection strategy — NWListener with serviceRegistrationUpdateHandler:
12
- /// The PolicyDenied error for a revoked Local Network permission surfaces
13
- /// in NWListener.serviceRegistrationUpdateHandler, not in stateUpdateHandler.
14
- /// A connection handler must also be set or NWListener fails with error 22
15
- /// before iOS even evaluates the permission.
16
- @objc public class LocalNetworkPermission: NSObject {
17
-
18
- // MARK: - Types
19
-
20
- public enum PermissionStatus {
21
- case granted
22
- case denied
23
- case unknown
24
- }
25
-
26
- // MARK: - Constants
27
-
28
- /// Must match the NSBonjourServices entry in Info.plist exactly.
29
- private static let bonjourServiceType = "_ssspos._tcp"
30
-
31
- // MARK: - Private state
32
-
33
- private var listener: NWListener?
34
-
35
- // MARK: - Permission check
36
-
37
- /// Checks Local Network permission by advertising a Bonjour service via NWListener.
38
- ///
39
- /// The permission denial for a revoked permission surfaces in
40
- /// serviceRegistrationUpdateHandler — not in stateUpdateHandler.
41
- /// A dummy connection handler is required to prevent error 22 (Invalid argument).
42
- ///
43
- /// - Parameter completion: Called on the main thread with the permission status.
44
- public func checkPermission(completion: @escaping (PermissionStatus) -> Void) {
45
- var completed = false
46
-
47
- let finish: (PermissionStatus) -> Void = { [weak self] status in
48
- guard !completed else { return }
49
- completed = true
50
- self?.listener?.cancel()
51
- self?.listener = nil
52
- DispatchQueue.main.async {
53
- print("✅ LocalNetworkPermission resolved: \(status)")
54
- completion(status)
55
- }
56
- }
57
-
58
- let timeout = DispatchWorkItem {
59
- print("⚠️ LocalNetworkPermission: timeout — resolving as .unknown")
60
- finish(.unknown)
61
- }
62
- DispatchQueue.main.asyncAfter(deadline: .now() + 5.0, execute: timeout)
63
-
64
- let params = NWParameters.tcp
65
- params.includePeerToPeer = true
66
-
67
- guard let listener = try? NWListener(using: params) else {
68
- print("⚠️ LocalNetworkPermission: could not create NWListener")
69
- timeout.cancel()
70
- finish(.unknown)
71
- return
72
- }
73
-
74
- listener.service = NWListener.Service(type: Self.bonjourServiceType)
75
- self.listener = listener
76
-
77
- // Required: NWListener fails with error 22 (Invalid argument) if no
78
- // connection handler is set. This dummy handler satisfies the requirement.
79
- listener.newConnectionHandler = { connection in
80
- connection.cancel()
81
- }
82
-
83
- // This is where iOS reports PolicyDenied for a revoked Local Network permission.
84
- // stateUpdateHandler only reports generic listener errors, not Bonjour policy errors.
85
- listener.serviceRegistrationUpdateHandler = { change in
86
- print("🔍 LocalNetworkPermission serviceRegistration: \(change)")
87
- switch change {
88
- case .add:
89
- // Service registered successfully — permission is granted.
90
- timeout.cancel()
91
- finish(.granted)
92
-
93
- case .remove:
94
- // Service was removed. On permission denial this fires immediately
95
- // after the _NWAdvertiser PolicyDenied error in the system logs.
96
- // We treat an immediate remove (before .add) as denied.
97
- timeout.cancel()
98
- finish(.denied)
99
-
100
- @unknown default:
101
- break
102
- }
103
- }
104
-
105
- // stateUpdateHandler still needed to catch non-permission listener failures.
106
- listener.stateUpdateHandler = { state in
107
- print("🔍 LocalNetworkPermission NWListener state: \(state)")
108
- switch state {
109
- case .failed(let error):
110
- let code = (error as NSError).code
111
- print("🔍 LocalNetworkPermission NWListener error: \(code) — \(error.localizedDescription)")
112
- // Only treat explicit policy errors as denied; other failures are unknown.
113
- timeout.cancel()
114
- finish(Self.isPolicyDenied(error) ? .denied : .unknown)
115
-
116
- case .cancelled:
117
- break
118
-
119
- default:
120
- break
121
- }
122
- }
123
-
124
- listener.start(queue: .main)
125
- }
126
-
127
- // MARK: - Recovery
128
-
129
- /// Opens the app's page in iOS Settings so the user can manually grant Local Network access.
130
- ///
131
- /// - Note: This is the only recovery path after denial — iOS cannot re-prompt.
132
- /// - Warning: On iOS 17, a device restart may be needed after granting the permission.
133
- public func openAppSettings() {
134
- guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
135
- DispatchQueue.main.async {
136
- UIApplication.shared.open(url)
137
- }
138
- }
139
-
140
- // MARK: - Private helpers
141
-
142
- /// Returns true if the NWError indicates a Local Network policy denial.
143
- ///
144
- /// Known denial codes:
145
- /// kDNSServiceErr_PolicyDenied = -65570
146
- /// kDNSServiceErr_NoAuth = -65555
147
- private static func isPolicyDenied(_ error: NWError) -> Bool {
148
- let nsError = error as NSError
149
- if nsError.code == -65570 || nsError.code == -65555 { return true }
150
-
151
- let desc = error.localizedDescription.lowercased()
152
- return desc.contains("policy") || desc.contains("denied") || desc.contains("noauth")
153
- }
154
- }