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

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/README.md CHANGED
@@ -10,12 +10,14 @@ A Capacitor plugin to run a local HTTP server on your device, allowing you to re
10
10
  - ✅ Receive requests via events and send responses back from the JS layer
11
11
  - ✅ CORS support enabled by default for local communication
12
12
  - ✅ Support for all HTTP methods (GET, POST, PUT, PATCH, DELETE, OPTIONS)
13
+ - ✅ Dynamic URL routing (e.g. `/orders/:id`) supported via middleware
13
14
  - ✅ Swift Package Manager (SPM) support
14
15
  - ✅ Tested with **Capacitor 8** and **Ionic 8**
15
16
 
16
17
  ---
17
18
 
18
19
  ## Installation
20
+
19
21
  ```bash
20
22
  npm install @cappitolian/http-local-server-swifter
21
23
  npx cap sync
@@ -26,11 +28,13 @@ npx cap sync
26
28
  ## Usage
27
29
 
28
30
  ### Import
31
+
29
32
  ```typescript
30
33
  import { HttpLocalServerSwifter } from '@cappitolian/http-local-server-swifter';
31
34
  ```
32
35
 
33
36
  ### Listen and Respond
37
+
34
38
  ```typescript
35
39
  // 1. Set up the listener for incoming requests
36
40
  await HttpLocalServerSwifter.addListener('onRequest', async (data) => {
@@ -42,9 +46,9 @@ await HttpLocalServerSwifter.addListener('onRequest', async (data) => {
42
46
  // 2. Send a response back to the client using the requestId
43
47
  await HttpLocalServerSwifter.sendResponse({
44
48
  requestId: data.requestId,
45
- body: JSON.stringify({
46
- success: true,
47
- message: 'Request processed!'
49
+ body: JSON.stringify({
50
+ success: true,
51
+ message: 'Request processed!'
48
52
  })
49
53
  });
50
54
  });
@@ -56,6 +60,7 @@ HttpLocalServerSwifter.connect().then(result => {
56
60
  ```
57
61
 
58
62
  ### Stop Server
63
+
59
64
  ```typescript
60
65
  // 4. Stop the server
61
66
  await HttpLocalServerSwifter.disconnect();
@@ -79,9 +84,9 @@ await HttpLocalServerSwifter.disconnect();
79
84
 
80
85
  ---
81
86
 
82
- ## Migration from v0.0.x
87
+ ## Migration from v0.1.x
83
88
 
84
- Version 0.1.0 migrates from GCDWebServer to Swifter on iOS for better Swift Package Manager support. The API remains the same, so no code changes are needed in your app.
89
+ Version 0.2.0 introduces middleware-based routing on iOS and dynamic response support (custom `status` and `headers`) on both platforms. See changes below.
85
90
 
86
91
  ---
87
92
 
@@ -94,42 +99,64 @@ MIT
94
99
  ## Support
95
100
 
96
101
  If you have any issues or feature requests, please open an issue on the repository.
97
- ```
98
102
 
99
103
  ---
100
104
 
101
105
  ## 📋 Cambios Principales
102
106
 
103
- ### **GCDWebServerSwifter**
104
-
105
- | Aspecto | GCDWebServer | Swifter |
106
- |---------|--------------|---------|
107
- | **Importar** | `import GCDWebServer` | `import Swifter` |
108
- | **Crear servidor** | `GCDWebServer()` | `HttpServer()` |
109
- | **Tipo de puerto** | `UInt` | `UInt16` |
110
- | **Handlers** | `.addDefaultHandler(forMethod:)` | `server["/(.*)"] = { ... }` |
111
- | **Request type** | `GCDWebServerRequest` | `HttpRequest` |
112
- | **Response type** | `GCDWebServerDataResponse` | `HttpResponse` |
113
- | **Método HTTP** | `request.method` | `request.method` (igual) |
114
- | **Path** | `request.url.path` | `request.path` |
115
- | **Body** | `(request as? GCDWebServerDataRequest)?.data` | `request.body` (bytes) |
116
- | **Headers** | `request.headers` | `request.headers` (igual) |
117
- | **Query params** | `request.query` | `request.queryParams` |
118
- | **Start server** | `try server.start(options:)` | `try server.start(port)` |
119
- | **Stop server** | `server.stop()` | `server.stop()` (igual) |
120
- | **Response** | `GCDWebServerDataResponse(text:)` | `.ok(.text())` |
121
- | **CORS headers** | `setValue(_:forAdditionalHeader:)` | Custom extension |
107
+ ### **Route Handlers Middleware (iOS)**
108
+
109
+ | Aspecto | v0.1.x | v0.2.0 |
110
+ |---------|--------|--------|
111
+ | **Routing** | `server["/:path"] = { ... }` | `server.middleware.append { ... }` |
112
+ | **Rutas dinámicas** | ❌ Solo un segmento (`/menu`) | ✅ Cualquier ruta (`/orders/:id`) |
113
+ | **CORS preflight** | Manejado por handler estático | Interceptado en middleware antes del JS |
114
+ | **Thread de inicio** | Main thread | Background thread (`DispatchQueue.global`) |
115
+ | **Respuesta dinámica** | Solo `body` | `body` + `status` + `headers` |
116
+
117
+ ### **sendResponse Nuevos campos opcionales**
118
+
119
+ ```typescript
120
+ await HttpLocalServerSwifter.sendResponse({
121
+ requestId: data.requestId,
122
+ body: JSON.stringify({ success: true }),
123
+ status: 200, // NEW: opcional, default 200
124
+ headers: { // NEW: opcional, headers custom
125
+ 'X-Custom-Header': 'value'
126
+ }
127
+ });
128
+ ```
129
+
130
+ ### **Archivos modificados**
131
+
132
+ | Archivo | Cambio |
133
+ |---------|--------|
134
+ | `HttpLocalServerSwifter.swift` | Middleware en lugar de route handlers; `handleJsResponse` acepta `[String: Any]` |
135
+ | `HttpLocalServerSwifterPlugin.swift` | `sendResponse` pasa `dictionaryRepresentation` completo |
136
+ | `HttpLocalServerSwifterPlugin.java` | `sendResponse` pasa `call.getData()` completo |
137
+ | `definitions.ts` | `HttpSendResponseOptions` agrega `status?` y `headers?` |
138
+ | `web.ts` | Mock actualizado con los nuevos campos |
122
139
 
123
140
  ---
124
141
 
125
142
  ## ✅ Pasos para Aplicar
126
143
 
127
- 1. **Reemplaza `HttpLocalServerSwifter.swift`** con la versión de arriba
128
- 2. **Actualiza `Package.swift`**
129
- 3. **Actualiza `.podspec`**
130
- 4. **Actualiza `package.json`** (versiones de Capacitor 8)
144
+ 1. **Reemplaza `HttpLocalServerSwifter.swift`** con la versión nueva (middleware)
145
+ 2. **Reemplaza `HttpLocalServerSwifterPlugin.swift`** con la versión nueva
146
+ 3. **Reemplaza `HttpLocalServerSwifterPlugin.java`** con la versión nueva
147
+ 4. **Actualiza `definitions.ts`**, **`web.ts`** e **`index.ts`**
131
148
  5. **En Xcode**:
132
149
  ```
133
150
  File → Packages → Reset Package Caches
134
151
  File → Packages → Resolve Package Versions
135
- Product → Clean Build Folder
152
+ Product → Clean Build Folder
153
+ Product → Run
154
+ ```
155
+ 6. **En Android Studio**:
156
+ ```
157
+ Build → Clean Project
158
+ Build → Rebuild Project
159
+ Run
160
+ ```
161
+
162
+ > ⚠️ `npx cap sync` solo sincroniza archivos web. Los cambios en código nativo Swift/Java **requieren recompilación desde el IDE**.
package/dist/docs.json CHANGED
@@ -39,12 +39,27 @@
39
39
  ],
40
40
  "returns": "Promise<void>",
41
41
  "tags": [],
42
- "docs": "Sends a response back to the client.\nNow supports status and headers to handle CORS Preflight correctly.",
42
+ "docs": "Sends a response back to the client.\nSupports 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
+ },
48
63
  {
49
64
  "name": "addListener",
50
65
  "signature": "(eventName: 'onRequest', listenerFunc: (data: HttpRequestData) => void | Promise<void>) => Promise<PluginListenerHandle>",
@@ -130,14 +145,14 @@
130
145
  {
131
146
  "name": "status",
132
147
  "tags": [],
133
- "docs": "* NEW: HTTP Status code (e.g., 200, 204, 404). \nDefault is 200.",
148
+ "docs": "HTTP Status code (e.g., 200, 204, 404).\nDefault is 200.",
134
149
  "complexTypes": [],
135
150
  "type": "number | undefined"
136
151
  },
137
152
  {
138
153
  "name": "headers",
139
154
  "tags": [],
140
- "docs": "* NEW: Custom HTTP headers.\nCrucial for fixing CORS by providing 'Access-Control-Allow-Origin'.",
155
+ "docs": "Custom HTTP headers.\nCrucial for fixing CORS by providing 'Access-Control-Allow-Origin'.",
141
156
  "complexTypes": [
142
157
  "Record"
143
158
  ],
@@ -20,11 +20,13 @@ export interface HttpSendResponseOptions {
20
20
  requestId: string;
21
21
  /** The response body (usually stringified JSON) */
22
22
  body: string;
23
- /** * NEW: HTTP Status code (e.g., 200, 204, 404).
23
+ /**
24
+ * HTTP Status code (e.g., 200, 204, 404).
24
25
  * Default is 200.
25
26
  */
26
27
  status?: number;
27
- /** * NEW: Custom HTTP headers.
28
+ /**
29
+ * Custom HTTP headers.
28
30
  * Crucial for fixing CORS by providing 'Access-Control-Allow-Origin'.
29
31
  */
30
32
  headers?: Record<string, string>;
@@ -34,9 +36,25 @@ export interface HttpLocalServerSwifterPlugin {
34
36
  disconnect(): Promise<void>;
35
37
  /**
36
38
  * Sends a response back to the client.
37
- * Now supports status and headers to handle CORS Preflight correctly.
39
+ * Supports status and headers to handle CORS Preflight correctly.
38
40
  */
39
41
  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>;
40
58
  addListener(eventName: 'onRequest', listenerFunc: (data: HttpRequestData) => void | Promise<void>): Promise<PluginListenerHandle>;
41
59
  removeAllListeners(): Promise<void>;
42
60
  }
@@ -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 /** * 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}"]}
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}"]}
package/dist/esm/web.d.ts CHANGED
@@ -5,4 +5,8 @@ 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>;
8
12
  }
package/dist/esm/web.js CHANGED
@@ -17,7 +17,13 @@ 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 });
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.');
21
27
  }
22
28
  }
23
29
  //# 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;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}"]}
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}"]}
@@ -31,7 +31,13 @@ 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 });
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.');
35
41
  }
36
42
  }
37
43
 
@@ -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//# 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;;;;;;;;;"}
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;;;;;;;;;"}
package/dist/plugin.js CHANGED
@@ -30,7 +30,13 @@ 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 });
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.');
34
40
  }
35
41
  }
36
42
 
@@ -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//# 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;;;;;;;;;;;;;;;"}
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;;;;;;;;;;;;;;;"}
@@ -9,55 +9,71 @@ 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
-
18
+
19
+ // Handles Local Network permission dialog and denial detection.
20
+ private let networkPermission = LocalNetworkPermission()
21
+
19
22
  public init(delegate: HttpLocalServerSwifterDelegate) {
20
23
  self.delegate = delegate
21
24
  super.init()
22
25
  }
23
-
26
+
24
27
  @objc public func connect(_ call: CAPPluginCall) {
25
- // IMPORTANT: Move execution to a background thread immediately
26
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
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
27
34
  guard let self = self else { return }
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)
37
- }
38
- return self?.processRequest(request) ?? .raw(500, "Internal Server Error", nil, nil)
35
+
36
+ if status == .denied {
37
+ print("❌ SWIFTER: Local Network permission denied by user")
38
+ call.reject("LOCAL_NETWORK_PERMISSION_DENIED")
39
+ return
39
40
  }
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")
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")
72
+ }
57
73
  }
58
74
  }
59
75
  }
60
-
76
+
61
77
  @objc public func disconnect(_ call: CAPPluginCall? = nil) {
62
78
  webServer?.stop()
63
79
  webServer = nil
@@ -65,18 +81,28 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
65
81
  call?.resolve()
66
82
  }
67
83
 
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
+
68
94
  private func processRequest(_ request: HttpRequest) -> HttpResponse {
69
95
  let requestId = UUID().uuidString
70
96
  var responseString: String?
71
97
  let semaphore = DispatchSemaphore(value: 0)
72
-
98
+
73
99
  Self.queue.async {
74
100
  Self.pendingResponses[requestId] = { jsResponse in
75
101
  responseString = jsResponse
76
102
  semaphore.signal()
77
103
  }
78
104
  }
79
-
105
+
80
106
  let requestData: [String: Any] = [
81
107
  "requestId": requestId,
82
108
  "method": request.method,
@@ -85,26 +111,25 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
85
111
  "query": request.queryParams,
86
112
  "body": String(bytes: request.body, encoding: .utf8) ?? ""
87
113
  ]
88
-
89
- // CRITICAL: notifyListeners MUST be called from the Main Thread
114
+
115
+ // notifyListeners MUST be called from the Main Thread.
90
116
  DispatchQueue.main.async {
91
117
  self.delegate?.httpLocalServerSwifterDidReceiveRequest(requestData)
92
118
  }
93
-
119
+
94
120
  let result = semaphore.wait(timeout: .now() + defaultTimeout)
95
-
121
+
96
122
  if result == .timedOut {
97
123
  Self.queue.async { Self.pendingResponses.removeValue(forKey: requestId) }
98
124
  return .raw(408, "Request Timeout", nil, nil)
99
125
  }
100
-
126
+
101
127
  return createDynamicResponse(responseString ?? "")
102
128
  }
103
129
 
104
130
  static func handleJsResponse(requestId: String, responseData: [String: Any]) {
105
131
  queue.async {
106
132
  if let callback = pendingResponses[requestId] {
107
- // Extract body, status and headers to pass as a JSON string to the semaphore
108
133
  if let jsonData = try? JSONSerialization.data(withJSONObject: responseData),
109
134
  let jsonString = String(data: jsonData, encoding: .utf8) {
110
135
  callback(jsonString)
@@ -114,6 +139,8 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
114
139
  }
115
140
  }
116
141
 
142
+ // MARK: - Response helpers
143
+
117
144
  private func createDynamicResponse(_ jsonResponse: String) -> HttpResponse {
118
145
  var finalStatus = 200
119
146
  var finalBody = jsonResponse
@@ -123,7 +150,7 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
123
150
  "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
124
151
  "Access-Control-Allow-Headers": "Origin, Content-Type, Accept, Authorization, X-Requested-With"
125
152
  ]
126
-
153
+
127
154
  if let data = jsonResponse.data(using: .utf8),
128
155
  let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
129
156
  finalBody = dict["body"] as? String ?? ""
@@ -132,7 +159,7 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
132
159
  for (key, value) in customHeaders { headers[key] = value }
133
160
  }
134
161
  }
135
-
162
+
136
163
  return .raw(finalStatus, "OK", headers) { try $0.write([UInt8](finalBody.utf8)) }
137
164
  }
138
165
 
@@ -145,6 +172,8 @@ public protocol HttpLocalServerSwifterDelegate: AnyObject {
145
172
  ], nil)
146
173
  }
147
174
 
175
+ // MARK: - Network utilities
176
+
148
177
  static func getWiFiAddress() -> String? {
149
178
  var address: String?
150
179
  var ifaddr: UnsafeMutablePointer<ifaddrs>?
@@ -8,37 +8,48 @@ 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)
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)
12
14
  ]
13
-
15
+
14
16
  private var localServer: HttpLocalServerSwifter?
15
-
17
+
16
18
  @objc func connect(_ call: CAPPluginCall) {
17
19
  if localServer == nil { localServer = HttpLocalServerSwifter(delegate: self) }
18
20
  localServer?.connect(call)
19
21
  }
20
-
22
+
21
23
  @objc func disconnect(_ call: CAPPluginCall) {
22
24
  localServer?.disconnect(call)
23
25
  localServer = nil
24
26
  }
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
-
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
+
41
52
  public func httpLocalServerSwifterDidReceiveRequest(_ data: [String: Any]) {
42
53
  notifyListeners("onRequest", data: data)
43
54
  }
44
- }
55
+ }
@@ -0,0 +1,114 @@
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:
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.
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: - Private state
27
+
28
+ private var browser: NWBrowser?
29
+
30
+ // MARK: - Permission check
31
+
32
+ /// Triggers the Local Network permission dialog and returns the result
33
+ /// asynchronously via the completion handler.
34
+ ///
35
+ /// - 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.
38
+ ///
39
+ /// - Note: iOS shows the dialog only once. On subsequent calls this method
40
+ /// will return the previously stored decision immediately (no dialog shown).
41
+ public func checkPermission(completion: @escaping (PermissionStatus) -> Void) {
42
+ let parameters = NWParameters()
43
+ parameters.includePeerToPeer = true
44
+
45
+ let browser = NWBrowser(
46
+ for: .bonjour(type: "_http._tcp", domain: "local."),
47
+ using: parameters
48
+ )
49
+ self.browser = browser
50
+
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
+ var completed = false
54
+ let timeout = DispatchWorkItem {
55
+ guard !completed else { return }
56
+ completed = true
57
+ browser.cancel()
58
+ DispatchQueue.main.async { completion(.unknown) }
59
+ }
60
+ DispatchQueue.main.asyncAfter(deadline: .now() + 3.0, execute: timeout)
61
+
62
+ browser.stateUpdateHandler = { state in
63
+ guard !completed else { return }
64
+
65
+ switch state {
66
+ case .ready:
67
+ // Browser started successfully — permission is granted
68
+ completed = true
69
+ timeout.cancel()
70
+ browser.cancel()
71
+ DispatchQueue.main.async { completion(.granted) }
72
+
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")
81
+
82
+ completed = true
83
+ timeout.cancel()
84
+ browser.cancel()
85
+ DispatchQueue.main.async {
86
+ completion(isDenied ? .denied : .unknown)
87
+ }
88
+
89
+ case .cancelled:
90
+ break // Expected when we cancel it ourselves
91
+
92
+ default:
93
+ break // .setup, .waiting — keep waiting
94
+ }
95
+ }
96
+
97
+ browser.start(queue: .main)
98
+ }
99
+
100
+ // MARK: - Recovery
101
+
102
+ /// Opens the app's page in iOS Settings so the user can manually grant Local Network access.
103
+ ///
104
+ /// - Note: This is the only recovery path available after the user denies the permission.
105
+ /// - Warning: On iOS 17, the user may need to restart the device after granting
106
+ /// the permission for it to take effect. This is a known iOS 17 bug.
107
+ public func openAppSettings() {
108
+ guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
109
+
110
+ DispatchQueue.main.async {
111
+ UIApplication.shared.open(url)
112
+ }
113
+ }
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cappitolian/http-local-server-swifter",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
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",