npm package: @finan-me/react-native-thermal-printer
Description: React Native Thermal Printer Library with ESC/POS, CPCL, TSPL support
Author: Finan
Latest version: 1.0.9

# @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

```bash
yarn add @finan-me/react-native-thermal-printer
```

### iOS Setup

```bash
cd ios && pod install
```

Add to `Info.plist`:

```xml
<!-- 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`:

```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:**

```xml
<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:**

```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()
}
```

## Quick Start

```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)
```

## 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

```typescript
{
  paperWidthMm?: 32 | 58 | 80,             // default: 58
  encoding?: 'CP1258' | 'UTF8' | 'ASCII',  // default: CP1258
  marginMm?: number,                        // default: 1mm each side
  keepAlive?: boolean
}
```

### Job Options

```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
}
```

### Print Configuration

```typescript
{
  address: string,
  copies?: number,              // number of copies (default: 1)
  delayBetweenCopies?: number,  // delay in ms (default: 200)
  options?: PrinterOptions
}
```

## Multi-Printer Printing

```typescript
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

```typescript
{
  options: {
    encoding: 'CP1258'
  }
}
```

## Error Handling

```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?
}
```

## 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