@rejaul/react-native-printer-kit 0.3.4 → 0.4.0

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
@@ -1,8 +1,8 @@
1
1
  # react-native-printer-kit
2
2
 
3
- A professional-grade React Native module for printing directly to Bluetooth POS/thermal (ESC/POS) printers on **Android devices only** — no third-party printer app, no OS print dialog. Print raw text, images, PDFs, and full HTML/CSS content, including **Bangla and other non-Latin scripts** that ESC/POS printer fonts don't support natively.
3
+ A professional-grade React Native module for printing directly to Bluetooth, Wi-Fi/LAN and USB POS/thermal (ESC/POS) printers on **Android devices only** — no third-party printer app, no OS print dialog. Print raw text, images, PDFs, and full HTML/CSS content, including **Bangla and other non-Latin scripts** that ESC/POS printer fonts don't support natively.
4
4
 
5
- > **Important:** This library is **exclusively for Android**. Bluetooth Classic SPP + ESC/POS is an Android-specific integration; iOS does not expose the APIs this relies on.
5
+ > **Important:** This library is **exclusively for Android**. Bluetooth Classic SPP, USB host and ESC/POS are Android-specific integrations; iOS does not expose the APIs this relies on.
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/@rejaul/react-native-printer-kit.svg)](https://www.npmjs.com/package/@rejaul/react-native-printer-kit)
8
8
  [![license](https://img.shields.io/npm/l/@rejaul/react-native-printer-kit.svg)](LICENSE)
@@ -18,7 +18,11 @@ It's a thin React Native bridge over [`android-printer-kit`](https://github.com/
18
18
 
19
19
  ## Features
20
20
 
21
- - **Direct Bluetooth Classic (SPP) connection** to any paired ESC/POS printer.
21
+ - **Three connection types** behind one API: Bluetooth Classic (SPP), Wi-Fi/LAN (raw TCP) and USB (OTG). Every print function is the same for all three.
22
+ - **Printer-only device list**: `getBondedBluetoothPrinters({ filter: 'LIKELY_PRINTERS' })` hides headphones, watches and other non-printers.
23
+ - **Printer checks**: `isPrinterReady()`, `getPrinterStatus()` (online / paper out / cover / error) and `printTestPage()`.
24
+ - **Save files** (e.g. the PDF from `htmlToPdf`) to Downloads, Documents, app storage or a chosen folder with `saveFile()`.
25
+ - **Honest connection notification**: if the printer drops (switched off, out of range, Bluetooth off, USB unplugged) the ongoing "connected" notification is replaced by a dismissible "disconnected" one.
22
26
  - **HTML → PDF → print** pipeline, so you can print an actual styled receipt/report.
23
27
  - **PDF → image → print** pipeline via Android's built-in `PdfRenderer`.
24
28
  - **Non-Latin text support** (Bangla and others) via image-based rendering.
@@ -36,7 +40,15 @@ It's a thin React Native bridge over [`android-printer-kit`](https://github.com/
36
40
 
37
41
  ## Scope
38
42
 
39
- This library speaks **Bluetooth Classic SPP + ESC/POS** — the standard used by virtually all budget Bluetooth receipt/thermal printers. It does not support Bluetooth LE printers, WiFi/network printers, USB-connected printers, or non-ESC/POS protocols (PCL/PostScript inkjet & laser printers).
43
+ This library speaks **ESC/POS** — the standard used by virtually all receipt/thermal POS printers — over:
44
+
45
+ | Connection | Notes |
46
+ |---|---|
47
+ | Bluetooth Classic (SPP) | The printer must be paired in Android's Bluetooth settings first. |
48
+ | Wi-Fi / LAN | Raw TCP, usually port 9100. Phone and printer must be on the same network; there is no discovery, so you supply the IP. |
49
+ | USB | Needs a phone with USB host (OTG) and the user's per-device permission (`requestUsbPermission`). |
50
+
51
+ Not supported: Bluetooth LE-only printers, label printers that use TSPL/ZPL/CPCL (Zebra, TSC, ...), and non-ESC/POS protocols (PCL/PostScript inkjet & laser printers).
40
52
 
41
53
  ## Minimum Requirements
42
54
 
@@ -52,7 +64,7 @@ npm install @rejaul/react-native-printer-kit
52
64
  yarn add @rejaul/react-native-printer-kit
53
65
  ```
54
66
 
55
- Nothing to configure manually — all required Bluetooth and foreground-service permissions are declared by the underlying native library's manifest and merge in automatically. You still need to request the runtime `BLUETOOTH_CONNECT` (Android 12+) and `POST_NOTIFICATIONS` (Android 13+) permissions at runtime — `requestBluetoothPermission()` handles both in one call; see [Permissions](#permissions) below.
67
+ Nothing to configure manually — all required Bluetooth, internet, USB-host and foreground-service permissions are declared by the underlying native library's manifest and merge in automatically. You still need to request the runtime `BLUETOOTH_CONNECT` (Android 12+) and `POST_NOTIFICATIONS` (Android 13+) permissions at runtime — `requestBluetoothPermission()` handles both in one call; see [Permissions](#permissions) below.
56
68
 
57
69
  ## Quick Start
58
70
 
@@ -117,11 +129,39 @@ Every function that takes data (anything beyond a bare callback) takes a single
117
129
 
118
130
  ### Connection
119
131
 
120
- #### `getBondedBluetoothPrinters(): BluetoothPrinterDevice[]`
121
- Lists Bluetooth devices already paired with the phone via Android's own Bluetooth settings.
132
+ #### `getBondedBluetoothPrinters(params?: GetBondedPrintersParams): BluetoothPrinterDevice[]`
133
+ Lists Bluetooth devices already paired with the phone via Android's own Bluetooth settings, best printer matches first. Each device has a `confidence` (`CONFIDENT` / `LIKELY` / `UNKNOWN`). Pass `{ filter: 'LIKELY_PRINTERS' }` to hide devices that do not look like printers (`'STRICT'` keeps only certain ones), or `extraNameKeywords: ['part-of-its-name']` if a real printer has an unusual name and gets hidden. With no argument every paired device is returned, as before.
134
+
135
+ #### `getUsbPrinters(): UsbPrinterDevice[]`
136
+ USB printers currently plugged in (devices exposing the USB *printer* class). Needs a phone with USB host (OTG).
137
+
138
+ #### `hasUsbPermission(params: UsbDeviceParams): boolean`
139
+ Whether the user already granted this app access to that USB printer.
140
+
141
+ #### `requestUsbPermission(params: UsbDeviceParams): Promise<boolean>`
142
+ Shows the system USB permission dialog and resolves with the user's answer. Call it before connecting. The grant is lost when the app is reinstalled.
122
143
 
123
144
  #### `connectPrinter(params: ConnectPrinterParams): Promise<boolean>`
124
- Opens an RFCOMM/SPP connection to the paired printer. Closes any existing connection first, and remembers the address for auto-reconnect on the next app launch.
145
+ Connects over Bluetooth (default), Wi-Fi/LAN or USB depending on `params.type`. Closes any existing connection first, and remembers the printer for auto-reconnect on the next app launch. Resolves `false` (does not reject) if it could not connect; for USB that includes a missing permission.
146
+
147
+ ```typescript
148
+ // Bluetooth (default) - the printer must be paired first
149
+ await connectPrinter({ address: 'AA:BB:CC:DD:EE:FF' });
150
+
151
+ // Wi-Fi / LAN
152
+ await connectPrinter({ address: '192.168.1.50', type: 'NETWORK', port: 9100 });
153
+
154
+ // USB - ask for permission first (system dialog), then connect
155
+ const usb = getUsbPrinters()[0];
156
+ if (usb && (await requestUsbPermission(usb))) {
157
+ await connectPrinter({
158
+ address: '',
159
+ type: 'USB',
160
+ vendorId: usb.vendorId,
161
+ productId: usb.productId,
162
+ });
163
+ }
164
+ ```
125
165
 
126
166
  #### `disconnectPrinter(): Promise<void>`
127
167
  Closes the current connection and forgets it for auto-reconnect.
@@ -132,6 +172,35 @@ Whether a printer connection is currently open.
132
172
  #### `getConnectedPrinter(): Promise<BluetoothPrinterDevice | null>`
133
173
  The currently connected printer, if any.
134
174
 
175
+ ### Printer checks
176
+
177
+ #### `isPrinterReady(): Promise<boolean>`
178
+ Connected **and** the printer accepts data right now (it may be switched off or out of range). It does not check paper or the cover.
179
+
180
+ #### `getPrinterStatus(params?: PrinterStatusParams): Promise<PrinterStatus>`
181
+ Asks the printer for its state. Many printers ignore the request: then `responded` is `false` and every other field is `null` (unknown, **not** "ok"). Things to know:
182
+
183
+ - Right after connecting, the first request can time out (`responded: false`); asking again a moment later works.
184
+ - What a printer reports depends on its firmware. On the USB receipt printer used for testing, an open cover was reported as `online: false, paperOut: true` and never as `coverOpen: true`. So read `paperOut: true` as "no paper *or* cover open", and do not rely on `coverOpen` unless you verified it on your printer model.
185
+ - `online: false` is the most reliable "do not print now" signal.
186
+ - A print call that resolves only means the bytes were sent. An offline or out-of-paper printer may still accept them, so check the status first when it matters.
187
+
188
+ #### `printTestPage(): Promise<void>`
189
+ Prints a short ASCII test page (connection type, printer, address, time).
190
+
191
+ ### Saving and opening files
192
+
193
+ #### `saveFile(params: SaveFileParams): Promise<SaveFileResult>`
194
+ Copies a finished file (for example the PDF from `htmlToPdf`) to `CACHE`, `APP_FILES`, public `DOWNLOADS` / `DOCUMENTS` or a `CUSTOM_FOLDER`. Never rejects: check `result.success` and `result.error`. On Android 10+ Downloads/Documents use MediaStore (no permission needed); on Android 9 and below you need `WRITE_EXTERNAL_STORAGE`. `overwrite: true` can only replace files this app created.
195
+
196
+ #### `openFile(params: OpenFileParams): boolean`
197
+ Opens a file with whatever installed app can handle its type (shows the "Open with" chooser if several match). Returns `false` if the file does not exist or no app can open it.
198
+
199
+ ### Bluetooth state
200
+
201
+ #### `isBluetoothEnabled(): boolean`
202
+ Whether the Bluetooth adapter itself is on. Needs no runtime permission, so check it before asking for the permission.
203
+
135
204
  ### Printing
136
205
 
137
206
  #### `printText(params: PrintTextParams): Promise<void>`
@@ -155,17 +224,105 @@ Renders HTML to a PDF file using an off-screen `WebView` and returns its path, o
155
224
  #### `printHtml(params: PrintHtmlParams): Promise<boolean>`
156
225
  Full pipeline: `htmlToPdf` → `printPdf`. This is how you print Bangla (or other non-Latin) text or any real HTML/CSS layout. Retries once (reconnect + resend) if the underlying write fails, e.g. from a "Broken pipe" when a cheap board drops the link mid-print.
157
226
 
227
+ ## Enums
228
+
229
+ The string options are also exported as constants, so you can write `PrinterFilter.LIKELY_PRINTERS` instead of a bare string (plain strings keep working too). Each name is both a runtime value and a type:
230
+
231
+ ```typescript
232
+ import {
233
+ PrinterFilter, // ALL | LIKELY_PRINTERS | STRICT
234
+ PrinterConfidence, // CONFIDENT | LIKELY | UNKNOWN
235
+ PrinterConnectionType, // BLUETOOTH | NETWORK | USB
236
+ PrinterImageType, // PNG | JPEG
237
+ FileLocation, // CACHE | APP_FILES | DOWNLOADS | DOCUMENTS | CUSTOM_FOLDER
238
+ } from '@rejaul/react-native-printer-kit';
239
+
240
+ getBondedBluetoothPrinters({ filter: PrinterFilter.LIKELY_PRINTERS });
241
+ connectPrinter({ address: '192.168.1.50', type: PrinterConnectionType.NETWORK });
242
+ saveFile({ sourcePath, location: FileLocation.DOWNLOADS });
243
+ ```
244
+
158
245
  ## Type Definitions
159
246
 
160
247
  ```typescript
248
+ type PrinterConfidence = 'CONFIDENT' | 'LIKELY' | 'UNKNOWN';
249
+ type PrinterFilter = 'ALL' | 'LIKELY_PRINTERS' | 'STRICT';
250
+ type PrinterConnectionType = 'BLUETOOTH' | 'NETWORK' | 'USB';
251
+
161
252
  type BluetoothPrinterDevice = {
253
+ name: string | null; // null for a Wi-Fi/LAN printer
254
+ address: string; // MAC, host, or "vendorId:productId" for USB
255
+ confidence: PrinterConfidence;
256
+ reason: string | null;
257
+ };
258
+
259
+ type GetBondedPrintersParams = {
260
+ filter?: PrinterFilter; // default 'ALL'
261
+ extraNameKeywords?: string[];
262
+ };
263
+
264
+ type UsbPrinterDevice = {
162
265
  name: string | null;
163
- address: string;
266
+ vendorId: number;
267
+ productId: number;
268
+ deviceName: string;
164
269
  };
165
270
 
271
+ type UsbDeviceParams = { vendorId: number; productId: number };
272
+
166
273
  type PrinterImageType = 'PNG' | 'JPEG';
167
274
 
168
- type ConnectPrinterParams = { address: string };
275
+ type ConnectPrinterParams = {
276
+ address: string; // Bluetooth MAC, or IP/host for NETWORK; '' for USB
277
+ type?: PrinterConnectionType; // default 'BLUETOOTH'
278
+ port?: number; // NETWORK only, default 9100
279
+ timeoutMs?: number; // NETWORK connect timeout, default 5000
280
+ vendorId?: number; // USB only
281
+ productId?: number; // USB only
282
+ notificationTitle?: string; // default 'Printer connected'
283
+ disconnectedNotificationTitle?: string; // default 'Printer disconnected'
284
+ };
285
+
286
+ type PrinterStatusParams = { timeoutMs?: number }; // default 800
287
+
288
+ // null = unknown (the printer did not report it), NOT "ok"
289
+ type PrinterStatus = {
290
+ connected: boolean;
291
+ responded: boolean;
292
+ online: boolean | null;
293
+ paperOut: boolean | null;
294
+ paperNearEnd: boolean | null;
295
+ coverOpen: boolean | null;
296
+ hasError: boolean | null;
297
+ };
298
+
299
+ type FileLocation =
300
+ | 'CACHE'
301
+ | 'APP_FILES'
302
+ | 'DOWNLOADS'
303
+ | 'DOCUMENTS'
304
+ | 'CUSTOM_FOLDER';
305
+
306
+ type SaveFileParams = {
307
+ sourcePath: string;
308
+ fileName?: string; // defaults to the source file's name
309
+ location?: FileLocation; // default 'DOWNLOADS'
310
+ customFolder?: string; // required for CUSTOM_FOLDER
311
+ subFolder?: string;
312
+ mimeType?: string; // guessed from the extension
313
+ overwrite?: boolean;
314
+ deleteSource?: boolean;
315
+ };
316
+
317
+ type SaveFileResult = {
318
+ success: boolean;
319
+ path: string | null;
320
+ uri: string | null;
321
+ fileName: string | null;
322
+ error: string | null;
323
+ };
324
+
325
+ type OpenFileParams = { path: string; mimeType?: string };
169
326
 
170
327
  type PrintTextParams = { text: string; feedLines?: number };
171
328
 
@@ -221,9 +378,13 @@ type PrintHtmlParams = {
221
378
 
222
379
  ## Persistent connection & auto-reconnect
223
380
 
224
- Connecting through this library keeps the printer connected even if the app is swiped away from Recents — the native side runs a foreground service that outlives the Activity/JS runtime. Every successful `connectPrinter()` call also remembers the device, so the next time the app is opened (even after being fully killed) it reconnects automatically without the user having to pick the printer again.
381
+ Connecting through this library keeps the printer connected even if the app is swiped away from Recents — the native side runs a foreground service that outlives the Activity/JS runtime. Every successful `connectPrinter()` call also remembers the printer (connection type, address/IP, port and USB ids), so the next time the app is opened (even after being fully killed) it reconnects automatically without the user having to pick the printer again.
382
+
383
+ `connectPrinter()` falls back to a direct RFCOMM channel if the standard SDP-based socket fails (common on printers with a broken SDP record). `isConnectedPrinter()` reflects a real disconnect for Bluetooth (Android's ACL-disconnect and Bluetooth-off events) and USB (unplugging) instead of a stale flag; a dropped Wi-Fi/LAN link is only noticed on the next write, which reconnects once automatically.
384
+
385
+ ## Connection lost notification
225
386
 
226
- `connectPrinter()` falls back to a direct RFCOMM channel if the standard SDP-based socket fails (common on printers with a broken SDP record), and `isConnectedPrinter()` reflects a real physical disconnect immediately (via Android's ACL-disconnect broadcast) instead of a stale flag that stays `true` after the printer silently drops the link.
387
+ When a connected printer drops on its own, the ongoing **"Printer connected"** notification is removed and a normal, dismissible **"Printer disconnected — \<printer name\>"** notification is posted. Reconnecting removes it again. Calling `disconnectPrinter()` yourself shows no notification. Change the wording with `notificationTitle` / `disconnectedNotificationTitle` in `connectPrinter()`.
227
388
 
228
389
  ## Printing Bangla / other non-Latin text
229
390
 
@@ -231,7 +392,7 @@ ESC/POS printer firmware fonts only cover ASCII, so `printText()` can't render B
231
392
 
232
393
  ## Example Application
233
394
 
234
- See the [`example`](./example) directory for a complete React Native application demonstrating permission handling, printer pairing/connection, and every print function above.
395
+ See the [`example`](./example) directory for a complete React Native application demonstrating permission handling, Bluetooth and USB connection, printer status, the test page, and the print functions above.
235
396
 
236
397
  ```bash
237
398
  cd example
@@ -61,5 +61,5 @@ repositories {
61
61
 
62
62
  dependencies {
63
63
  implementation "com.facebook.react:react-android"
64
- implementation("com.github.DeveloperRejaul:android-printer-kit:v0.0.14")
64
+ implementation("com.github.DeveloperRejaul:android-printer-kit:v0.1.0")
65
65
  }
@@ -15,18 +15,23 @@ import com.facebook.react.bridge.WritableArray
15
15
  import com.facebook.react.bridge.WritableMap
16
16
  import com.facebook.react.modules.core.PermissionAwareActivity
17
17
  import com.rezaul.printerkit.BluetoothPermissions
18
- import com.rezaul.printerkit.BluetoothPrinter
19
- import com.rezaul.printerkit.BluetoothPrinterDevice
20
- import com.rezaul.printerkit.BluetoothPrinterService
21
- import com.rezaul.printerkit.ConnectPrinterParams
22
- import com.rezaul.printerkit.HtmlToPdfParams
23
- import com.rezaul.printerkit.PdfToImageParams
24
- import com.rezaul.printerkit.PrintHtmlParams
25
- import com.rezaul.printerkit.PrintImageBase64Params
26
- import com.rezaul.printerkit.PrintImageFileParams
27
- import com.rezaul.printerkit.PrintPdfParams
28
- import com.rezaul.printerkit.PrinterImageType
29
- import com.rezaul.printerkit.PrintTextParams
18
+ import com.rezaul.printerkit.PosPrinter
19
+ import com.rezaul.printerkit.PrinterService
20
+ import com.rezaul.printerkit.model.BluetoothPrinterDevice
21
+ import com.rezaul.printerkit.model.ConnectPrinterParams
22
+ import com.rezaul.printerkit.model.FileLocation
23
+ import com.rezaul.printerkit.model.HtmlToPdfParams
24
+ import com.rezaul.printerkit.model.PdfToImageParams
25
+ import com.rezaul.printerkit.model.PrintHtmlParams
26
+ import com.rezaul.printerkit.model.PrintImageBase64Params
27
+ import com.rezaul.printerkit.model.PrintImageFileParams
28
+ import com.rezaul.printerkit.model.PrintPdfParams
29
+ import com.rezaul.printerkit.model.PrintTextParams
30
+ import com.rezaul.printerkit.model.PrinterConnectionType
31
+ import com.rezaul.printerkit.model.PrinterFilter
32
+ import com.rezaul.printerkit.model.PrinterImageType
33
+ import com.rezaul.printerkit.model.SaveFileParams
34
+ import com.rezaul.printerkit.model.UsbPrinterDevice
30
35
  import java.util.concurrent.Executors
31
36
 
32
37
  /** Reads an optional numeric field, falling back to [default] if absent/null. */
@@ -41,21 +46,33 @@ private fun ReadableMap.optIntOrNull(key: String): Int? =
41
46
  private fun ReadableMap.optLong(key: String, default: Long): Long =
42
47
  if (hasKey(key) && !isNull(key)) getDouble(key).toLong() else default
43
48
 
49
+ /** Reads an optional string field, or null if absent/null. */
50
+ private fun ReadableMap.optString(key: String): String? =
51
+ if (hasKey(key) && !isNull(key)) getString(key) else null
52
+
53
+ /** Reads an optional boolean field, falling back to [default] if absent/null. */
54
+ private fun ReadableMap.optBoolean(key: String, default: Boolean): Boolean =
55
+ if (hasKey(key) && !isNull(key)) getBoolean(key) else default
56
+
57
+ /** Parses an enum by name, falling back to [default] for a missing or unknown value. */
58
+ private inline fun <reified T : Enum<T>> enumOrDefault(name: String?, default: T): T =
59
+ name?.let { runCatching { enumValueOf<T>(it) }.getOrNull() } ?: default
60
+
44
61
  /**
45
- * Bridges [com.rezaul.printerkit.BluetoothPrinter] to React Native.
62
+ * Bridges [com.rezaul.printerkit.PosPrinter] to React Native.
46
63
  *
47
- * Connect/print calls run through a bound [BluetoothPrinterService] (started once,
48
- * on module init) instead of a bare [BluetoothPrinter], so the connection survives
49
- * the app being swiped away from Recents - see [BluetoothPrinterService]. Calls made
64
+ * Connect/print calls run through a bound [PrinterService] (started once,
65
+ * on module init) instead of a bare [PosPrinter], so the connection survives
66
+ * the app being swiped away from Recents - see [PrinterService]. Calls made
50
67
  * before the service finishes binding (essentially instant, but not synchronous) are
51
68
  * queued and run once it connects.
52
69
  *
53
- * All blocking Bluetooth/PDF/image I/O runs on a single background executor, off the
70
+ * All blocking Bluetooth/USB/network/PDF/image I/O runs on a single background executor, off the
54
71
  * calling (JS) thread - also serializing printer writes, since concurrent writes to
55
72
  * the same socket from two overlapping calls would corrupt the output.
56
73
  *
57
74
  * Every JS-facing method that takes data receives a single [ReadableMap] (the object
58
- * param the TS Spec declares), matching [com.rezaul.printerkit.BluetoothPrinter]'s own
75
+ * param the TS Spec declares), matching [com.rezaul.printerkit.PosPrinter]'s own
59
76
  * single-params-object functions on the native side.
60
77
  */
61
78
  class ReactNativePrinterKitModule(reactContext: ReactApplicationContext) :
@@ -63,17 +80,17 @@ class ReactNativePrinterKitModule(reactContext: ReactApplicationContext) :
63
80
 
64
81
  private val executor = Executors.newSingleThreadExecutor()
65
82
 
66
- // getBondedBluetoothPrinters() is synchronous (no connection involved) and must
67
- // answer immediately, so it uses its own plain BluetoothPrinter rather than
68
- // waiting on the service to bind.
69
- private val standalonePrinter by lazy { BluetoothPrinter(reactApplicationContext) }
83
+ // getBondedBluetoothPrinters(), getUsbPrinters() and the USB permission calls need no
84
+ // connection and must answer immediately, so they use their own plain PosPrinter
85
+ // rather than waiting on the service to bind.
86
+ private val standalonePrinter by lazy { PosPrinter(reactApplicationContext) }
70
87
 
71
- private var printerService: BluetoothPrinterService? = null
72
- private val pendingActions = mutableListOf<(BluetoothPrinterService) -> Unit>()
88
+ private var printerService: PrinterService? = null
89
+ private val pendingActions = mutableListOf<(PrinterService) -> Unit>()
73
90
 
74
91
  private val serviceConnection = object : ServiceConnection {
75
92
  override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
76
- val service = (binder as BluetoothPrinterService.LocalBinder).getService()
93
+ val service = (binder as PrinterService.LocalBinder).getService()
77
94
  printerService = service
78
95
  val queued = synchronized(pendingActions) {
79
96
  val copy = pendingActions.toList()
@@ -89,13 +106,13 @@ class ReactNativePrinterKitModule(reactContext: ReactApplicationContext) :
89
106
  }
90
107
 
91
108
  init {
92
- val intent = Intent(reactApplicationContext, BluetoothPrinterService::class.java)
109
+ val intent = Intent(reactApplicationContext, PrinterService::class.java)
93
110
  reactApplicationContext.startService(intent)
94
111
  reactApplicationContext.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
95
112
  }
96
113
 
97
114
  /** Runs [action] with the bound service, once available (queued if not yet bound). */
98
- private fun withService(action: (BluetoothPrinterService) -> Unit) {
115
+ private fun withService(action: (PrinterService) -> Unit) {
99
116
  val service = printerService
100
117
  if (service != null) {
101
118
  action(service)
@@ -108,6 +125,17 @@ class ReactNativePrinterKitModule(reactContext: ReactApplicationContext) :
108
125
  val map = Arguments.createMap()
109
126
  map.putString("name", device.name)
110
127
  map.putString("address", device.address)
128
+ map.putString("confidence", device.confidence.name)
129
+ map.putString("reason", device.reason)
130
+ return map
131
+ }
132
+
133
+ private fun usbToMap(device: UsbPrinterDevice): WritableMap {
134
+ val map = Arguments.createMap()
135
+ map.putString("name", device.name)
136
+ map.putInt("vendorId", device.vendorId)
137
+ map.putInt("productId", device.productId)
138
+ map.putString("deviceName", device.deviceName)
111
139
  return map
112
140
  }
113
141
 
@@ -155,24 +183,74 @@ class ReactNativePrinterKitModule(reactContext: ReactApplicationContext) :
155
183
  }
156
184
  }
157
185
 
158
- override fun getBondedBluetoothPrinters(): WritableArray {
186
+ override fun getBondedBluetoothPrinters(params: ReadableMap): WritableArray {
187
+ val filter = enumOrDefault(params.optString("filter"), PrinterFilter.ALL)
188
+ val keywords = params.getArray("extraNameKeywords")?.let { array ->
189
+ (0 until array.size()).mapNotNull { array.getString(it) }
190
+ } ?: emptyList()
159
191
  val result: WritableArray = Arguments.createArray()
160
- standalonePrinter.getBondedBluetoothPrinters().forEach { device ->
192
+ standalonePrinter.getBondedBluetoothPrinters(filter, keywords).forEach { device ->
161
193
  result.pushMap(deviceToMap(device))
162
194
  }
163
195
  return result
164
196
  }
165
197
 
198
+ override fun getUsbPrinters(): WritableArray {
199
+ val result: WritableArray = Arguments.createArray()
200
+ standalonePrinter.getUsbPrinters().forEach { device ->
201
+ result.pushMap(usbToMap(device))
202
+ }
203
+ return result
204
+ }
205
+
206
+ override fun hasUsbPermission(params: ReadableMap): Boolean {
207
+ if (!params.hasKey("vendorId") || !params.hasKey("productId")) return false
208
+ return standalonePrinter.hasUsbPermission(
209
+ params.getDouble("vendorId").toInt(),
210
+ params.getDouble("productId").toInt()
211
+ )
212
+ }
213
+
214
+ override fun requestUsbPermission(params: ReadableMap, promise: Promise) {
215
+ if (!params.hasKey("vendorId") || !params.hasKey("productId")) {
216
+ promise.reject("ERROR", "vendorId and productId are required")
217
+ return
218
+ }
219
+ try {
220
+ // The result arrives on the main thread once the user answers the system dialog.
221
+ standalonePrinter.requestUsbPermission(
222
+ params.getDouble("vendorId").toInt(),
223
+ params.getDouble("productId").toInt()
224
+ ) { granted -> promise.resolve(granted) }
225
+ } catch (e: Exception) {
226
+ promise.reject("ERROR", e.message)
227
+ }
228
+ }
229
+
166
230
  override fun connectPrinter(params: ReadableMap, promise: Promise) {
167
- val address = params.getString("address")
231
+ val type = enumOrDefault(params.optString("type"), PrinterConnectionType.BLUETOOTH)
232
+ // USB printers are identified by vendorId/productId, so address may be empty for them.
233
+ val address = params.optString("address") ?: if (type == PrinterConnectionType.USB) "" else null
168
234
  if (address == null) {
169
235
  promise.reject("ERROR", "address is required")
170
236
  return
171
237
  }
238
+ val defaults = ConnectPrinterParams(address)
239
+ val connectParams = ConnectPrinterParams(
240
+ address = address,
241
+ notificationTitle = params.optString("notificationTitle") ?: defaults.notificationTitle,
242
+ type = type,
243
+ port = params.optInt("port", defaults.port),
244
+ timeoutMs = params.optInt("timeoutMs", defaults.timeoutMs),
245
+ vendorId = params.optIntOrNull("vendorId"),
246
+ productId = params.optIntOrNull("productId"),
247
+ disconnectedNotificationTitle =
248
+ params.optString("disconnectedNotificationTitle") ?: defaults.disconnectedNotificationTitle
249
+ )
172
250
  withService { service ->
173
251
  executor.execute {
174
252
  try {
175
- promise.resolve(service.connectAndKeepAlive(ConnectPrinterParams(address)))
253
+ promise.resolve(service.connectAndKeepAlive(connectParams))
176
254
  } catch (e: Exception) {
177
255
  promise.reject("ERROR", e.message)
178
256
  }
@@ -218,6 +296,72 @@ class ReactNativePrinterKitModule(reactContext: ReactApplicationContext) :
218
296
  }
219
297
  }
220
298
 
299
+ override fun isPrinterReady(promise: Promise) {
300
+ withService { service ->
301
+ executor.execute {
302
+ try {
303
+ promise.resolve(service.printer.isPrinterReady())
304
+ } catch (e: Exception) {
305
+ promise.reject("ERROR", e.message)
306
+ }
307
+ }
308
+ }
309
+ }
310
+
311
+ override fun getPrinterStatus(params: ReadableMap, promise: Promise) {
312
+ withService { service ->
313
+ executor.execute {
314
+ try {
315
+ val status = service.printer.getPrinterStatus(params.optInt("timeoutMs", 800))
316
+ // Unknown (null) fields must reach JS as null, which makeNativeMap preserves.
317
+ promise.resolve(Arguments.makeNativeMap(status.toMap()))
318
+ } catch (e: Exception) {
319
+ promise.reject("ERROR", e.message)
320
+ }
321
+ }
322
+ }
323
+ }
324
+
325
+ override fun printTestPage(promise: Promise) {
326
+ withService { service ->
327
+ executor.execute {
328
+ try {
329
+ service.printer.printTestPage()
330
+ promise.resolve(null)
331
+ } catch (e: Exception) {
332
+ promise.reject("ERROR", e.message)
333
+ }
334
+ }
335
+ }
336
+ }
337
+
338
+ override fun saveFile(params: ReadableMap, promise: Promise) {
339
+ val sourcePath = params.getString("sourcePath")
340
+ if (sourcePath == null) {
341
+ promise.reject("ERROR", "sourcePath is required")
342
+ return
343
+ }
344
+ executor.execute {
345
+ try {
346
+ val result = standalonePrinter.saveFile(
347
+ SaveFileParams(
348
+ sourcePath = sourcePath,
349
+ fileName = params.optString("fileName"),
350
+ location = enumOrDefault(params.optString("location"), FileLocation.DOWNLOADS),
351
+ customFolder = params.optString("customFolder"),
352
+ subFolder = params.optString("subFolder"),
353
+ mimeType = params.optString("mimeType"),
354
+ overwrite = params.optBoolean("overwrite", false),
355
+ deleteSource = params.optBoolean("deleteSource", false)
356
+ )
357
+ )
358
+ promise.resolve(Arguments.makeNativeMap(result.toMap()))
359
+ } catch (e: Exception) {
360
+ promise.reject("ERROR", e.message)
361
+ }
362
+ }
363
+ }
364
+
221
365
  override fun printText(params: ReadableMap, promise: Promise) {
222
366
  val text = params.getString("text")
223
367
  if (text == null) {
@@ -1,6 +1,15 @@
1
1
  "use strict";
2
2
 
3
3
  import { TurboModuleRegistry, Platform } from 'react-native';
4
+
5
+ /** How sure the library is that a paired Bluetooth device is a printer. */
6
+
7
+ /** `ALL` lists every paired device, `LIKELY_PRINTERS` hides non-printers, `STRICT` keeps only certain ones. */
8
+
9
+ /**
10
+ * `null` means "unknown" - the printer did not answer or does not report that condition. It is NOT "ok".
11
+ */
12
+
4
13
  function getModule() {
5
14
  return Platform.OS === 'android' ? TurboModuleRegistry.getEnforcing('ReactNativePrinterKit') : {};
6
15
  }
@@ -1 +1 @@
1
- {"version":3,"names":["TurboModuleRegistry","Platform","getModule","OS","getEnforcing"],"sourceRoot":"../../src","sources":["NativeReactNativePrinterKit.ts"],"mappings":";;AAAA,SAASA,mBAAmB,EAAoBC,QAAQ,QAAQ,cAAc;AA2L9E,SAASC,SAASA,CAAA,EAAS;EACzB,OAAOD,QAAQ,CAACE,EAAE,KAAK,SAAS,GAC5BH,mBAAmB,CAACI,YAAY,CAAO,uBAAuB,CAAC,GAC9D,CAAC,CAAU;AAClB;AAEA,eAAeF,SAAS,CAAC,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["TurboModuleRegistry","Platform","getModule","OS","getEnforcing"],"sourceRoot":"../../src","sources":["NativeReactNativePrinterKit.ts"],"mappings":";;AAAA,SAASA,mBAAmB,EAAoBC,QAAQ,QAAQ,cAAc;;AAE9E;;AAGA;;AA2DA;AACA;AACA;;AA8PA,SAASC,SAASA,CAAA,EAAS;EACzB,OAAOD,QAAQ,CAACE,EAAE,KAAK,SAAS,GAC5BH,mBAAmB,CAACI,YAAY,CAAO,uBAAuB,CAAC,GAC9D,CAAC,CAAU;AAClB;AAEA,eAAeF,SAAS,CAAC,CAAC","ignoreList":[]}
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+
3
+ // Runtime values for the string unions the native module uses, so callers can write
4
+ // `PrinterFilter.LIKELY_PRINTERS` instead of a bare string. Each object is checked
5
+ // against its union with `satisfies`, so a typo or a value the native side does not
6
+ // know fails to compile. The plain strings keep working everywhere too.
7
+
8
+ /** How sure the library is that a paired Bluetooth device is a printer. */
9
+ export const PrinterConfidence = {
10
+ CONFIDENT: 'CONFIDENT',
11
+ LIKELY: 'LIKELY',
12
+ UNKNOWN: 'UNKNOWN'
13
+ };
14
+ /** `ALL` lists every paired device, `LIKELY_PRINTERS` hides non-printers, `STRICT` keeps only certain ones. */
15
+ export const PrinterFilter = {
16
+ ALL: 'ALL',
17
+ LIKELY_PRINTERS: 'LIKELY_PRINTERS',
18
+ STRICT: 'STRICT'
19
+ };
20
+ export const PrinterConnectionType = {
21
+ BLUETOOTH: 'BLUETOOTH',
22
+ NETWORK: 'NETWORK',
23
+ USB: 'USB'
24
+ };
25
+ export const PrinterImageType = {
26
+ PNG: 'PNG',
27
+ JPEG: 'JPEG'
28
+ };
29
+ export const FileLocation = {
30
+ CACHE: 'CACHE',
31
+ APP_FILES: 'APP_FILES',
32
+ DOWNLOADS: 'DOWNLOADS',
33
+ DOCUMENTS: 'DOCUMENTS',
34
+ CUSTOM_FOLDER: 'CUSTOM_FOLDER'
35
+ };
36
+ //# sourceMappingURL=enums.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["PrinterConfidence","CONFIDENT","LIKELY","UNKNOWN","PrinterFilter","ALL","LIKELY_PRINTERS","STRICT","PrinterConnectionType","BLUETOOTH","NETWORK","USB","PrinterImageType","PNG","JPEG","FileLocation","CACHE","APP_FILES","DOWNLOADS","DOCUMENTS","CUSTOM_FOLDER"],"sourceRoot":"../../src","sources":["enums.ts"],"mappings":";;AAQA;AACA;AACA;AACA;;AAEA;AACA,OAAO,MAAMA,iBAAiB,GAAG;EAC/BC,SAAS,EAAE,WAAW;EACtBC,MAAM,EAAE,QAAQ;EAChBC,OAAO,EAAE;AACX,CAA2E;AAI3E;AACA,OAAO,MAAMC,aAAa,GAAG;EAC3BC,GAAG,EAAE,KAAK;EACVC,eAAe,EAAE,iBAAiB;EAClCC,MAAM,EAAE;AACV,CAAmE;AAGnE,OAAO,MAAMC,qBAAqB,GAAG;EACnCC,SAAS,EAAE,WAAW;EACtBC,OAAO,EAAE,SAAS;EAClBC,GAAG,EAAE;AACP,CAGC;AAID,OAAO,MAAMC,gBAAgB,GAAG;EAC9BC,GAAG,EAAE,KAAK;EACVC,IAAI,EAAE;AACR,CAAyE;AAIzE,OAAO,MAAMC,YAAY,GAAG;EAC1BC,KAAK,EAAE,OAAO;EACdC,SAAS,EAAE,WAAW;EACtBC,SAAS,EAAE,WAAW;EACtBC,SAAS,EAAE,WAAW;EACtBC,aAAa,EAAE;AACjB,CAAiE","ignoreList":[]}