@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 +174 -13
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/rejaul/reactnativeprinterkit/ReactNativePrinterKitModule.kt +175 -31
- package/lib/module/NativeReactNativePrinterKit.js +9 -0
- package/lib/module/NativeReactNativePrinterKit.js.map +1 -1
- package/lib/module/enums.js +36 -0
- package/lib/module/enums.js.map +1 -0
- package/lib/module/index.js +69 -6
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/NativeReactNativePrinterKit.d.ts +117 -5
- package/lib/typescript/src/NativeReactNativePrinterKit.d.ts.map +1 -1
- package/lib/typescript/src/enums.d.ts +34 -0
- package/lib/typescript/src/enums.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +47 -7
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeReactNativePrinterKit.ts +138 -5
- package/src/enums.ts +56 -0
- package/src/index.tsx +98 -9
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
|
|
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
|
[](https://www.npmjs.com/package/@rejaul/react-native-printer-kit)
|
|
8
8
|
[](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
|
-
- **
|
|
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 **
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
package/android/build.gradle
CHANGED
package/android/src/main/java/com/rejaul/reactnativeprinterkit/ReactNativePrinterKitModule.kt
CHANGED
|
@@ -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.
|
|
19
|
-
import com.rezaul.printerkit.
|
|
20
|
-
import com.rezaul.printerkit.
|
|
21
|
-
import com.rezaul.printerkit.ConnectPrinterParams
|
|
22
|
-
import com.rezaul.printerkit.
|
|
23
|
-
import com.rezaul.printerkit.
|
|
24
|
-
import com.rezaul.printerkit.
|
|
25
|
-
import com.rezaul.printerkit.
|
|
26
|
-
import com.rezaul.printerkit.
|
|
27
|
-
import com.rezaul.printerkit.
|
|
28
|
-
import com.rezaul.printerkit.
|
|
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.
|
|
62
|
+
* Bridges [com.rezaul.printerkit.PosPrinter] to React Native.
|
|
46
63
|
*
|
|
47
|
-
* Connect/print calls run through a bound [
|
|
48
|
-
* on module init) instead of a bare [
|
|
49
|
-
* the app being swiped away from Recents - see [
|
|
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.
|
|
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()
|
|
67
|
-
// answer immediately, so
|
|
68
|
-
// waiting on the service to bind.
|
|
69
|
-
private val standalonePrinter by lazy {
|
|
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:
|
|
72
|
-
private val pendingActions = mutableListOf<(
|
|
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
|
|
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,
|
|
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: (
|
|
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
|
|
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(
|
|
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;
|
|
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":[]}
|