React Native Thermal Printer Library with ESC/POS, CPCL, TSPL support
npm install @finan-me/react-native-thermal-printerReact Native library for ESC/POS thermal printers with Bluetooth, BLE, and LAN support.
- ✅ Multi-connection: Bluetooth Classic, BLE, LAN/WiFi
- ✅ Vietnamese support: Full CP1258 encoding
- ✅ Rich printing: Text, images, QR, barcodes, tables
- ✅ Multi-printer: Print to multiple printers concurrently
- ✅ Margin & Alignment: Consistent margins across all printer types
- ✅ Cross-platform: Android & iOS
``bash`
yarn add @finan-me/react-native-thermal-printer
`bash`
cd ios && pod install
Add to Info.plist:
`xml
`
Notes:
- iOS primarily uses BLE for thermal printers
- Bluetooth Classic requires MFi certification (most printers don't have)
- User will see permission prompt on first Bluetooth access
Add to AndroidManifest.xml:
`xml
android:usesPermissionFlags="neverForLocation"
tools:targetApi="31" />
tools:targetApi="31" />
android:maxSdkVersion="30" />
android:maxSdkVersion="30" />
android:maxSdkVersion="30" />
`
Permission Explanations:
| Permission | API Level | Purpose | Required? |
| ---------------------- | --------- | ---------------------------- | -------------------- |
| BLUETOOTH_SCAN | 31+ | Scan for Bluetooth devices | ✅ Yes |BLUETOOTH_CONNECT
| | 31+ | Connect to Bluetooth devices | ✅ Yes |BLUETOOTH
| | ≤30 | Legacy Bluetooth access | ✅ Yes (old Android) |BLUETOOTH_ADMIN
| | ≤30 | Legacy Bluetooth discovery | ✅ Yes (old Android) |ACCESS_FINE_LOCATION
| | ≤30 | BLE scan on old Android | ✅ Yes (old Android) |INTERNET
| | All | LAN/WiFi printing | ✅ Yes |ACCESS_NETWORK_STATE
| | All | Check network connectivity | ✅ Yes |
neverForLocation Flag:
`xml`
android:usesPermissionFlags="neverForLocation"
tools:targetApi="31" />
- Purpose: Tells Android you DON'T use Bluetooth for location tracking
- Effect: User won't see "Location" in permission prompt
- When to use: When you ONLY scan Bluetooth for printers (not for location)
Without neverForLocation: ❌ "App wants to access Bluetooth and Location"neverForLocation
With : ✅ "App wants to access Nearby devices"
Runtime Permissions:
`typescript
import {PermissionsAndroid, Platform} from 'react-native'
async function requestBluetoothPermissions() {
if (Platform.OS === 'android') {
if (Platform.Version >= 31) {
// Android 12+
const granted = await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN,
PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT,
])
return (
granted['android.permission.BLUETOOTH_SCAN'] === 'granted' &&
granted['android.permission.BLUETOOTH_CONNECT'] === 'granted'
)
} else {
// Android 11 and below
const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION)
return granted === 'granted'
}
}
return true // iOS handles automatically
}
// Use before scanning
const hasPermission = await requestBluetoothPermissions()
if (hasPermission) {
await ThermalPrinter.scanDevices()
}
`
`typescript
import {ThermalPrinter} from '@finan-me/react-native-thermal-printer'
// 1. Scan devices
const {paired, found} = await ThermalPrinter.scanDevices()
// 2. Print receipt
const job = {
printers: [
{
address: 'bt:AA:BB:CC:DD:EE:FF',
options: {
paperWidthMm: 58,
encoding: 'CP1258', // Vietnamese
marginMm: 1, // 1mm margin each side (default)
},
},
],
documents: [
[
// Header
{type: 'text', content: 'COFFEE SHOP', style: {align: 'center', bold: true, size: 'double'}},
{type: 'text', content: '123 Main St', style: {align: 'center'}},
{type: 'line'},
// Table
{
type: 'table',
headers: ['Item', 'Qty', 'Price'],
rows: [
['Cappuccino', '2', '90.000đ'],
['Sandwich', '1', '35.000đ'],
],
columnWidths: [50, 20, 30],
alignments: ['left', 'center', 'right'],
},
// Total
{type: 'line'},
{type: 'text', content: 'TOTAL: 125.000đ', style: {bold: true, size: 'double_width'}},
// QR payment
{type: 'qr', content: 'https://payment.link/123', size: 6, align: 'center'},
// Footer
{type: 'text', content: 'Cảm ơn quý khách!', style: {align: 'center'}},
{type: 'feed', lines: 3},
{type: 'cut'},
],
],
}
await ThermalPrinter.printReceipt(job)
`
| Type | Format | Example |
| ----------------- | ------------- | ------------------------ |
| Bluetooth Classic | bt:MAC | bt:AA:BB:CC:DD:EE:FF |ble:MAC
| BLE | | ble:AA:BB:CC:DD:EE:FF |lan:IP:PORT
| LAN/WiFi | | lan:192.168.1.100:9100 |
Text: {type: 'text', content: 'Hello', style: {align: 'center', bold: true, size: 'double'}}
Line: {type: 'line'}
Table: {type: 'table', headers: ['A', 'B'], rows: [['1', '2']], columnWidths: [50, 50]}
Columns: {type: 'columns', columns: [{content: 'Left', width: 50}, {content: 'Right', width: 50, align: 'right'}]}
QR Code: {type: 'qr', content: 'https://...', size: 6, align: 'center'}
Barcode: {type: 'barcode', content: '123456', format: 'CODE128', align: 'center'}
Image: {type: 'image', imagePath: '/path/to/image.png', options: {align: 'center', marginMm: 2}}
Feed: {type: 'feed', lines: 3}
Spacer: {type: 'spacer', height: 2, fill: '-'}
Cut: {type: 'cut', partial: true}
`typescript`
{
paperWidthMm?: 32 | 58 | 80, // default: 58
encoding?: 'CP1258' | 'UTF8' | 'ASCII', // default: CP1258
marginMm?: number, // default: 1mm each side
keepAlive?: boolean
}
`typescript`
{
concurrent?: boolean, // print to multiple printers in parallel
continueOnError?: boolean, // continue if one printer fails
onProgress?: (completed: number, total: number) => void,
onJobComplete?: (address: string, success: boolean) => void
}
`typescript`
{
address: string,
copies?: number, // number of copies (default: 1)
delayBetweenCopies?: number, // delay in ms (default: 200)
options?: PrinterOptions
}
`typescript${completed}/${total}
const job = {
printers: [
{address: 'bt:11:11:11:11:11:11', copies: 2}, // Kitchen: 2 copies
{address: 'lan:192.168.1.100:9100'}, // Counter: 1 copy
],
documents: [[{type: 'text', content: 'Order #123'}, {type: 'cut'}]],
options: {
concurrent: true, // Print in parallel
continueOnError: true,
onProgress: (completed, total) => console.log(),
},
}
const result = await ThermalPrinter.printReceipt(job)
// result.success, result.results (per-printer status)
`
`typescript`
{
options: {
encoding: 'CP1258'
}
}
`typescript`
try {
await ThermalPrinter.printReceipt(job)
} catch (error) {
console.log(error.code) // E1001, E2001, E4003...
console.log(error.message) // Human readable
console.log(error.suggestion) // How to fix
console.log(error.retryable) // Can retry?
}
Vietnamese not printing?
- Use encoding: 'CP1258'testCodepages()
- Test with utility
Connection timeout?
- Check printer is on and in range
- Use testConnection()` before printing
Image not printing?
- Use local file path (not base64)
- Images auto-resize to paper width
MIT