npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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

npm install @honeypathkar/react-native-thermal-printer

Android 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/POS

All 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