@finan-me/react-native-thermal-printer
v1.0.9
Published
React Native Thermal Printer Library with ESC/POS, CPCL, TSPL support
Readme
@finan-me/react-native-thermal-printer
React Native library for ESC/POS thermal printers with Bluetooth, BLE, and LAN support.
Features
- ✅ 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
Installation
yarn add @finan-me/react-native-thermal-printeriOS Setup
cd ios && pod installAdd to Info.plist:
<!-- Required: Bluetooth access for connecting to printers -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app needs Bluetooth access to connect to thermal printers for receipt printing</string>
<!-- iOS 13+: Required for discovering BLE printers -->
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app needs Bluetooth to discover and connect to thermal printers</string>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
Android Setup
Add to AndroidManifest.xml:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- ========== NETWORK PERMISSIONS ========== -->
<!-- Required: For LAN/WiFi printing -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- ========== ANDROID 12+ (API 31+) ========== -->
<!-- Required: Scan for Bluetooth devices -->
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="31" />
<!-- Required: Connect to Bluetooth devices -->
<uses-permission
android:name="android.permission.BLUETOOTH_CONNECT"
tools:targetApi="31" />
<!-- ========== ANDROID 11 AND BELOW (API ≤30) ========== -->
<!-- Required: Legacy Bluetooth permissions -->
<uses-permission
android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission
android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<!-- Required: BLE scan requires location on API ≤30 -->
<uses-permission
android:name="android.permission.ACCESS_FINE_LOCATION"
android:maxSdkVersion="30" />
</manifest>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:
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
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"
With neverForLocation: ✅ "App wants to access Nearby devices"
Runtime Permissions:
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()
}Quick Start
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)Address Format
| Type | Format | Example |
| ----------------- | ------------- | ------------------------ |
| Bluetooth Classic | bt:MAC | bt:AA:BB:CC:DD:EE:FF |
| BLE | ble:MAC | ble:AA:BB:CC:DD:EE:FF |
| LAN/WiFi | lan:IP:PORT | lan:192.168.1.100:9100 |
Supported Content Types
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}
Options
Printer Options
{
paperWidthMm?: 32 | 58 | 80, // default: 58
encoding?: 'CP1258' | 'UTF8' | 'ASCII', // default: CP1258
marginMm?: number, // default: 1mm each side
keepAlive?: boolean
}Job Options
{
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
}Print Configuration
{
address: string,
copies?: number, // number of copies (default: 1)
delayBetweenCopies?: number, // delay in ms (default: 200)
options?: PrinterOptions
}Multi-Printer Printing
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(`${completed}/${total}`),
},
}
const result = await ThermalPrinter.printReceipt(job)
// result.success, result.results (per-printer status)Vietnamese Support
{
options: {
encoding: 'CP1258'
}
}Error Handling
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?
}Troubleshooting
Vietnamese not printing?
- Use
encoding: 'CP1258' - Test with
testCodepages()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
License
MIT
