@honeypathkar/react-native-thermal-printer
v1.1.0
Published
Thermal receipt printing for React Native. One receipt definition drives Sunmi built-in heads, generic ESC/POS printers over Classic Bluetooth, and BLE pocket "cat printers" (X5 / MX05 / GB01) that accept raster images only. Includes fixed-width receipt l
Maintainers
Readme
@honeypathkar/react-native-thermal-printer
Thermal receipt printing for React Native. One receipt definition drives three completely different kinds of printer.
| Printer | Speaks | Reports status back? | |---|---|---| | Sunmi built-in head (V2 / T2 / D2) | AIDL method calls | Yes — paper, cover, temperature | | Counter-top Bluetooth (generic 58/80 mm) | ESC/POS over RFCOMM | No | | Pocket "cat printer" (X5, MX05, GB01, GB02) | 1-bit raster over BLE GATT | No |
The last one accepts no text at all — only images. This library rasterises the receipt natively so the same layout reaches every printer.
Android only. iOS has no equivalent API for any of these transports. Every function returns a safe default on iOS rather than throwing, so shared code needs no platform branching.
Contents
- Install
- Quick start
- Choosing a printer
- Receipt blocks
- Printing plain text
- Print darkness
- Paper widths
- Bluetooth device info
- Rendering without printing
- What each transport can tell you
- Troubleshooting
- Supported printers
- API reference
Install
npm install @honeypathkar/react-native-thermal-printerAndroid setup
Autolinking handles Gradle. Register the package in
android/app/src/main/java/.../MainApplication.kt:
import com.honeypathkar.thermalprinter.ThermalPrinterPackage
override val reactHost: ReactHost by lazy {
getDefaultReactHost(
context = applicationContext,
packageList = PackageList(this).packages.apply {
add(ThermalPrinterPackage())
},
)
}Permissions are merged in from the library manifest — you do not need to
declare BLUETOOTH_CONNECT, BLUETOOTH_SCAN or the Sunmi <queries> entry
yourself. Runtime consent is requested for you on first use.
Minimum SDK 21. No vendor SDK, AAR or developer account is required — the Sunmi integration binds the printer service through its published AIDL contract.
Quick start
import {
usePrinter,
type ReceiptBlock,
} from '@honeypathkar/react-native-thermal-printer';
const blocks: ReceiptBlock[] = [
{ type: 'text', value: 'My Store', align: 'center', bold: true, size: 'large' },
{ type: 'divider' },
{ type: 'row', left: 'Invoice', right: 'INV-001' },
{ type: 'item', name: 'Green Tea', quantity: 2, price: 120, amount: 240 },
{ type: 'divider', char: '=' },
{ type: 'row', left: 'TOTAL', right: '240.00', bold: true, size: 'large' },
{ type: 'feed', lines: 2 },
{ type: 'cut' },
];
function Checkout() {
const { print, status, canConfirmPrint } = usePrinter({
paperWidth: '58mm',
externalAdapter: 'ble',
});
const onPrint = async () => {
const result = await print(blocks);
if (!result.success) {
console.warn(result.error);
return;
}
// Bluetooth and BLE cannot confirm paper actually moved
console.log(canConfirmPrint ? 'Printed' : 'Sent to printer');
};
}usePrinter auto-detects: a built-in head when the device has one, otherwise the
externalAdapter you name. It re-checks status when the app returns to the
foreground — typically the moment a merchant has reloaded paper.
Without the hook
import { printReceipt, hasBuiltInPrinter } from '@honeypathkar/react-native-thermal-printer';
const adapter = (await hasBuiltInPrinter()) ? 'built_in' : 'ble';
const result = await printReceipt(blocks, adapter, { paperWidth: '58mm', copies: 1 });Choosing a printer
BLE pocket printers
import {
scanForBlePrinters,
filterLikelyPrinters,
connectBlePrinter,
} from '@honeypathkar/react-native-thermal-printer';
const { devices, error } = await scanForBlePrinters();
if (error === 'bluetooth_off') { /* prompt the user */ }
// Drops earbuds, speakers and controllers, keeps everything that could be a printer
const candidates = filterLikelyPrinters(devices);
await connectBlePrinter(candidates[0].address);The scan includes bonded devices as well as advertising ones. A printer that is already paired and connected stops advertising, so a scan alone would never find the very device the user just set up.
filterLikelyPrinters filters on "definitely not a printer" — audio, input,
phone, computer, wearable classes — rather than "is a printer". Most pocket
printers report no printer class at all, so the stricter test would hide them. If
filtering would empty the list it returns everything, so an oddly classified
printer can never become unreachable.
Classic Bluetooth (ESC/POS)
import { getPairedPrinters, connectBluetoothPrinter } from '@honeypathkar/react-native-thermal-printer';
const paired = await getPairedPrinters(); // bonded devices from Android settings
await connectBluetoothPrinter(paired[0].address);Pairing itself is Android's job — it needs a PIN. This lists what the OS already knows rather than duplicating that flow.
Receipt blocks
Blocks are semantic, not positional. They describe what a line means, and each renderer lays it out for its own medium — which is what lets an on-screen preview match the paper exactly.
| Block | Renders as |
|---|---|
| { type: 'text', value, align?, bold?, size? } | A line, optionally centred/right, bold, double-width |
| { type: 'row', left, right, bold?, size? } | Label left, value hard right |
| { type: 'item', name, quantity, price, amount } | Product name, then indented qty x price with the total right |
| { type: 'divider', char? } | A full-width rule of any character |
| { type: 'feed', lines? } | Blank lines |
| { type: 'cut' } | Paper cut (ignored by printers without a cutter) |
align is 'left' | 'center' | 'right', size is 'normal' | 'large'.
A double-width heading too wide for the paper automatically drops to normal size rather than wrapping mid-title. Long product names wrap; the quantity and amount are never sacrificed to fit them.
Printing plain text
A receipt is just an array of blocks, so a single name is one block. These helpers save assembling the trailing feed and cut every time:
import { printText, textBlocks, labelledTextBlocks } from '@honeypathkar/react-native-thermal-printer';
await printText('Honey Pathkar', 'ble');
await printText(['Table 4', 'Order 118'], 'ble', { size: 'large' });
// Or build blocks to combine with others
const blocks = labelledTextBlocks('TOKEN', 'A-42');Text printing goes through the same pipeline as a receipt — same adapter, darkness and reconnect logic — so if a line prints, receipts will too. That makes it a genuine diagnostic rather than a separate path that might succeed while real printing is broken.
Print darkness
await printReceipt(blocks, adapter, { quality: 'high' });| Quality | BLE energy | ESC/POS heating |
|---|---|---|
| low | 8000 | 60 |
| normal (default) | 12000 | 80 |
| high | 20000 | 140 |
Darker burns a heavier dot — clearer on cheap or aged paper, but slower and harder on battery and print-head heat.
Not supported on a Sunmi built-in head: its AIDL exposes no density command,
so the head manages its own darkness and quality is ignored there rather than
silently pretending to work.
Paper widths
| Width | Columns | Dots |
|---|---|---|
| '58mm' | 32 | 384 |
| '80mm' | 42 | 576 |
The same blocks reflow to either. Column counts assume Font A on a 203 dpi head.
Bluetooth device info
import {
getAdapterState,
getBondedDevices,
getDeviceInfo,
isDeviceConnected,
} from '@honeypathkar/react-native-thermal-printer';
const { supported, enabled } = await getAdapterState();
const devices = await getBondedDevices();
// [{ name, address, bondState, transport, deviceClassName, looksLikePrinter }]transport is 'classic' | 'ble' | 'dual' | 'unknown' — the most useful field
when a printer will not work. It tells you which radio a device actually speaks
before you attempt a connection, which distinguishes "wrong transport" from
"broken printer" in one call.
Rendering without printing
import {
renderReceiptText,
renderReceiptLines,
buildEscPosBytes,
} from '@honeypathkar/react-native-thermal-printer';
renderReceiptText(blocks, '58mm'); // string — previews and tests
renderReceiptLines(blocks, '58mm'); // [{ text, align, bold, size }]
buildEscPosBytes(blocks, '58mm'); // number[] — raw ESC/POSAll pure functions with no native dependency, so receipt layout can be unit-tested off-device.
What each transport can tell you
Only a built-in head reports real status. getPrinterStatus('built_in')
returns out_of_paper, cover_open and overheated.
Classic SPP and BLE are write-only. A successful write proves the bytes left the
phone — nothing more. Use isOptimisticAdapter(adapter), or the hook's
canConfirmPrint, to word your success message honestly:
showToast(canConfirmPrint ? 'Receipt printed' : 'Receipt sent to printer');Telling a merchant a receipt printed when it may not have is worse than saying less.
Troubleshooting
"Connected, reports success, nothing prints." Every cause below is already handled internally, listed here because they are what to check on a new printer:
- ESC/POS buffer overrun. Low-cost printers have ~256-byte firmware buffers. Writes are chunked to 128 bytes with a pause; sending a whole receipt at once is silently discarded.
- BLE MTU. One raster row is a 56-byte packet but the default ATT MTU allows 20 usable bytes. The module negotiates a larger MTU and splits packets to fit.
- Padding must survive encoding. Leading spaces are the centring on a fixed character grid. Trimming them leaves every centred line flush-left on paper while previews still look correct.
BLE scan finds nothing. The library manifest declares BLUETOOTH_SCAN with
neverForLocation. Without that flag Android 12+ withholds scan results unless
Location Services are on, and the callback never fires — no error, no devices. If
you override the manifest, keep the flag.
A paired printer is missing from the scan. Expected — a connected device stops
advertising. scanForBlePrinters seeds the list with bonded devices for exactly
this reason.
Sunmi head not found. Check buildFeatures { aidl true } if you have
customised the library build, and that the <queries> entry survived manifest
merging. Android 11+ hides the printer service without it and bindService()
fails silently.
Supported printers
BLE — the 0x51 0x78 cat-printer family: X5, X6, MX05, MX06, MX09, GB01,
GB02, GT01 and clones on the same board, including those sold with companion
apps like Tiny Print.
Not supported: PeriPage and Paperang use different protocols. Clones with different GATT UUIDs will connect and then find no write characteristic.
Classic Bluetooth — any printer that accepts ESC/POS over SPP, which is effectively every generic 58 mm and 80 mm counter-top unit.
Built-in — Sunmi and terminals that ship the same woyou.aidlservice.jiuiv5
service. Other OEMs (iMin, Telpo, PAX, Urovo) each ship their own SDK and are not
supported; on those devices hasBuiltInPrinter() returns false and you can fall
back to an external printer.
API reference
Printing
printReceipt(blocks, adapter, options) · printText(value, adapter, options) ·
usePrinter(options) · getPrinterStatus(adapter) · hasBuiltInPrinter() ·
feedPaper(lines) · cutPaper() · isOptimisticAdapter(adapter)
BLE
scanForBlePrinters() · filterLikelyPrinters(devices) ·
connectBlePrinter(address) · isBlePrinterConnected() · disconnectBlePrinter()
Classic Bluetooth
getPairedPrinters() · connectBluetoothPrinter(address) ·
isBluetoothPrinterConnected() · disconnectBluetoothPrinter()
Device info
getAdapterState() · getBondedDevices() · getDeviceInfo(address) ·
isDeviceConnected(address) · requestBluetoothPermissions()
Layout
renderReceiptLines(blocks, paperWidth) · renderReceiptText(blocks, paperWidth) ·
buildEscPosBytes(blocks, paperWidth, quality) ·
buildEscPosBase64(blocks, paperWidth, quality) · textBlocks(value, options) ·
labelledTextBlocks(label, value, options) · getPaperProfile(paperWidth) ·
getQualityProfile(quality) · effectiveColumns(columns, size)
Text helpers
toPrinterAscii · toAscii · truncate · wrapText · padLeft · padRight ·
padCenter · formatMoney
Types
ReceiptBlock · RenderedLine · PrinterAdapter · PrinterState ·
PrinterStatus · PrintResult · PrintOptions · PrintQuality · PaperWidth ·
BluetoothDevice · BluetoothDeviceInfo · AdapterState · ScanOutcome
License
MIT
