Expo module for controlling Bixolon thermal printers with Bluetooth connectivity
npm install @sincpro/printer-expo


A powerful React Native module for controlling thermal printers in Expo applications. Built with Clean Architecture and Hexagonal Architecture (Ports & Adapters) for maximum flexibility and maintainability.
- 🔗 Multiple Connectivity: Bluetooth, WiFi, and USB support
- 🖨️ Advanced Printing: Text, QR codes, barcodes, images, and PDFs
- 📋 Structured Receipts: Header/body/footer with flexible line types
- ⚙️ Configurable: Margins, density, speed, orientation, auto-cutter
- 🏗️ Clean Architecture: SOLID principles, testable, swappable adapters
- 📚 Official SDK: Integration with Bixolon SDK (extensible to other brands)
- 📝 TypeScript: 100% type-safe API with comprehensive definitions
- 🛠️ Easy Setup: Simple installation and minimal configuration
- 📱 Android Focus: Optimized for Android thermal printers
``bash`
npm install @sincpro/printer-expoor
yarn add @sincpro/printer-expo
- Expo SDK: >=52.0.0
- React Native: Compatible with Expo SDK
- Platform: Android (iOS not currently supported)
`bashRebuild native modules
npx expo prebuild --clean
⚙️ Configuration
$3
The module requires Bluetooth permissions. Add to your
app.json:`json
{
"expo": {
"android": {
"permissions": [
"android.permission.BLUETOOTH",
"android.permission.BLUETOOTH_ADMIN",
"android.permission.BLUETOOTH_SCAN",
"android.permission.BLUETOOTH_CONNECT",
"android.permission.ACCESS_FINE_LOCATION",
"android.permission.ACCESS_COARSE_LOCATION"
]
}
}
}
`Note: These permissions are required for Bluetooth device discovery and connection on Android 12+.
🚀 Quick Start
$3
`typescript
import { bluetooth, connection, config, print } from '@sincpro/printer-expo';// 1. Get paired devices
const devices = bluetooth.getPairedDevices();
const printers = devices.filter(d => d.isPrinter);
// 2. Connect to printer via Bluetooth
await connection.connectBluetooth(printers[0].address, 30000); // 30s timeout
// 3. Optional: Configure printer
await config.set({
marginLeft: 10,
marginTop: 5,
density: 'dark',
speed: 'medium',
orientation: 'top_to_bottom',
autoCutter: { enabled: true, fullCut: true }
});
// 4. Print a simple text
await print.text('Hello World!', {
fontSize: 'large',
alignment: 'center',
bold: true,
media: { preset: 'continuous80mm' }
});
// 5. Print a complete receipt
await print.receipt({
header: [
{ type: 'text', content: 'MY STORE', fontSize: 'large', alignment: 'center', bold: true },
{ type: 'text', content: '123 Main Street', alignment: 'center' },
{ type: 'separator' },
],
body: [
{ type: 'keyValue', key: 'Product 1', value: '$10.00' },
{ type: 'keyValue', key: 'Product 2', value: '$15.00' },
{ type: 'separator' },
{ type: 'keyValue', key: 'TOTAL', value: '$25.00', bold: true },
],
footer: [
{ type: 'qr', data: 'https://mystore.com/receipt/123', alignment: 'center' },
{ type: 'text', content: 'Thank you!', alignment: 'center' },
{ type: 'space', lines: 2 },
]
}, { media: { preset: 'continuous80mm' } });
// 6. Disconnect
await connection.disconnect();
`$3
`typescript
import React, { useState } from 'react';
import { View, Button, FlatList, Text, Alert } from 'react-native';
import { bluetooth, connection, print } from '@sincpro/printer-expo';
import type { BluetoothDevice } from '@sincpro/printer-expo';export default function PrinterScreen() {
const [devices, setDevices] = useState([]);
const [connected, setConnected] = useState(false);
const scanDevices = async () => {
try {
const foundDevices = bluetooth.getPairedDevices();
setDevices(foundDevices.filter((d) => d.isPrinter));
} catch (error) {
Alert.alert('Error', error.message);
}
};
const connectDevice = async (device: BluetoothDevice) => {
try {
await connection.connectBluetooth(device.address, 30000);
setConnected(true);
Alert.alert('Success',
Connected to ${device.name});
} catch (error) {
Alert.alert('Error', 'Connection failed');
}
}; const printTest = async () => {
try {
await print.receipt({
header: [
{ type: 'text', content: 'Test Receipt', fontSize: 'large', alignment: 'center' },
{ type: 'separator' },
],
body: [{ type: 'text', content: 'This is a test print' }],
footer: [{ type: 'space', lines: 2 }],
});
Alert.alert('Success', 'Receipt printed');
} catch (error) {
Alert.alert('Error', error.message);
}
};
return (
data={devices}
keyExtractor={(item) => item.address}
renderItem={({ item }) => (
---
📚 API Reference
$3
####
bluetooth.getPairedDevices(): BluetoothDevice[]Get all paired/bonded Bluetooth devices (synchronous).
`typescript
import { bluetooth } from '@sincpro/printer-expo';const devices = bluetooth.getPairedDevices();
const printers = devices.filter(d => d.isPrinter);
`####
bluetooth.getPairedPrinters(): PairedPrinter[]Get only paired devices that are printers (synchronous).
`typescript
const printers = bluetooth.getPairedPrinters();
console.log(printers); // [{ name: 'SPP-R200III', address: '00:11:22:AA:BB:CC' }]
`---
####
connection.connectBluetooth(address: string, timeoutMs?: number): PromiseConnect to a printer via Bluetooth.
Parameters:
-
address: MAC address of the printer (e.g., "00:11:22:AA:BB:CC")
- timeoutMs: Connection timeout in milliseconds (default: 10000)`typescript
import { connection } from '@sincpro/printer-expo';await connection.connectBluetooth('00:11:22:AA:BB:CC', 30000);
`####
connection.connectWifi(ip: string, port?: number, timeoutMs?: number): PromiseConnect to a printer via WiFi.
Parameters:
-
ip: IP address (e.g., "192.168.1.100")
- port: TCP port (default: 9100)
- timeoutMs: Connection timeout in milliseconds (default: 10000)`typescript
await connection.connectWifi('192.168.1.100', 9100, 30000);
`####
connection.connectUsb(): PromiseConnect to a printer via USB.
`typescript
await connection.connectUsb();
`####
connection.disconnect(): PromiseDisconnect from the current printer.
`typescript
await connection.disconnect();
`####
connection.isConnected(): booleanCheck if currently connected (synchronous).
`typescript
const connected = connection.isConnected();
`####
connection.getStatus(): PromiseGet current printer status (paper, cover, errors).
`typescript
const status = await connection.getStatus();
console.log('Connection:', status.connectionState); // 'CONNECTED' | 'CONNECTING' | 'DISCONNECTED' | 'ERROR'
console.log('Has paper:', status.hasPaper);
console.log('Cover open:', status.isCoverOpen);
console.log('Error:', status.errorMessage);
`####
connection.getInfo(): PromiseGet printer information (model, firmware, serial).
`typescript
const info = await connection.getInfo();
console.log('Model:', info.model);
console.log('Firmware:', info.firmware);
console.log('Serial:', info.serial);
console.log('DPI:', info.dpi);
`####
connection.getDpi(): numberGet printer DPI (synchronous).
`typescript
const dpi = connection.getDpi(); // e.g., 203 or 300
`---
$3
####
config.set(printerConfig: PrinterConfig): PromiseSet printer configuration (margins, density, speed, orientation, cutter). This sets the default config and applies it immediately if connected.
`typescript
import { config } from '@sincpro/printer-expo';await config.set({
marginLeft: 10, // Left margin in dots
marginTop: 5, // Top margin in dots
density: 'dark', // 'light' | 'medium' | 'dark' | 'extra_dark'
speed: 'medium', // 'slow' | 'medium' | 'fast' | 'extra_fast'
orientation: 'top_to_bottom', // 'top_to_bottom' | 'bottom_to_top'
autoCutter: {
enabled: true,
fullCut: true // true = full cut, false = partial cut
}
});
`####
config.get(): PrinterConfigGet current printer configuration (synchronous).
`typescript
const currentConfig = config.get();
console.log('Density:', currentConfig.density);
console.log('Speed:', currentConfig.speed);
`---
$3
####
print.text(text: string, options?: PrintTextOptions): PromisePrint a single line of text.
Options:
-
fontSize: 'small' | 'medium' | 'large' | 'xlarge'
- alignment: 'left' | 'center' | 'right'
- bold: boolean
- media: MediaConfig`typescript
import { print } from '@sincpro/printer-expo';await print.text('Hello World!', {
fontSize: 'large',
alignment: 'center',
bold: true,
media: { preset: 'continuous80mm' }
});
`####
print.texts(texts: string[], options?: PrintTextsOptions): PromisePrint multiple lines of text.
`typescript
await print.texts(
['Line 1', 'Line 2', 'Line 3'],
{ fontSize: 'medium', media: { preset: 'continuous80mm' } }
);
`####
print.qr(data: string, options?: PrintQROptions): PromisePrint a QR code.
Options:
-
size: QR size 1-10 (default: 5)
- alignment: 'left' | 'center' | 'right'
- media: MediaConfig`typescript
await print.qr('https://sincpro.com', {
size: 8,
alignment: 'center',
media: { preset: 'continuous80mm' }
});
`####
print.barcode(data: string, options?: PrintBarcodeOptions): PromisePrint a barcode.
Options:
-
type: 'CODE128' | 'CODE39' | 'EAN13' | 'EAN8' | 'UPCA' | 'UPCE' | 'CODE93' | 'CODABAR'
- height: Barcode height in dots
- alignment: 'left' | 'center' | 'right'
- media: MediaConfig`typescript
await print.barcode('123456789012', {
type: 'CODE128',
height: 80,
alignment: 'center',
media: { preset: 'continuous80mm' }
});
`####
print.imageBase64(base64Data: string, options?: PrintImageOptions): PromisePrint an image from base64 data.
Options:
-
alignment: 'left' | 'center' | 'right'
- media: MediaConfig`typescript
await print.imageBase64(base64ImageData, {
alignment: 'center',
media: { preset: 'continuous80mm' }
});
`####
print.pdfBase64(base64Data: string, options?: PrintPdfOptions): PromisePrint a PDF page from base64 data.
Options:
-
page: Page number to print (default: 0)
- alignment: 'left' | 'center' | 'right'
- media: MediaConfig`typescript
await print.pdfBase64(base64PdfData, {
page: 0,
alignment: 'center',
media: { preset: 'continuous80mm' }
});
`####
print.getPdfPageCount(base64Data: string): numberGet page count from a PDF (synchronous).
`typescript
const pageCount = print.getPdfPageCount(base64PdfData);
`####
print.keyValue(key: string, value: string, options?: PrintKeyValueOptions): PromisePrint a key-value pair (two columns).
`typescript
await print.keyValue('Total', '$25.00', {
fontSize: 'large',
bold: true,
media: { preset: 'continuous80mm' }
});
`####
print.receipt(receipt: Receipt, options?: PrintReceiptOptions): PromisePrint a complete structured receipt with header, body, and footer sections.
Options:
-
media: MediaConfig
- copies: Number of copies to print (default: 1)`typescript
await print.receipt({
header: [
{ type: 'text', content: 'MY STORE', fontSize: 'large', alignment: 'center', bold: true },
{ type: 'text', content: '123 Main Street', alignment: 'center' },
{ type: 'separator' },
],
body: [
{ type: 'keyValue', key: 'Product 1', value: '$10.00' },
{ type: 'keyValue', key: 'Product 2', value: '$15.00' },
{ type: 'separator' },
{ type: 'keyValue', key: 'Tax', value: '$2.50' },
],
footer: [
{ type: 'separator' },
{ type: 'keyValue', key: 'TOTAL', value: '$27.50', bold: true, fontSize: 'large' },
{ type: 'qr', data: 'https://mystore.com/receipt/123', alignment: 'center', size: 6 },
{ type: 'text', content: 'Thank you for your purchase!', alignment: 'center' },
{ type: 'space', lines: 2 },
]
}, { media: { preset: 'continuous80mm' }, copies: 1 });
`---
$3
Receipt lines are the building blocks of structured receipts. Each line type has specific properties.
####
TextLinePrint formatted text with customizable style.
`typescript
{
type: 'text',
content: string,
fontSize?: 'small' | 'medium' | 'large' | 'xlarge',
bold?: boolean,
alignment?: 'left' | 'center' | 'right'
}
`Example:
`typescript
{ type: 'text', content: 'INVOICE', fontSize: 'xlarge', alignment: 'center', bold: true }
`####
KeyValueLinePrint key-value pairs in two columns (common in receipts).
`typescript
{
type: 'keyValue',
key: string,
value: string,
fontSize?: 'small' | 'medium' | 'large' | 'xlarge',
bold?: boolean
}
`Example:
`typescript
{ type: 'keyValue', key: 'Subtotal', value: '$25.00' }
{ type: 'keyValue', key: 'TOTAL', value: '$27.50', bold: true, fontSize: 'large' }
`####
QRLineEmbed QR codes in receipts.
`typescript
{
type: 'qr',
data: string,
size?: number, // 1-10, default: 5
alignment?: 'left' | 'center' | 'right'
}
`Example:
`typescript
{ type: 'qr', data: 'https://store.com/receipt/12345', size: 6, alignment: 'center' }
`####
BarcodeLineEmbed barcodes in receipts.
`typescript
{
type: 'barcode',
data: string,
barcodeType?: 'CODE128' | 'CODE39' | 'EAN13' | 'EAN8' | 'UPCA' | 'UPCE' | 'CODE93' | 'CODABAR',
height?: number,
alignment?: 'left' | 'center' | 'right'
}
`Example:
`typescript
{ type: 'barcode', data: '123456789012', barcodeType: 'EAN13', height: 80, alignment: 'center' }
`####
ImageLineEmbed images (base64) in receipts.
`typescript
{
type: 'image',
base64: string,
alignment?: 'left' | 'center' | 'right'
}
`Example:
`typescript
{ type: 'image', base64: 'iVBORw0KGgoAAAANS...', alignment: 'center' }
`####
SeparatorLinePrint horizontal separator lines.
`typescript
{
type: 'separator',
char?: string, // Character to repeat, default: '-'
length?: number // Line length in characters, default: 48
}
`Examples:
`typescript
{ type: 'separator' }
{ type: 'separator', char: '=', length: 32 }
`####
SpaceLineAdd blank lines for spacing.
`typescript
{
type: 'space',
lines?: number // Number of blank lines, default: 1
}
`Example:
`typescript
{ type: 'space', lines: 2 }
`####
ColumnsLinePrint multiple columns in one row.
`typescript
{
type: 'columns',
columns: Array<{
text: string,
widthRatio?: number,
alignment?: 'left' | 'center' | 'right'
}>,
fontSize?: 'small' | 'medium' | 'large' | 'xlarge',
bold?: boolean
}
`Example:
`typescript
{
type: 'columns',
columns: [
{ text: 'Item', widthRatio: 2, alignment: 'left' },
{ text: 'Qty', widthRatio: 1, alignment: 'center' },
{ text: 'Price', widthRatio: 1, alignment: 'right' }
],
bold: true
}
`---
$3
Configure paper/label dimensions for printing. You can use presets or custom configurations.
#### Using Presets
`typescript
// Continuous paper presets
{ preset: 'continuous58mm' } // 58mm continuous paper
{ preset: 'continuous72mm' } // 72mm continuous paper
{ preset: 'continuous80mm' } // 80mm continuous paper (most common)
`#### Custom Configuration (Millimeters)
`typescript
// Custom continuous paper
{
widthMm: 72,
type: 'continuous'
}// Labels with gap
{
widthMm: 50,
heightMm: 30,
gapMm: 3,
type: 'gap'
}
// Labels with black mark
{
widthMm: 60,
heightMm: 40,
type: 'black_mark'
}
`#### Custom Configuration (Dots)
If you need precise control, you can specify dimensions in dots (based on printer DPI).
`typescript
{
widthDots: 576, // 72mm at 203 DPI
heightDots: 240, // 30mm at 203 DPI
gapDots: 24, // 3mm at 203 DPI
type: 'gap'
}
`Note: The module automatically converts millimeters to dots based on the printer's DPI. Using millimeters is recommended for easier configuration.
---
📦 TypeScript Types
$3
`typescript
interface BluetoothDevice {
name: string; // Device name
address: string; // MAC address (e.g., "00:11:22:AA:BB:CC")
isPrinter: boolean; // True if device is identified as a printer
}
`$3
`typescript
interface PairedPrinter {
name: string; // Printer name
address: string; // MAC address
}
`$3
`typescript
interface PrinterStatus {
connectionState: 'DISCONNECTED' | 'CONNECTING' | 'CONNECTED' | 'ERROR';
hasPaper: boolean; // True if paper is available
isCoverOpen: boolean; // True if printer cover is open
isOverheated: boolean; // True if printer is overheated
hasError: boolean; // True if printer has an error
errorMessage: string | null; // Error message if hasError is true
}
`$3
`typescript
interface PrinterInfo {
model: string; // Printer model (e.g., "SPP-R200III")
firmware: string; // Firmware version
serial: string; // Serial number
dpi: number; // Printer DPI (e.g., 203 or 300)
}
`$3
`typescript
interface PrinterConfig {
marginLeft?: number; // Left margin in dots
marginTop?: number; // Top margin in dots
density?: 'light' | 'medium' | 'dark' | 'extra_dark';
speed?: 'slow' | 'medium' | 'fast' | 'extra_fast';
orientation?: 'top_to_bottom' | 'bottom_to_top';
autoCutter?: {
enabled: boolean; // Enable auto cutter
fullCut?: boolean; // Full cut (true) or partial cut (false)
};
}
`$3
`typescript
interface MediaConfig {
// Use preset for common paper sizes
preset?: 'continuous58mm' | 'continuous72mm' | 'continuous80mm';
// Or custom configuration in millimeters
widthMm?: number; // Paper width in mm
heightMm?: number; // Label height in mm (for labels)
gapMm?: number; // Gap size in mm (for labels)
// Or custom configuration in dots
widthDots?: number; // Paper width in dots
heightDots?: number; // Label height in dots
gapDots?: number; // Gap size in dots
// Media type
type?: 'continuous' | 'gap' | 'label' | 'black_mark';
}
`$3
`typescript
interface Receipt {
header?: ReceiptLine[]; // Header section (logo, store info)
body?: ReceiptLine[]; // Body section (items, details)
footer?: ReceiptLine[]; // Footer section (totals, QR, thanks)
}
`$3
`typescript
type ReceiptLine =
| TextLine
| KeyValueLine
| QRLine
| BarcodeLine
| ImageLine
| SeparatorLine
| SpaceLine
| ColumnsLine;
`---
🎯 Usage Examples
$3
`typescript
import { bluetooth, connection, print } from '@sincpro/printer-expo';async function basicPrint() {
try {
// 1. Get paired printers
const printers = bluetooth.getPairedPrinters();
if (printers.length === 0) {
throw new Error('No paired printers found');
}
// 2. Connect to first printer
await connection.connectBluetooth(printers[0].address, 30000);
console.log('Connected to', printers[0].name);
// 3. Print simple text
await print.text('Hello from Expo!', {
fontSize: 'large',
alignment: 'center',
bold: true,
media: { preset: 'continuous80mm' }
});
// 4. Disconnect
await connection.disconnect();
console.log('Print complete!');
} catch (error) {
console.error('Print failed:', error);
}
}
`$3
`typescript
import { connection, print } from '@sincpro/printer-expo';async function printSalesReceipt(items: Array<{ name: string; qty: number; price: number }>) {
const subtotal = items.reduce((sum, item) => sum + (item.qty * item.price), 0);
const tax = subtotal * 0.08; // 8% tax
const total = subtotal + tax;
await print.receipt({
header: [
{ type: 'text', content: '🏪 MY RETAIL STORE', fontSize: 'xlarge', alignment: 'center', bold: true },
{ type: 'text', content: '123 Commerce Street', alignment: 'center' },
{ type: 'text', content: 'Phone: (555) 123-4567', alignment: 'center' },
{ type: 'separator', char: '=' },
{ type: 'text', content: 'SALES RECEIPT', fontSize: 'large', alignment: 'center' },
{ type: 'separator', char: '=' },
{ type: 'space' },
{ type: 'keyValue', key: 'Date', value: new Date().toLocaleDateString() },
{ type: 'keyValue', key: 'Receipt #', value: 'RCP-' + Date.now() },
{ type: 'space' },
{ type: 'separator' },
],
body: [
{ type: 'text', content: 'ITEMS', bold: true },
{ type: 'separator' },
...items.map(item => ({
type: 'keyValue' as const,
key:
${item.name} (x${item.qty}),
value: $${(item.qty * item.price).toFixed(2)}
})),
{ type: 'space' },
{ type: 'separator' },
{ type: 'keyValue', key: 'Subtotal', value: $${subtotal.toFixed(2)} },
{ type: 'keyValue', key: 'Tax (8%)', value: $${tax.toFixed(2)} },
],
footer: [
{ type: 'separator', char: '=' },
{ type: 'keyValue', key: 'TOTAL', value: $${total.toFixed(2)}, fontSize: 'large', bold: true },
{ type: 'separator', char: '=' },
{ type: 'space', lines: 2 },
{ type: 'text', content: 'Thank you for your purchase!', alignment: 'center' },
{ type: 'text', content: 'Visit us at www.mystore.com', alignment: 'center' },
{ type: 'space' },
{ type: 'qr', data: https://mystore.com/receipt/${Date.now()}, size: 6, alignment: 'center' },
{ type: 'space', lines: 3 },
]
}, { media: { preset: 'continuous80mm' } });
}// Usage
printSalesReceipt([
{ name: 'Coffee', qty: 2, price: 4.50 },
{ name: 'Croissant', qty: 1, price: 3.25 },
{ name: 'Orange Juice', qty: 1, price: 2.75 }
]);
`$3
`typescript
import { config, print } from '@sincpro/printer-expo';async function printProductLabel(product: { name: string; sku: string; price: number }) {
// Configure for label printing
await config.set({
marginLeft: 5,
marginTop: 5,
density: 'dark',
speed: 'medium'
});
await print.receipt({
header: [
{ type: 'text', content: product.name, fontSize: 'large', alignment: 'center', bold: true },
{ type: 'space' },
],
body: [
{ type: 'barcode', data: product.sku, barcodeType: 'CODE128', height: 60, alignment: 'center' },
{ type: 'space' },
{ type: 'text', content:
SKU: ${product.sku}, alignment: 'center' },
],
footer: [
{ type: 'space' },
{ type: 'text', content: $${product.price.toFixed(2)}, fontSize: 'xlarge', alignment: 'center', bold: true },
]
}, {
media: {
widthMm: 50,
heightMm: 30,
gapMm: 3,
type: 'gap'
}
});
}// Usage
printProductLabel({
name: 'Premium Coffee Beans',
sku: '1234567890',
price: 12.99
});
`$3
`typescript
import { connection, print } from '@sincpro/printer-expo';
import type { Receipt } from '@sincpro/printer-expo';async function safePrint(receipt: Receipt) {
try {
// Check connection
if (!connection.isConnected()) {
throw new Error('Printer not connected');
}
// Check printer status
const status = await connection.getStatus();
if (status.connectionState !== 'CONNECTED') {
throw new Error(
Printer is ${status.connectionState.toLowerCase()});
}
if (!status.hasPaper) {
throw new Error('Printer is out of paper');
}
if (status.isCoverOpen) {
throw new Error('Printer cover is open');
}
if (status.isOverheated) {
throw new Error('Printer is overheated');
}
if (status.hasError) {
throw new Error(Printer error: ${status.errorMessage});
} // Print receipt
await print.receipt(receipt, { media: { preset: 'continuous80mm' } });
console.log('✅ Print successful');
return { success: true };
} catch (error) {
console.error('❌ Print failed:', error);
// Return user-friendly error
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
// Usage in React component
function PrintButton() {
const handlePrint = async () => {
const result = await safePrint(myReceipt);
if (result.success) {
Alert.alert('Success', 'Receipt printed successfully');
} else {
Alert.alert('Print Error', result.error);
}
};
return ;
}
`$3
`typescript
import { useState, useEffect } from 'react';
import { bluetooth, connection, print } from '@sincpro/printer-expo';
import type { BluetoothDevice, PrinterStatus, Receipt } from '@sincpro/printer-expo';export function usePrinter() {
const [devices, setDevices] = useState([]);
const [connected, setConnected] = useState(false);
const [status, setStatus] = useState(null);
useEffect(() => {
checkConnection();
}, []);
const checkConnection = () => {
setConnected(connection.isConnected());
};
const scanDevices = () => {
try {
const foundDevices = bluetooth.getPairedDevices();
setDevices(foundDevices.filter(d => d.isPrinter));
return foundDevices;
} catch (error) {
console.error('Scan failed:', error);
throw error;
}
};
const connect = async (address: string, timeoutMs = 30000) => {
try {
await connection.connectBluetooth(address, timeoutMs);
setConnected(true);
const printerStatus = await connection.getStatus();
setStatus(printerStatus);
return true;
} catch (error) {
console.error('Connection failed:', error);
throw error;
}
};
const disconnect = async () => {
try {
await connection.disconnect();
setConnected(false);
setStatus(null);
return true;
} catch (error) {
console.error('Disconnection failed:', error);
throw error;
}
};
const printReceipt = async (receipt: Receipt) => {
if (!connected) {
throw new Error('Not connected to printer');
}
await print.receipt(receipt, { media: { preset: 'continuous80mm' } });
};
const refreshStatus = async () => {
if (connected) {
const printerStatus = await connection.getStatus();
setStatus(printerStatus);
return printerStatus;
}
return null;
};
return {
devices,
connected,
status,
scanDevices,
connect,
disconnect,
printReceipt,
refreshStatus,
};
}
// Usage in component
function MyPrinterComponent() {
const printer = usePrinter();
return (
title="Print Receipt"
onPress={() => printer.printReceipt(myReceipt)}
disabled={!printer.connected}
/>
{printer.status && (
Status: {printer.status.connectionState}
)}
);
}
`---
🏗️ Architecture
This module follows Clean Architecture with Hexagonal Architecture (Ports & Adapters) principles for maximum maintainability and extensibility.
$3
`
┌─────────────────────────────────────────────────────────┐
│ Dependencies ALWAYS point inward (toward Domain) │
└─────────────────────────────────────────────────────────┘TypeScript (React Native)
↓
ENTRYPOINT ← Expo Modules API bridge
↓
SERVICE ← Use cases & orchestration
↓
DOMAIN ← Business entities & rules (interfaces)
↑
┌─────┴─────┐
ADAPTER INFRASTRUCTURE
(Vendors) (Platform APIs)
`$3
| Layer | Purpose | Examples |
|-------|---------|----------|
| TypeScript | React Native API |
bluetooth, connection, config, print |
| Entrypoint | Expo ↔ Kotlin bridge | PrinterModule.kt |
| Service | Business logic | ConnectivityService, PrintService |
| Domain | Contracts & entities | IPrinter, Receipt, MediaConfig |
| Adapter | Vendor SDKs | BixolonPrinterAdapter |
| Infrastructure | Platform utilities | AndroidBluetoothProvider, BinaryConverter |$3
- ✅ Testable: Mock adapters and services independently
- ✅ Maintainable: Clear separation of concerns
- ✅ Extensible: Easy to add new printer brands (Zebra, Epson, Star, etc.)
- ✅ Swappable: Change implementations without affecting business logic
- ✅ Framework-independent: Domain layer has no Android/iOS dependencies
$3
The architecture makes it easy to support additional printer brands:
1. Create Adapter: Implement
IPrinter interface for new vendor SDK
2. Register in SDK: Add to SincproPrinterSdk entry point
3. No changes needed: Business logic and API remain unchangedSee ARCHITECTURE.md for detailed architecture documentation.
---
🖨️ Supported Printers
$3
This module currently supports Bixolon thermal printers via the official Bixolon SDK.
#### Tested Models
- ✅ SPP-R200III - 2" mobile printer (58mm)
- ✅ SPP-R300 - 3" mobile printer (80mm)
- ✅ SPP-R400 - 4" mobile printer (112mm)
- ✅ SRP-275III - 3" desktop printer (80mm)
- ✅ SRP-350III - 3" desktop printer (80mm)
- ✅ SRP-352III - 3" desktop printer (80mm)
#### Compatible Models
The following Bixolon models should work but have not been tested:
- SPP-R210, SPP-R220, SPP-R310, SPP-R410
- SRP-330II, SRP-350plusIII, SRP-380
- XD3-40d, XD5-40d
- XT5-40, XT5-43
#### Connectivity Support
| Connection Type | Status | Notes |
|----------------|--------|-------|
| Bluetooth | ✅ Fully supported | Most common for mobile printers |
| WiFi | ✅ Supported | For network-connected printers |
| USB | ⚠️ Limited | Requires USB OTG on Android |
$3
The architecture supports adding other printer brands. To add support:
1. Implement the
IPrinter interface in a new adapter
2. Integrate the vendor's SDK (Zebra, Epson, Star, etc.)
3. Register the adapter in the SDK entry pointSee CONTRIBUTING.md for development guidelines.
---
🛠️ Troubleshooting
$3
Problem: Connection fails or times out
Solutions:
1. Verify Bluetooth is enabled on the device
2. Check device is paired:
bluetooth.getPairedDevices()
3. Ensure printer is powered on and in range (< 10 meters)
4. Try increasing timeout: connectBluetooth(address, 60000) (60 seconds)
5. Restart printer and retry connectionExample:
`typescript
try {
await connection.connectBluetooth(address, 60000);
} catch (error) {
console.error('Connection failed:', error);
// Try restarting printer
}
`$3
Problem: Print command succeeds but nothing prints
Solutions:
1. Check printer status:
`typescript
const status = await connection.getStatus();
if (!status.hasPaper) console.error('No paper!');
if (status.isCoverOpen) console.error('Cover open!');
`
2. Verify connection: connection.isConnected()
3. Check media configuration matches paper type
4. Ensure printer is not in an error state$3
Problem: Paper doesn't feed after printing
Solutions:
1. Add space lines at the end of receipt:
`typescript
footer: [
// ... other lines
{ type: 'space', lines: 3 } // Add extra space
]
`
2. Configure auto-cutter:
`typescript
await config.set({
autoCutter: { enabled: true, fullCut: true }
});
`$3
Problem: "Permission denied" errors
Solution: Ensure permissions are declared in
app.json and granted at runtime. Android 12+ requires runtime permission grants.`json
{
"expo": {
"android": {
"permissions": [
"android.permission.BLUETOOTH",
"android.permission.BLUETOOTH_ADMIN",
"android.permission.BLUETOOTH_SCAN",
"android.permission.BLUETOOTH_CONNECT"
]
}
}
}
`$3
Problem: Images or QR codes don't appear
Solutions:
1. Check base64 data is valid
2. Ensure alignment is correct
3. Try different QR size (1-10)
4. Verify printer supports graphics
$3
Problem:
Module "SincproPrinter" not foundSolutions:
1. Rebuild native modules:
`bash
npx expo prebuild --clean
npx expo run:android
`
2. Clear cache:
`bash
npm start -- --clear
`---
📖 Resources
- Package: NPM Package
- Repository: GitHub
- Architecture: ARCHITECTURE.md - Detailed architecture guide
- Contributing: CONTRIBUTING.md - Development guidelines
- Expo Modules: Official Documentation
- Bixolon: Official Website
---
🤝 Contributing
We welcome contributions! Please see CONTRIBUTING.md for:
- Development setup and environment
- Code standards (ktlint, Prettier, ESLint)
- Architecture guidelines and patterns
- Git workflow and branch naming
- Pull request process and review
- Testing requirements
$3
`bash
Clone the repository
git clone https://github.com/Sincpro-SRL/sincpro_printer_expo.git
cd sincpro_printer_expoInstall dependencies
npm installBuild TypeScript
npm run buildFormat code
npm run format
npm run format:kotlinLint code
npm run lint
npm run lint:kotlin
``---
MIT License - see LICENSE file for details.
Copyright (c) 2024 Sincpro SRL
---
- Bixolon for the official printer SDK
- Expo team for the Modules API
- Contributors and testers who helped improve this module
- Open source community for inspiration and support
---
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Email: support@sincpro.com
---
Made with ❤️ by Sincpro SRL